You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We recently released our TWA (our app) to customers and on day 1 are experiencing a very consistent issue with Google Play Billing. When we try to call getDetails() on a SKU as well as when we call listPurchases(), we receive a "DOMException: clientAppUnavailable", and the promise fails. Here are the tracebacks:
We are confident though that Play Services are being initialized:
After a lot of debugging, our current lead is that the issue may be with our Delegation Service. On Android 11, the Delegation Service runs and the extra command handler is registered successfully. On Android 13, the Delegation Service fails to run and a clientAppUnavailable DOM exception is raised. Below are all the files we believe are relevant:
web_app_manifest.json
{
"packageId": "com.coursicle.coursicle",
"host": "daniel.coursicle.com",
"short_name":"Coursicle",
"enableNotifications": true,
"features": {
"playBilling": {
"enabled": true
}
},
"alphaDependencies": {
"enabled": true
},
"name":"Coursicle | Plan your schedule and get into classes",
"start_url":"/?pwa=true",
"background_color":"#ffffff",
"display":"standalone",
"theme_color":"#ffffff",
"icons":[{"src":"/homepage/img/coursicleCLogo512.png",
"sizes":"512x512",
"type":"image/png",
"purpose":"any"}],
"screenshots":[{"src":"/homepage/img/screenshot1.png","type":"image/png"},
{"src":"/homepage/img/screenshot2.png","type":"image/png"},
{"src":"/homepage/img/screenshot3.png","type":"image/png"},
{"src":"/homepage/img/screenshot4.png","type":"image/png"},
{"src":"/homepage/img/screenshot5.png","type":"image/png"}]
}
// https://developer.chrome.com/docs/android/trusted-web-activity/receive-payments-play-billing/
window.initBilling = function(){
window.billingService
window.hostSite = window.location.host.split(".")[0];
$(document).ready(function(){
window.billingSemester = $('#semesterSelect').val();
});
// Confirms that google billing is available
// Should only be enabled if user has logged into their google play account
// Gets details for current semester product
// Updates UI to reflect details
var googleBilling = async function(){
if ('getDigitalGoodsService' in window) {
// Digital Goods API is supported!
try {
window.billingService = await window.getDigitalGoodsService('https://play.google.com/billing');
// Get details for most relevant product
var skuDetailFun = async function(){
var prodToShow = ""
if (window.hostSite == "www"){
prodToShow = "com.coursicle.coursicle."+window.billingSemester+"premium"
} else {
prodToShow = "dev.coursicle.coursicle."+window.billingSemester+"premium"
}
console.log(prodToShow);
var skuDetails = await window.billingService.getDetails([prodToShow]);
// There should only be one product in the return object
if (!hasPurchasedPremium()){
for (var index in skuDetails) {
var item = skuDetails[index]
// Format the price according to the user locale.
const localizedPrice = new Intl.NumberFormat(
navigator.language,
{style: 'currency', currency: item.price.currency}
).format(item.price.value);
$("#premiumButton").data('price', localizedPrice)
$("#premiumButton").text(localizedPrice)
}
}
}
skuDetailFun();
// Check and redeem purchases
// TODO-Miguel check and acknowledge in local storage
const existingPurchases = await window.billingService.listPurchases();
const userData = store.get('userData')
const premium = userData["premium"]
var relevantPremium = ""
for (const sem in premium){
if (sem == window.billingSemester){
relevantPremium = sem
}
}
//hasPurchasedPremium()
if (existingPurchases.length != 0 && relevantPremium != "" ) {
for (const p in existingPurchases) {
// TODO-Miguel comment out consume for prod
if (window.hostSite=="miguel") {
//window.billingService.consume(existingPurchases[p].purchaseToken)
//break;
}
// Update the UI with items the user is already entitled to.
var prodToShow = ""
if (window.hostSite == "www"){
prodToShow = "com.coursicle.coursicle."+window.billingSemester+"premium"
} else {
prodToShow = "dev.coursicle.coursicle."+window.billingSemester+"premium"
}
if (existingPurchases[p].itemId == prodToShow) {
// TODO-Miguel Add expiration date to settings screen
//$('#premiumButton').text("Purchased")
//$('#premiumButton').css("background-color","green")
var term = window.billingSemester.substring(0,window.billingSemester.length-4)
var year = window.billingSemester.substring(window.billingSemester.length-4)
var expirationDate = ""
if (term=="fall"){
expirationDate = "October"
} else if (term=="spring"){
expirationDate = "March"
} else if (term=="winter"){
expirationDate = "February"
}
$("#premiumSetting").find(".settingsValue").text("Expires " + expirationDate + " " + year)
}
}
}
} catch (error) {
console.log("Google Play Billing is not available. Use another payment flow.", error);
return;
}
}
}
// Execute google billing to get product details and accept payment
googleBilling();
}
// MAKE SURE you go to "chrome://flags/" and enabling billing for test devices
// This function is used to process payments for premium using the google billing API
async function makePurchase(sku) {
// Define the preferred payment method and item ID
const paymentMethods = [{
supportedMethods: ["https://play.google.com/billing"],
data: {
sku: sku,
},
}];
var request = new PaymentRequest(paymentMethods);
// launch purchase pop-up
try {
const paymentResponse = await request.show();
const {purchaseToken} = paymentResponse.details;
const paymentComplete = await paymentResponse.complete('success');
var currentSemesterPurchased = true
} catch (error) {
console.log(error)
if (error.message.includes('was cancelled')) {
// User dismissed native dialog
logWarning('User chose not to subscribe:', error);
} else {
// Report unexpected error
reportError(error, 'PaymentRequest.show() failed');
$('#premiumButton').text($('#premiumButton').data('price'))
$('#premiumSpinner').hide()
}
var currentSemesterPurchased = false
}
// Check and redeem purchases
try {
const existingPurchases = await window.billingService.listPurchases();
for (purchase in existingPurchases) { // TODO-Miguel check against storage and user data
if (purchase.itemId == sku) {
currentSemesterPurchased = true
}
}
}
catch (error) {
console.log("billingService error", error)
}
if (currentSemesterPurchased) {
$('#premiumSpinner').hide()
$('#premiumButton').text("Purchased")
$('#premiumButton').css("background-color","#4ea83c")
// Update the UI with items the user is already entitled to.
// TODO-Miguel Add expiration date to settings screen
var term = window.billingSemester.substring(0,window.billingSemester.length-4)
var year = window.billingSemester.substring(window.billingSemester.length-4)
var expirationDate = ""
if (term == "fall") {
expirationDate = "October"
}
else if (term == "spring") {
expirationDate = "March"
}
else if (term == "winter") {
expirationDate = "February"
}
$("#premiumSetting").find(".settingsValue").text("Expires " + expirationDate + " " + year)
var userData = store.get('userData')
var purchases = userData["premium"]
if (purchases == null) {
userData["premium"] = []
}
var premiumObj = {}
var billingSemester = window.billingSemester
premiumObj[billingSemester] = "purchased"
userData["premium"].push(premiumObj)
// make explicit change to server userData
setUserData(uuid=store.get("uuid"), deviceID=null, token=null, school=null, userDataJsonString=JSON.stringify(userData))
store.set("userData", userData)
}
setTimeout(function(){
hideSlidableModal()
},3000);
}
$(document).on('click', '#premiumButton', function(){
var prodToShow = ""
if (window.hostSite == "www"){
prodToShow = "com.coursicle.coursicle."+window.billingSemester+"premium"
} else {
prodToShow = "dev.coursicle.coursicle."+window.billingSemester+"premium"
}
$('#premiumButton').text('Confirming...')
$('#premiumSpinner').show()
makePurchase(prodToShow)
})
It seems like others have encountered this issue as well, although any fix they found did not work for us, and they in general were targeting older SDK versions:
Thank you so much for any assistance you can provide. We're really excited about our new PWA and this is the only major issue we've encountered during our conversion from native.
The text was updated successfully, but these errors were encountered:
@ramakula we have not been able to fix it. At this point, we're looking now at switching to native push notifications for our TWA (now that postMessage support has been added, allowing easy communication between the Android wrapper and web app), and once we've done that I think we'll revive our native Android billing inside the wrapper, so that we can just use something we know works, since the support for billing in a TWA is pretty scant right now. We're probably losing 10% of Android revenue do to this.
That said, we've posted this issue all over the place, so maybe as more people adopt TWA and struggle with billing, Google will handle this issue.
We recently released our TWA (our app) to customers and on day 1 are experiencing a very consistent issue with Google Play Billing. When we try to call
getDetails()
on a SKU as well as when we calllistPurchases()
, we receive a "DOMException: clientAppUnavailable", and the promise fails. Here are the tracebacks:We are confident though that Play Services are being initialized:
After a lot of debugging, our current lead is that the issue may be with our Delegation Service. On Android 11, the Delegation Service runs and the extra command handler is registered successfully. On Android 13, the Delegation Service fails to run and a clientAppUnavailable DOM exception is raised. Below are all the files we believe are relevant:
web_app_manifest.json
AndroidManifest.xml
build.gradle(:app)
DelegationService.kt
manifest.json (on our server)
purchase.js
Here's our device information:
Device: Galaxy A03s (working)
- OS: Android 11
- Browsers Installed: Chrome
- Browser Versions: Chrome 114.0.5735.131
- android-browser-helper library version: 2.4.0
Device: Galaxy S22 Ultra (not working)
- OS: Android 13
- Browsers Installed: Chrome
- Browser Versions: Chrome 114.0.5735.130
- android-browser-helper library version: 2.4.0
Here's a comprehensive list of everything we've tried so far:
It seems like others have encountered this issue as well, although any fix they found did not work for us, and they in general were targeting older SDK versions:
Thank you so much for any assistance you can provide. We're really excited about our new PWA and this is the only major issue we've encountered during our conversion from native.
The text was updated successfully, but these errors were encountered: