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.
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: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
What I've verified
window.getDigitalGoodsServiceexistsPaymentRequestexistsgetDigitalGoodsService("https://play.google.com/billing")succeedsservice.getDetails()returns all products correctlycanMakePayment()returnstruefynz_mensal)Sentry logs
The exception occurs exactly here:
The Google Play purchase dialog is never displayed.
Code:
Device tests
Using the same Google account:
Working
Failing
The behavior changes depending on the device, even when using the same Google account.
Notes
Everything before
request.show()succeeds:canMakePayment()returnstrueThe only failing step is
request.show(), which immediately throwsAbortError: 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()succeedscanMakePayment()returnstrueAny 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.