提问人:monstermac77 提问时间:6/22/2023 更新时间:8/26/2023 访问量:154
TWA 中一致的 Google Play 结算错误:clientAppUnavailable(Android 13、API 33 及更高版本)
Consistent Google Play Billing error in TWA: clientAppUnavailable (Android 13, API 33 and above)
问:
我们最近向客户发布了 TWA(我们的应用),在第 1 天,Google Play 结算服务就遇到了非常一致的问题。当我们尝试调用 SKU 以及调用时,我们会收到“DOMException: clientAppUnavailable”,并且承诺失败。以下是回溯:getDetails()
listPurchases()
不过,我们确信 Play 服务正在初始化:
经过大量调试,我们目前的线索是问题可能出在我们的委托服务上。在 Android 11 上,委派服务会运行,并且额外的命令处理程序已成功注册。在 Android 13 上,委派服务无法运行,并引发 clientAppUnavailable DOM 异常。以下是我们认为相关的所有文件:
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"}]
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--package="com.coursicle.coursicle" >-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:name="CoursicleApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:manageSpaceActivity="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
android:backupAgent=".MyBackupAgent">
<meta-data android:name="com.google.android.backup.api_key"
android:value="[redacted]" />
<!-- PWA Stuff -->
<meta-data
android:name="asset_statements"
android:resource="@string/assetStatements" />
<meta-data
android:name="web_manifest_url"
android:value="@string/webManifestUrl" />
<meta-data
android:name="twa_generator"
android:value="@string/generatorApp" />
<activity android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity">
<meta-data
android:name="android.support.customtabs.trusted.MANAGE_SPACE_URL"
android:value="@string/launchUrl" />
</activity>
<!--android:alwaysRetainTaskState="true"-->
<activity android:name="LauncherActivity"
android:label="@string/launcherName"
android:exported="true"
android:supportsRtl="true">
<meta-data android:name="android.support.customtabs.trusted.DEFAULT_URL"
android:value="@string/launchUrl" />
<meta-data android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
android:resource="@color/navigationColor" />
<meta-data android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
android:resource="@color/navigationColor" />
<meta-data android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR_DARK"
android:resource="@color/navigationColorDark" />
<meta-data android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR"
android:resource="@color/navigationDividerColor" />
<meta-data android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR_DARK"
android:resource="@color/navigationDividerColorDark" />
<meta-data android:name="android.support.customtabs.trusted.SPLASH_IMAGE_DRAWABLE"
android:resource="@mipmap/ic_launcher"/>
<meta-data android:name="android.support.customtabs.trusted.SPLASH_SCREEN_BACKGROUND_COLOR"
android:resource="@color/backgroundColor"/>
<meta-data android:name="android.support.customtabs.trusted.SPLASH_SCREEN_FADE_OUT_DURATION"
android:value="@integer/splashScreenFadeOutDuration"/>
<meta-data android:name="android.support.customtabs.trusted.FILE_PROVIDER_AUTHORITY"
android:value="@string/providerAuthority"/>
<!--meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" /-->
<meta-data android:name="android.support.customtabs.trusted.FALLBACK_STRATEGY"
android:value="@string/fallbackType" />
<meta-data android:name="android.support.customtabs.trusted.SCREEN_ORIENTATION"
android:value="@string/orientation"/>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="daniel.coursicle.com"
android:scheme="https" />
</intent-filter>
</activity>
<activity android:name="com.google.androidbrowserhelper.trusted.FocusActivity" />
<activity android:name="com.google.androidbrowserhelper.trusted.WebViewFallbackActivity"
android:configChanges="orientation|screenSize" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/providerAuthority"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<service
android:name=".DelegationService"
android:enabled="true"
android:exported="true">
<meta-data
android:name="android.support.customtabs.trusted.SMALL_ICON"
android:resource="@mipmap/ic_launcher" />
<intent-filter>
<action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<!--
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
-->
</service>
<activity
android:name="com.google.androidbrowserhelper.playbilling.provider.PaymentActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:configChanges="keyboardHidden|keyboard|orientation|screenLayout|screenSize"
android:exported="true">
<intent-filter>
<action android:name="org.chromium.intent.action.PAY" />
</intent-filter>
<meta-data
android:name="org.chromium.default_payment_method_name"
android:value="https://play.google.com/billing" />
</activity>
<!-- This service checks who calls it at runtime. -->
<service
android:name="com.google.androidbrowserhelper.playbilling.provider.PaymentService"
android:exported="true" >
<intent-filter>
<action android:name="org.chromium.intent.action.IS_READY_TO_PAY" />
</intent-filter>
</service>
</application>
</manifest>
build.gradle(:应用)
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
namespace 'com.coursicle.coursicle'
signingConfigs {
debug {
storeFile file('Coursicle.jks')
storePassword '[redacted]'
keyAlias '[redacted]'
keyPassword '[redacted]'
}
}
compileSdkVersion 33
defaultConfig {
applicationId "com.coursicle.coursicle"
multiDexEnabled true
minSdkVersion 21
targetSdkVersion 33
versionCode 58 // TODO [push]: increment this before generating the APK
versionName "3.1" // TODO [push]: increment this before generating the APK
multiDexEnabled true
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
debug {
signingConfig signingConfigs.debug
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
}
dataBinding{
enabled = true
}
}
dependencies {
implementation 'com.google.androidbrowserhelper:billing:1.0.0-alpha09'
implementation 'com.google.android.material:material:1.3.0' // needed for app theme
implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.4.0'
// why?
implementation 'com.android.support:multidex:1.0.1'
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Which of these do we really need now?
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
//testImplementation 'junit:junit:4.12'
//androidTestImplementation 'androidx.test.ext:junit:1.1.1'
//androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
def fuel_version = "2.3.1"
implementation "com.github.kittinunf.fuel:fuel:$fuel_version"
implementation "com.github.kittinunf.fuel:fuel-android:$fuel_version"
}
apply plugin: 'com.google.gms.google-services'
代表团服务.kt
package com.coursicle.coursicle
import com.google.androidbrowserhelper.playbilling.digitalgoods.DigitalGoodsRequestHandler
import com.google.androidbrowserhelper.trusted.DelegationService
class DelegationService : DelegationService() {
override fun onCreate() {
super.onCreate()
Log.d("delegationService",getApplicationContext().toString())
registerExtraCommandHandler(DigitalGoodsRequestHandler(getApplicationContext()))
}
}
manifest.json(在我们的服务器上)
{
"packageId": "com.coursicle.coursicle",
"host": "daniel.coursicle.com",
"short_name":"Coursicle",
"enableNotifications": true,
"features": {
"playBilling": {
"enabled": true
}
},
"alphaDependencies": {
"enabled": true
},
"name":"Coursicle",
"start_url":"/?pwa=true",
"background_color":"#ffffff",
"display":"standalone",
"orientation": "portrait",
"theme_color":"#ffffff",
"icons":[{"src":"/homepage/img/coursicleCLogoLarge.png",
"sizes":"512x512",
"type":"image/png",
"purpose":"any"}]
}
购买.js
// 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)
})
以下是我们的设备信息:
设备:Galaxy A03s(工作)
- 操作系统: Android 11
- 已安装浏览器:Chrome
- 浏览器版本: Chrome 114.0.5735.131
- android-browser-helper 库版本:2.4.0
设备:Galaxy S22 Ultra(不工作)
- 操作系统: Android 13
- 已安装浏览器:Chrome
- 浏览器版本: Chrome 114.0.5735.130
- android-browser-helper 库版本:2.4.0
以下是我们迄今为止尝试过的所有内容的完整列表:
- 清除 Google Play 商店缓存
- 将 build.gradle 中的 targetSdkVersion 从 33 递增到 34。
- 确保将com.android.vending.BILLING权限添加到AndroidManifest.xml
- 确保 Google Play 服务是最新的。
- 与 PWA 计费指南进行了逐行比较 (https://chromeos.dev/en/publish/pwa-play-billing)
似乎其他人也遇到了这个问题,尽管他们发现的任何修复都对我们不起作用,而且他们通常针对较旧的 SDK 版本:
- 为什么 Digital Goods API 的 getService 方法在我的 TWA 中拒绝 clientAppUnavailable?
- https://github.com/GoogleChromeLabs/bubblewrap/issues/640#issue-1105007499
非常感谢您提供的任何帮助。我们对新的 PWA 感到非常兴奋,这是我们在从本机转换过程中遇到的唯一主要问题。
答:
-3赞
Alexander
8/26/2023
#1
我找到了解决方案。 问题出在我网站的机器人.txt文件中。它被设置为禁止来自 Google 机器人的所有页面。更改后,计费在 2 小时内开始工作。
评论