Skip to content

TWA Play Billing - PaymentRequest.show() fails with AbortError: Invalid state on some devices while working on others #1046

Description

@t-heu

Description

I'm integrating Google Play Billing in a Trusted Web Activity using the Digital Goods API.

The implementation follows the official documentation:
https://developer.chrome.com/docs/android/trusted-web-activity/receive-payments-play-billing

Everything works correctly on some devices, but on others PaymentRequest.show() immediately throws:

AbortError: Invalid state.

The strange part is that the exact same app version and even the same Google account work on one device but fail on another.

Environment

  • Trusted Web Activity generated with PWABuilder
  • App distributed through Google Play Closed Testing
  • Installed directly from Google Play (not sideloaded)
  • Google Play Billing enabled
  • Digital Goods API
  • Payment Request API

What I've verified

  • window.getDigitalGoodsService exists
  • PaymentRequest exists
  • getDigitalGoodsService("https://play.google.com/billing") succeeds
  • service.getDetails() returns all products correctly
  • canMakePayment() returns true
  • The correct SKU is found (fynz_mensal)
  • Products are Active in Play Console
  • Testers are enrolled in Closed Testing
  • License testers are configured
  • The app is installed from the Play Store
  • Same AAB version on every tested device

Sentry logs

preload:start
supportsGoogleBilling=true

preload:products-loaded
productsFound=[
  "fynz_mensal",
  "fynz_semestral",
  "fynz_anual"
]

preload:done
canMakePayment=true

checkout
googleBillingReady=true

route-decided
isGoogleBilling=true

before-show

182ms later

AbortError: Invalid state.

The exception occurs exactly here:

const response = await request.show();

The Google Play purchase dialog is never displayed.

Code:

const [googleBilling, setGoogleBilling] = useState<{
    service: any;
    products: any[];
    canMakePayment: boolean;
  } | null>(null);

  useEffect(() => {
    let cancelled = false;

    (async () => {
      const supportsGoogleBilling =
        typeof (window as any).getDigitalGoodsService === "function" &&
        typeof (window as any).PaymentRequest === "function";

      if (!supportsGoogleBilling) return;

      try {
        const service = await (window as any).getDigitalGoodsService(
          "https://play.google.com/billing"
        );

        const googleIds = [
          "fynz_mensal",
          "fynz_semestral",
          "fynz_anual",
        ];

        const products = await service.getDetails(googleIds);

        const testRequest = new PaymentRequest(
          [{ supportedMethods: "https://play.google.com/billing", data: { sku: googleIds[0] } }],
          { total: { label: "check", amount: { currency: "BRL", value: "0.00" } } }
        );
        const canMakePayment = await testRequest.canMakePayment();

        if (!cancelled) {
          setGoogleBilling({ service, products, canMakePayment });
        }
      } catch (e) {
        console.warn("Google Play Billing indisponível:", e);
      }
    })();

    return () => {
      cancelled = true;
    };
  }, []);

  async function iniciarAssinatura(priceId: string) {
    if (loadingCheck) return;

    try {
      setLoadingCheck(priceId);

      let googlePlayId = "";

      switch (priceId) {
        case process.env.NEXT_PUBLIC_STRIPE_PLAN_MENSAL:
          googlePlayId = "fynz_mensal";
          break;

        case process.env.NEXT_PUBLIC_STRIPE_PLAN_SEMESTRAL:
          googlePlayId = "fynz_semestral";
          break;

        case process.env.NEXT_PUBLIC_STRIPE_PLAN_ANUAL:
          googlePlayId = "fynz_anual";
          break;
      }

      const isGoogleBilling =
        googlePlayId !== "" &&
        googleBilling !== null &&
        googleBilling.canMakePayment &&
        googleBilling.products.some((p: any) => p.itemId === googlePlayId);

      if (isGoogleBilling) {
        try {
          const request = new PaymentRequest(
            [
              {
                supportedMethods: "https://play.google.com/billing",
                data: { sku: googlePlayId },
              },
            ],
            {
              total: {
                label: "Fynz Premium",
                amount: { currency: "BRL", value: "0.00" },
              },
            }
          );

          const response = await request.show();

          const purchaseToken =
            (response as any).details?.purchaseToken ??
            (response as any).details?.token;

          await response.complete("success");

          if (!purchaseToken) {
            showToast("error", "não recebido.");
            return;
          }

          const user = await getDadosUsuario();
          if (!user) {
            showToast("info", "Você precisa estar logado.");
            return;
          }

          const salvar = await fetch("/api/checkout/google", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              userId: user.id,
              googlePlayId,
              purchaseToken,
            }),
          });

          const json = await salvar.json();

          if (!salvar.ok) {
            showToast("error", json.error || "Erro ao salvar assinatura.");
            return;
          }

          location.reload();
        } catch (e) {
          Sentry.captureException(e);
          console.error(e);
          showToast("error", "Erro ao processar a compra.");
        }
        return;
      }

      const user = await getDadosUsuario();

      if (!user) {
        showToast("info", "Você precisa estar logado.");
        return;
      }

      const response = await fetch("/api/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          priceId,
          userId: user.id,
          userEmail: user.email,
        }),
      });

      const dados = await response.json();

      if (dados.url) {
        location.href = dados.url;
      } else {
        showToast("error", dados.error || "Erro ao iniciar assinatura.");
      }
    } catch (e) {
      Sentry.captureException(e);
      console.error(e);
      showToast("error", "Erro ao iniciar assinatura.");
    } finally {
      setLoadingCheck(null);
    }
  }

Device tests

Using the same Google account:

Working

  • Motorola device
  • Xiaomi (Android 13)

Failing

  • Samsung Galaxy A30 (Android 10)
  • Another Samsung device
  • Another Motorola device

The behavior changes depending on the device, even when using the same Google account.

Notes

Everything before request.show() succeeds:

  • Digital Goods API is available
  • Products are returned correctly
  • canMakePayment() returns true

The only failing step is request.show(), which immediately throws AbortError: Invalid state.

Question

Is this a known issue with Trusted Web Activities, the Digital Goods API, or the Payment Request API?

Are there any known device compatibility issues or additional debugging methods that could explain why request.show() is aborted even though:

  • getDetails() succeeds
  • canMakePayment() returns true
  • the app is installed from Google Play
  • Google Play Billing is correctly configured
  • the same Google account works on some devices but not on others

Any guidance would be greatly appreciated.

Additional finding

On one Samsung device, changing the default browser from Samsung Internet to Google Chrome made PaymentRequest.show() work immediately without changing the app or Google account.

This suggests the issue may depend on the browser implementation used by the TWA rather than the Play Billing configuration itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions