@capgo/native-purchases 7.1.11 → 7.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
<a href="https://capgo.app/"><img src='https://raw.githubusercontent.com/Cap-go/capgo/main/assets/capgo_banner.png' alt='Capgo - Instant updates for capacitor'/></a>
|
|
3
3
|
|
|
4
4
|
<div align="center">
|
|
5
|
-
<h2><a href="https://capgo.app/?ref=plugin"> ➡️ Get Instant updates for your App with Capgo
|
|
6
|
-
<h2><a href="https://capgo.app/consulting/?ref=plugin">
|
|
5
|
+
<h2><a href="https://capgo.app/?ref=plugin"> ➡️ Get Instant updates for your App with Capgo</a></h2>
|
|
6
|
+
<h2><a href="https://capgo.app/consulting/?ref=plugin"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
9
|
## In-app Purchases Made Easy
|
|
@@ -17,6 +17,17 @@ npm install @capgo/native-purchases
|
|
|
17
17
|
npx cap sync
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
## 📚 Testing Guides
|
|
21
|
+
|
|
22
|
+
Complete visual testing guides for both platforms:
|
|
23
|
+
|
|
24
|
+
| Platform | Guide | Content |
|
|
25
|
+
|----------|-------|---------|
|
|
26
|
+
| 🍎 **iOS** | **[iOS Testing Guide](./docs/iOS_TESTING_GUIDE.md)** | StoreKit Local Testing, Sandbox Testing, Developer Mode setup |
|
|
27
|
+
| 🤖 **Android** | **[Android Testing Guide](./docs/Android_TESTING_GUIDE.md)** | Internal Testing, License Testing, Internal App Sharing |
|
|
28
|
+
|
|
29
|
+
> 💡 **Quick Start**: Choose **StoreKit Local Testing** for iOS or **Internal Testing** for Android for the fastest development experience.
|
|
30
|
+
|
|
20
31
|
## Android
|
|
21
32
|
|
|
22
33
|
Add this to manifest
|
|
@@ -25,6 +36,46 @@ Add this to manifest
|
|
|
25
36
|
<uses-permission android:name="com.android.vending.BILLING" />
|
|
26
37
|
```
|
|
27
38
|
|
|
39
|
+
### Testing with Google Play Console
|
|
40
|
+
|
|
41
|
+
> 📖 **[Complete Android Testing Guide](./docs/Android_TESTING_GUIDE.md)** - Comprehensive guide covering Internal Testing, License Testing, and Internal App Sharing methods with step-by-step instructions, troubleshooting, and best practices.
|
|
42
|
+
|
|
43
|
+
For testing in-app purchases on Android:
|
|
44
|
+
|
|
45
|
+
1. Upload your app to Google Play Console (internal testing track is sufficient)
|
|
46
|
+
2. Create test accounts in Google Play Console:
|
|
47
|
+
- Go to Google Play Console
|
|
48
|
+
- Navigate to "Setup" > "License testing"
|
|
49
|
+
- Add Gmail accounts to "License testers" list
|
|
50
|
+
3. Install the app from Google Play Store on a device signed in with a test account
|
|
51
|
+
4. Test purchases will be free and won't charge real money
|
|
52
|
+
|
|
53
|
+
## iOS
|
|
54
|
+
|
|
55
|
+
Add the "In-App Purchase" capability to your Xcode project:
|
|
56
|
+
|
|
57
|
+
1. Open your project in Xcode
|
|
58
|
+
2. Select your app target
|
|
59
|
+
3. Go to "Signing & Capabilities" tab
|
|
60
|
+
4. Click the "+" button to add a capability
|
|
61
|
+
5. Search for and add "In-App Purchase"
|
|
62
|
+
|
|
63
|
+
> ⚠️ **App Store Requirement**: You MUST display product names and prices using data from the plugin (`product.title`, `product.priceString`). Hardcoded values will cause App Store rejection.
|
|
64
|
+
|
|
65
|
+
> 📖 **[Complete iOS Testing Guide](./docs/iOS_TESTING_GUIDE.md)** - Comprehensive guide covering both Sandbox and StoreKit local testing methods with step-by-step instructions, troubleshooting, and best practices.
|
|
66
|
+
|
|
67
|
+
### Testing with Sandbox
|
|
68
|
+
|
|
69
|
+
For testing in-app purchases on iOS:
|
|
70
|
+
|
|
71
|
+
1. Create a sandbox test user in App Store Connect:
|
|
72
|
+
- Go to App Store Connect
|
|
73
|
+
- Navigate to "Users and Access" > "Sandbox Testers"
|
|
74
|
+
- Create a new sandbox tester account
|
|
75
|
+
2. On your iOS device, sign out of your regular Apple ID in Settings > App Store
|
|
76
|
+
3. Install and run your app
|
|
77
|
+
4. When prompted for Apple ID during purchase testing, use your sandbox account credentials
|
|
78
|
+
|
|
28
79
|
## Usage
|
|
29
80
|
|
|
30
81
|
Import the plugin in your TypeScript file:
|
|
@@ -33,175 +84,270 @@ Import the plugin in your TypeScript file:
|
|
|
33
84
|
import { NativePurchases } from '@capgo/native-purchases';
|
|
34
85
|
```
|
|
35
86
|
|
|
36
|
-
###
|
|
87
|
+
### Complete Example: Get Product Info and Purchase
|
|
37
88
|
|
|
38
|
-
|
|
39
|
-
We only support Storekit 2 on iOS (iOS 15+) and google play on Android
|
|
89
|
+
Here's a complete example showing how to get product information and make a purchase:
|
|
40
90
|
|
|
41
91
|
```typescript
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
92
|
+
import { NativePurchases } from '@capgo/native-purchases';
|
|
93
|
+
|
|
94
|
+
class PurchaseManager {
|
|
95
|
+
private productId = 'com.yourapp.premium.monthly';
|
|
96
|
+
|
|
97
|
+
async initializeStore() {
|
|
98
|
+
try {
|
|
99
|
+
// 1. Check if billing is supported
|
|
100
|
+
const { isBillingSupported } = await NativePurchases.isBillingSupported();
|
|
101
|
+
if (!isBillingSupported) {
|
|
102
|
+
throw new Error('Billing not supported on this device');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Get product information (REQUIRED by Apple - no hardcoded prices!)
|
|
106
|
+
const product = await this.getProductInfo();
|
|
107
|
+
|
|
108
|
+
// 3. Display product with dynamic info from store
|
|
109
|
+
this.displayProduct(product);
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Store initialization failed:', error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getProductInfo() {
|
|
117
|
+
try {
|
|
118
|
+
const { product } = await NativePurchases.getProduct({
|
|
119
|
+
productIdentifier: this.productId
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
console.log('Product loaded:', {
|
|
123
|
+
id: product.identifier,
|
|
124
|
+
title: product.title, // Use this for display (required by Apple)
|
|
125
|
+
price: product.priceString, // Use this for display (required by Apple)
|
|
126
|
+
description: product.description
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return product;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Failed to get product:', error);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
displayProduct(product: any) {
|
|
137
|
+
// ✅ CORRECT: Use dynamic product info (required by Apple)
|
|
138
|
+
document.getElementById('product-title')!.textContent = product.title;
|
|
139
|
+
document.getElementById('product-price')!.textContent = product.priceString;
|
|
140
|
+
document.getElementById('product-description')!.textContent = product.description;
|
|
141
|
+
|
|
142
|
+
// ❌ WRONG: Never hardcode prices - Apple will reject your app
|
|
143
|
+
// document.getElementById('product-price')!.textContent = '$9.99/month';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async purchaseProduct() {
|
|
147
|
+
try {
|
|
148
|
+
console.log('Starting purchase...');
|
|
149
|
+
|
|
150
|
+
const result = await NativePurchases.purchaseProduct({
|
|
151
|
+
productIdentifier: this.productId,
|
|
152
|
+
quantity: 1
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
console.log('Purchase successful!', result.transactionId);
|
|
156
|
+
|
|
157
|
+
// Handle successful purchase
|
|
158
|
+
await this.handleSuccessfulPurchase(result.transactionId);
|
|
159
|
+
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('Purchase failed:', error);
|
|
162
|
+
this.handlePurchaseError(error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async handleSuccessfulPurchase(transactionId: string) {
|
|
167
|
+
// 1. Grant access to premium features
|
|
168
|
+
localStorage.setItem('premium_active', 'true');
|
|
169
|
+
|
|
170
|
+
// 2. Update UI
|
|
171
|
+
document.getElementById('subscription-status')!.textContent = 'Premium Active';
|
|
172
|
+
|
|
173
|
+
// 3. Optional: Verify purchase on your server
|
|
174
|
+
await this.verifyPurchaseOnServer(transactionId);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
handlePurchaseError(error: any) {
|
|
178
|
+
// Handle different error scenarios
|
|
179
|
+
if (error.message.includes('User cancelled')) {
|
|
180
|
+
console.log('User cancelled the purchase');
|
|
181
|
+
} else if (error.message.includes('Network')) {
|
|
182
|
+
alert('Network error. Please check your connection and try again.');
|
|
47
183
|
} else {
|
|
48
|
-
|
|
184
|
+
alert('Purchase failed. Please try again.');
|
|
49
185
|
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error('Error checking billing support:', error);
|
|
52
186
|
}
|
|
53
|
-
|
|
187
|
+
|
|
188
|
+
async verifyPurchaseOnServer(transactionId: string) {
|
|
189
|
+
try {
|
|
190
|
+
// Send transaction to your server for verification
|
|
191
|
+
const response = await fetch('/api/verify-purchase', {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
|
194
|
+
body: JSON.stringify({ transactionId })
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const result = await response.json();
|
|
198
|
+
console.log('Server verification:', result);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('Server verification failed:', error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async restorePurchases() {
|
|
205
|
+
try {
|
|
206
|
+
await NativePurchases.restorePurchases();
|
|
207
|
+
console.log('Purchases restored successfully');
|
|
208
|
+
|
|
209
|
+
// Check if user has active premium after restore
|
|
210
|
+
const product = await this.getProductInfo();
|
|
211
|
+
// Update UI based on restored purchases
|
|
212
|
+
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('Failed to restore purchases:', error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Usage in your app
|
|
220
|
+
const purchaseManager = new PurchaseManager();
|
|
221
|
+
|
|
222
|
+
// Initialize when app starts
|
|
223
|
+
purchaseManager.initializeStore();
|
|
224
|
+
|
|
225
|
+
// Attach to UI buttons
|
|
226
|
+
document.getElementById('buy-button')?.addEventListener('click', () => {
|
|
227
|
+
purchaseManager.purchaseProduct();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
document.getElementById('restore-button')?.addEventListener('click', () => {
|
|
231
|
+
purchaseManager.restorePurchases();
|
|
232
|
+
});
|
|
54
233
|
```
|
|
55
234
|
|
|
56
|
-
###
|
|
235
|
+
### Quick Examples
|
|
57
236
|
|
|
58
|
-
|
|
237
|
+
#### Get Multiple Products
|
|
59
238
|
|
|
60
239
|
```typescript
|
|
61
|
-
|
|
240
|
+
// Get multiple products at once
|
|
241
|
+
const getProducts = async () => {
|
|
62
242
|
try {
|
|
63
243
|
const { products } = await NativePurchases.getProducts({
|
|
64
|
-
productIdentifiers: [
|
|
65
|
-
|
|
244
|
+
productIdentifiers: [
|
|
245
|
+
'com.yourapp.premium.monthly',
|
|
246
|
+
'com.yourapp.premium.yearly',
|
|
247
|
+
'com.yourapp.remove_ads'
|
|
248
|
+
]
|
|
66
249
|
});
|
|
67
|
-
|
|
250
|
+
|
|
251
|
+
products.forEach(product => {
|
|
252
|
+
console.log(`${product.title}: ${product.priceString}`);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return products;
|
|
68
256
|
} catch (error) {
|
|
69
257
|
console.error('Error getting products:', error);
|
|
70
258
|
}
|
|
71
259
|
};
|
|
72
260
|
```
|
|
73
261
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
To initiate a purchase:
|
|
262
|
+
#### Simple Purchase Flow
|
|
77
263
|
|
|
78
264
|
```typescript
|
|
79
|
-
|
|
265
|
+
// Simple one-function purchase
|
|
266
|
+
const buyPremium = async () => {
|
|
80
267
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
268
|
+
// Check billing support
|
|
269
|
+
const { isBillingSupported } = await NativePurchases.isBillingSupported();
|
|
270
|
+
if (!isBillingSupported) {
|
|
271
|
+
alert('Purchases not supported on this device');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Get product (for price display)
|
|
276
|
+
const { product } = await NativePurchases.getProduct({
|
|
277
|
+
productIdentifier: 'com.yourapp.premium'
|
|
84
278
|
});
|
|
85
|
-
console.log('Purchase successful:', transaction);
|
|
86
|
-
// Handle the successful purchase (e.g., unlock content, update UI)
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error('Purchase failed:', error);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
```
|
|
92
279
|
|
|
93
|
-
|
|
280
|
+
// Confirm with user (showing real price from store)
|
|
281
|
+
const confirmed = confirm(`Purchase ${product.title} for ${product.priceString}?`);
|
|
282
|
+
if (!confirmed) return;
|
|
94
283
|
|
|
95
|
-
|
|
284
|
+
// Make purchase
|
|
285
|
+
const result = await NativePurchases.purchaseProduct({
|
|
286
|
+
productIdentifier: 'com.yourapp.premium',
|
|
287
|
+
quantity: 1
|
|
288
|
+
});
|
|
96
289
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const { customerInfo } = await NativePurchases.restorePurchases();
|
|
101
|
-
console.log('Restored purchases:', customerInfo);
|
|
102
|
-
// Update your app's state based on the restored purchases
|
|
290
|
+
alert('Purchase successful! Transaction ID: ' + result.transactionId);
|
|
291
|
+
|
|
103
292
|
} catch (error) {
|
|
104
|
-
|
|
293
|
+
alert('Purchase failed: ' + error.message);
|
|
105
294
|
}
|
|
106
295
|
};
|
|
107
296
|
```
|
|
108
297
|
|
|
109
|
-
|
|
298
|
+
### Check if billing is supported
|
|
110
299
|
|
|
111
|
-
|
|
300
|
+
Before attempting to make purchases, check if billing is supported on the device:
|
|
301
|
+
We only support Storekit 2 on iOS (iOS 15+) and google play on Android
|
|
112
302
|
|
|
113
303
|
```typescript
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class Store {
|
|
118
|
-
private products: Product[] = [];
|
|
119
|
-
|
|
120
|
-
async initialize() {
|
|
121
|
-
if (Capacitor.isNativePlatform()) {
|
|
122
|
-
try {
|
|
123
|
-
await this.checkBillingSupport();
|
|
124
|
-
await this.loadProducts();
|
|
125
|
-
} catch (error) {
|
|
126
|
-
console.error('Store initialization failed:', error);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private async checkBillingSupport() {
|
|
304
|
+
const checkBillingSupport = async () => {
|
|
305
|
+
try {
|
|
132
306
|
const { isBillingSupported } = await NativePurchases.isBillingSupported();
|
|
133
|
-
if (
|
|
134
|
-
|
|
307
|
+
if (isBillingSupported) {
|
|
308
|
+
console.log('Billing is supported on this device');
|
|
309
|
+
} else {
|
|
310
|
+
console.log('Billing is not supported on this device');
|
|
135
311
|
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('Error checking billing support:', error);
|
|
136
314
|
}
|
|
315
|
+
};
|
|
316
|
+
```
|
|
137
317
|
|
|
138
|
-
|
|
139
|
-
const productIds = ['premium_subscription', 'remove_ads', 'coin_pack'];
|
|
140
|
-
const { products } = await NativePurchases.getProducts({
|
|
141
|
-
productIdentifiers: productIds,
|
|
142
|
-
productType: PURCHASE_TYPE.INAPP
|
|
143
|
-
});
|
|
144
|
-
this.products = products;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
getProducts() {
|
|
148
|
-
return this.products;
|
|
149
|
-
}
|
|
318
|
+
### API Reference
|
|
150
319
|
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const transaction = await NativePurchases.purchaseProduct({
|
|
154
|
-
productIdentifier: productId,
|
|
155
|
-
productType: PURCHASE_TYPE.INAPP
|
|
156
|
-
});
|
|
157
|
-
console.log('Purchase successful:', transaction);
|
|
158
|
-
// Handle the successful purchase
|
|
159
|
-
return transaction;
|
|
160
|
-
} catch (error) {
|
|
161
|
-
console.error('Purchase failed:', error);
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
320
|
+
#### Core Methods
|
|
165
321
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
console.log('Restored purchases:', customerInfo);
|
|
170
|
-
// Update app state based on restored purchases
|
|
171
|
-
return customerInfo;
|
|
172
|
-
} catch (error) {
|
|
173
|
-
console.error('Failed to restore purchases:', error);
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
322
|
+
```typescript
|
|
323
|
+
// Check if in-app purchases are supported
|
|
324
|
+
await NativePurchases.isBillingSupported();
|
|
178
325
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
await store.initialize();
|
|
326
|
+
// Get single product information
|
|
327
|
+
await NativePurchases.getProduct({ productIdentifier: 'product_id' });
|
|
182
328
|
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
console.log('Available products:', products);
|
|
329
|
+
// Get multiple products
|
|
330
|
+
await NativePurchases.getProducts({ productIdentifiers: ['id1', 'id2'] });
|
|
186
331
|
|
|
187
332
|
// Purchase a product
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
console.error('Purchase failed:', error);
|
|
193
|
-
}
|
|
333
|
+
await NativePurchases.purchaseProduct({
|
|
334
|
+
productIdentifier: 'product_id',
|
|
335
|
+
quantity: 1
|
|
336
|
+
});
|
|
194
337
|
|
|
195
|
-
// Restore purchases
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
console.error('Failed to restore purchases:', error);
|
|
201
|
-
}
|
|
338
|
+
// Restore previous purchases
|
|
339
|
+
await NativePurchases.restorePurchases();
|
|
340
|
+
|
|
341
|
+
// Get plugin version
|
|
342
|
+
await NativePurchases.getPluginVersion();
|
|
202
343
|
```
|
|
203
344
|
|
|
204
|
-
|
|
345
|
+
### Important Notes
|
|
346
|
+
|
|
347
|
+
- **Apple Requirement**: Always display product names and prices from StoreKit data, never hardcode them
|
|
348
|
+
- **Error Handling**: Implement proper error handling for network issues and user cancellations
|
|
349
|
+
- **Server Verification**: Always verify purchases on your server for security
|
|
350
|
+
- **Testing**: Use the comprehensive testing guides for both iOS and Android platforms
|
|
205
351
|
|
|
206
352
|
## Backend Validation
|
|
207
353
|
|
package/android/build.gradle
CHANGED
|
@@ -49,7 +49,7 @@ repositories {
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
dependencies {
|
|
52
|
-
implementation "com.google.guava:guava:33.4.
|
|
52
|
+
implementation "com.google.guava:guava:33.4.8-android"
|
|
53
53
|
def billing_version = "7.1.1"
|
|
54
54
|
implementation "com.android.billingclient:billing:$billing_version"
|
|
55
55
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
@@ -40,19 +40,24 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
40
40
|
|
|
41
41
|
@PluginMethod
|
|
42
42
|
public void isBillingSupported(PluginCall call) {
|
|
43
|
+
Log.d(TAG, "isBillingSupported() called");
|
|
43
44
|
JSObject ret = new JSObject();
|
|
44
45
|
ret.put("isBillingSupported", true);
|
|
45
|
-
|
|
46
|
+
Log.d(TAG, "isBillingSupported() returning true");
|
|
47
|
+
call.resolve(ret);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
@Override
|
|
49
51
|
public void load() {
|
|
50
52
|
super.load();
|
|
53
|
+
Log.d(TAG, "Plugin load() called");
|
|
51
54
|
Log.i(NativePurchasesPlugin.TAG, "load");
|
|
52
55
|
semaphoreDown();
|
|
56
|
+
Log.d(TAG, "Plugin load() completed");
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
private void semaphoreWait(Number waitTime) {
|
|
60
|
+
Log.d(TAG, "semaphoreWait() called with waitTime: " + waitTime);
|
|
56
61
|
Log.i(NativePurchasesPlugin.TAG, "semaphoreWait " + waitTime);
|
|
57
62
|
try {
|
|
58
63
|
// Log.i(CapacitorUpdater.TAG, "semaphoreReady count " + CapacitorUpdaterPlugin.this.semaphoreReady.getCount());
|
|
@@ -67,20 +72,26 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
67
72
|
"semaphoreReady count " +
|
|
68
73
|
NativePurchasesPlugin.this.semaphoreReady.getPhase()
|
|
69
74
|
);
|
|
75
|
+
Log.d(TAG, "semaphoreWait() completed successfully");
|
|
70
76
|
} catch (InterruptedException e) {
|
|
77
|
+
Log.d(TAG, "semaphoreWait() InterruptedException: " + e.getMessage());
|
|
71
78
|
Log.i(NativePurchasesPlugin.TAG, "semaphoreWait InterruptedException");
|
|
72
79
|
e.printStackTrace();
|
|
73
80
|
} catch (TimeoutException e) {
|
|
81
|
+
Log.d(TAG, "semaphoreWait() TimeoutException: " + e.getMessage());
|
|
74
82
|
throw new RuntimeException(e);
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
private void semaphoreUp() {
|
|
87
|
+
Log.d(TAG, "semaphoreUp() called");
|
|
79
88
|
Log.i(NativePurchasesPlugin.TAG, "semaphoreUp");
|
|
80
89
|
NativePurchasesPlugin.this.semaphoreReady.register();
|
|
90
|
+
Log.d(TAG, "semaphoreUp() completed");
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
private void semaphoreDown() {
|
|
94
|
+
Log.d(TAG, "semaphoreDown() called");
|
|
84
95
|
Log.i(NativePurchasesPlugin.TAG, "semaphoreDown");
|
|
85
96
|
Log.i(
|
|
86
97
|
NativePurchasesPlugin.TAG,
|
|
@@ -88,57 +99,86 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
88
99
|
NativePurchasesPlugin.this.semaphoreReady.getPhase()
|
|
89
100
|
);
|
|
90
101
|
NativePurchasesPlugin.this.semaphoreReady.arriveAndDeregister();
|
|
102
|
+
Log.d(TAG, "semaphoreDown() completed");
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
private void closeBillingClient() {
|
|
106
|
+
Log.d(TAG, "closeBillingClient() called");
|
|
94
107
|
if (billingClient != null) {
|
|
108
|
+
Log.d(TAG, "Ending billing client connection");
|
|
95
109
|
billingClient.endConnection();
|
|
96
110
|
billingClient = null;
|
|
97
111
|
semaphoreDown();
|
|
112
|
+
Log.d(TAG, "Billing client closed and set to null");
|
|
113
|
+
} else {
|
|
114
|
+
Log.d(TAG, "Billing client was already null");
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
private void handlePurchase(Purchase purchase, PluginCall purchaseCall) {
|
|
119
|
+
Log.d(TAG, "handlePurchase() called");
|
|
120
|
+
Log.d(TAG, "Purchase details: " + purchase.toString());
|
|
102
121
|
Log.i(NativePurchasesPlugin.TAG, "handlePurchase" + purchase);
|
|
103
122
|
Log.i(
|
|
104
123
|
NativePurchasesPlugin.TAG,
|
|
105
124
|
"getPurchaseState" + purchase.getPurchaseState()
|
|
106
125
|
);
|
|
126
|
+
Log.d(TAG, "Purchase state: " + purchase.getPurchaseState());
|
|
127
|
+
Log.d(TAG, "Purchase token: " + purchase.getPurchaseToken());
|
|
128
|
+
Log.d(TAG, "Is acknowledged: " + purchase.isAcknowledged());
|
|
129
|
+
|
|
107
130
|
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
|
|
131
|
+
Log.d(TAG, "Purchase state is PURCHASED");
|
|
108
132
|
// Grant entitlement to the user, then acknowledge the purchase
|
|
109
133
|
// if sub then acknowledgePurchase
|
|
110
134
|
// if one time then consumePurchase
|
|
111
135
|
if (purchase.isAcknowledged()) {
|
|
136
|
+
Log.d(TAG, "Purchase already acknowledged, consuming...");
|
|
112
137
|
ConsumeParams consumeParams = ConsumeParams.newBuilder()
|
|
113
138
|
.setPurchaseToken(purchase.getPurchaseToken())
|
|
114
139
|
.build();
|
|
115
140
|
billingClient.consumeAsync(consumeParams, this::onConsumeResponse);
|
|
116
141
|
} else {
|
|
142
|
+
Log.d(TAG, "Purchase not acknowledged, acknowledging...");
|
|
117
143
|
acknowledgePurchase(purchase.getPurchaseToken());
|
|
118
144
|
}
|
|
119
145
|
|
|
120
146
|
JSObject ret = new JSObject();
|
|
121
147
|
ret.put("transactionId", purchase.getPurchaseToken());
|
|
148
|
+
Log.d(
|
|
149
|
+
TAG,
|
|
150
|
+
"Resolving purchase call with transactionId: " +
|
|
151
|
+
purchase.getPurchaseToken()
|
|
152
|
+
);
|
|
122
153
|
if (purchaseCall != null) {
|
|
123
154
|
purchaseCall.resolve(ret);
|
|
155
|
+
} else {
|
|
156
|
+
Log.d(TAG, "purchaseCall is null, cannot resolve");
|
|
124
157
|
}
|
|
125
158
|
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
|
|
159
|
+
Log.d(TAG, "Purchase state is PENDING");
|
|
126
160
|
// Here you can confirm to the user that they've started the pending
|
|
127
161
|
// purchase, and to complete it, they should follow instructions that are
|
|
128
162
|
// given to them. You can also choose to remind the user to complete the
|
|
129
163
|
// purchase if you detect that it is still pending.
|
|
130
164
|
if (purchaseCall != null) {
|
|
131
165
|
purchaseCall.reject("Purchase is pending");
|
|
166
|
+
} else {
|
|
167
|
+
Log.d(TAG, "purchaseCall is null for pending purchase");
|
|
132
168
|
}
|
|
133
169
|
} else {
|
|
170
|
+
Log.d(TAG, "Purchase state is OTHER: " + purchase.getPurchaseState());
|
|
134
171
|
// Handle any other error codes.
|
|
135
172
|
if (purchaseCall != null) {
|
|
136
173
|
purchaseCall.reject("Purchase is not purchased");
|
|
174
|
+
} else {
|
|
175
|
+
Log.d(TAG, "purchaseCall is null for failed purchase");
|
|
137
176
|
}
|
|
138
177
|
}
|
|
139
178
|
}
|
|
140
179
|
|
|
141
180
|
private void acknowledgePurchase(String purchaseToken) {
|
|
181
|
+
Log.d(TAG, "acknowledgePurchase() called with token: " + purchaseToken);
|
|
142
182
|
AcknowledgePurchaseParams acknowledgePurchaseParams =
|
|
143
183
|
AcknowledgePurchaseParams.newBuilder()
|
|
144
184
|
.setPurchaseToken(purchaseToken)
|
|
@@ -149,6 +189,14 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
149
189
|
@Override
|
|
150
190
|
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
|
|
151
191
|
// Handle the result of the acknowledge purchase
|
|
192
|
+
Log.d(TAG, "onAcknowledgePurchaseResponse() called");
|
|
193
|
+
Log.d(
|
|
194
|
+
TAG,
|
|
195
|
+
"Acknowledge result: " +
|
|
196
|
+
billingResult.getResponseCode() +
|
|
197
|
+
" - " +
|
|
198
|
+
billingResult.getDebugMessage()
|
|
199
|
+
);
|
|
152
200
|
Log.i(
|
|
153
201
|
NativePurchasesPlugin.TAG,
|
|
154
202
|
"onAcknowledgePurchaseResponse" + billingResult
|
|
@@ -159,10 +207,12 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
159
207
|
}
|
|
160
208
|
|
|
161
209
|
private void initBillingClient(PluginCall purchaseCall) {
|
|
210
|
+
Log.d(TAG, "initBillingClient() called");
|
|
162
211
|
semaphoreWait(10);
|
|
163
212
|
closeBillingClient();
|
|
164
213
|
semaphoreUp();
|
|
165
214
|
CountDownLatch semaphoreReady = new CountDownLatch(1);
|
|
215
|
+
Log.d(TAG, "Creating new BillingClient");
|
|
166
216
|
billingClient = BillingClient.newBuilder(getContext())
|
|
167
217
|
.setListener(
|
|
168
218
|
new PurchasesUpdatedListener() {
|
|
@@ -171,6 +221,18 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
171
221
|
BillingResult billingResult,
|
|
172
222
|
List<Purchase> purchases
|
|
173
223
|
) {
|
|
224
|
+
Log.d(TAG, "onPurchasesUpdated() called");
|
|
225
|
+
Log.d(
|
|
226
|
+
TAG,
|
|
227
|
+
"Billing result: " +
|
|
228
|
+
billingResult.getResponseCode() +
|
|
229
|
+
" - " +
|
|
230
|
+
billingResult.getDebugMessage()
|
|
231
|
+
);
|
|
232
|
+
Log.d(
|
|
233
|
+
TAG,
|
|
234
|
+
"Purchases count: " + (purchases != null ? purchases.size() : 0)
|
|
235
|
+
);
|
|
174
236
|
Log.i(
|
|
175
237
|
NativePurchasesPlugin.TAG,
|
|
176
238
|
"onPurchasesUpdated" + billingResult
|
|
@@ -180,12 +242,17 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
180
242
|
BillingClient.BillingResponseCode.OK &&
|
|
181
243
|
purchases != null
|
|
182
244
|
) {
|
|
245
|
+
Log.d(
|
|
246
|
+
TAG,
|
|
247
|
+
"Purchase update successful, processing first purchase"
|
|
248
|
+
);
|
|
183
249
|
// for (Purchase purchase : purchases) {
|
|
184
250
|
// handlePurchase(purchase, purchaseCall);
|
|
185
251
|
// }
|
|
186
252
|
handlePurchase(purchases.get(0), purchaseCall);
|
|
187
253
|
} else {
|
|
188
254
|
// Handle any other error codes.
|
|
255
|
+
Log.d(TAG, "Purchase update failed or purchases is null");
|
|
189
256
|
Log.i(
|
|
190
257
|
NativePurchasesPlugin.TAG,
|
|
191
258
|
"onPurchasesUpdated" + billingResult
|
|
@@ -201,59 +268,91 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
201
268
|
)
|
|
202
269
|
.enablePendingPurchases()
|
|
203
270
|
.build();
|
|
271
|
+
Log.d(TAG, "Starting billing client connection");
|
|
204
272
|
billingClient.startConnection(
|
|
205
273
|
new BillingClientStateListener() {
|
|
206
274
|
@Override
|
|
207
275
|
public void onBillingSetupFinished(BillingResult billingResult) {
|
|
276
|
+
Log.d(TAG, "onBillingSetupFinished() called");
|
|
277
|
+
Log.d(
|
|
278
|
+
TAG,
|
|
279
|
+
"Setup result: " +
|
|
280
|
+
billingResult.getResponseCode() +
|
|
281
|
+
" - " +
|
|
282
|
+
billingResult.getDebugMessage()
|
|
283
|
+
);
|
|
208
284
|
if (
|
|
209
285
|
billingResult.getResponseCode() ==
|
|
210
286
|
BillingClient.BillingResponseCode.OK
|
|
211
287
|
) {
|
|
288
|
+
Log.d(TAG, "Billing setup successful, client is ready");
|
|
212
289
|
// The BillingClient is ready. You can query purchases here.
|
|
213
290
|
semaphoreReady.countDown();
|
|
291
|
+
} else {
|
|
292
|
+
Log.d(TAG, "Billing setup failed");
|
|
214
293
|
}
|
|
215
294
|
}
|
|
216
295
|
|
|
217
296
|
@Override
|
|
218
297
|
public void onBillingServiceDisconnected() {
|
|
298
|
+
Log.d(TAG, "onBillingServiceDisconnected() called");
|
|
219
299
|
// Try to restart the connection on the next request to
|
|
220
300
|
// Google Play by calling the startConnection() method.
|
|
221
301
|
}
|
|
222
302
|
}
|
|
223
303
|
);
|
|
224
304
|
try {
|
|
305
|
+
Log.d(TAG, "Waiting for billing client setup to finish");
|
|
225
306
|
semaphoreReady.await();
|
|
307
|
+
Log.d(TAG, "Billing client setup completed");
|
|
226
308
|
} catch (InterruptedException e) {
|
|
309
|
+
Log.d(
|
|
310
|
+
TAG,
|
|
311
|
+
"InterruptedException while waiting for billing setup: " +
|
|
312
|
+
e.getMessage()
|
|
313
|
+
);
|
|
227
314
|
e.printStackTrace();
|
|
228
315
|
}
|
|
229
316
|
}
|
|
230
317
|
|
|
231
318
|
@PluginMethod
|
|
232
319
|
public void getPluginVersion(final PluginCall call) {
|
|
320
|
+
Log.d(TAG, "getPluginVersion() called");
|
|
233
321
|
try {
|
|
234
322
|
final JSObject ret = new JSObject();
|
|
235
323
|
ret.put("version", this.PLUGIN_VERSION);
|
|
324
|
+
Log.d(TAG, "Returning plugin version: " + this.PLUGIN_VERSION);
|
|
236
325
|
call.resolve(ret);
|
|
237
326
|
} catch (final Exception e) {
|
|
327
|
+
Log.d(TAG, "Error getting plugin version: " + e.getMessage());
|
|
238
328
|
call.reject("Could not get plugin version", e);
|
|
239
329
|
}
|
|
240
330
|
}
|
|
241
331
|
|
|
242
332
|
@PluginMethod
|
|
243
333
|
public void purchaseProduct(PluginCall call) {
|
|
334
|
+
Log.d(TAG, "purchaseProduct() called");
|
|
244
335
|
String productIdentifier = call.getString("productIdentifier");
|
|
245
336
|
String planIdentifier = call.getString("planIdentifier");
|
|
246
337
|
String productType = call.getString("productType", "inapp");
|
|
247
338
|
Number quantity = call.getInt("quantity", 1);
|
|
339
|
+
|
|
340
|
+
Log.d(TAG, "Product identifier: " + productIdentifier);
|
|
341
|
+
Log.d(TAG, "Plan identifier: " + planIdentifier);
|
|
342
|
+
Log.d(TAG, "Product type: " + productType);
|
|
343
|
+
Log.d(TAG, "Quantity: " + quantity);
|
|
344
|
+
|
|
248
345
|
// cannot use quantity, because it's done in native modal
|
|
249
346
|
Log.d("CapacitorPurchases", "purchaseProduct: " + productIdentifier);
|
|
250
347
|
if (productIdentifier == null || productIdentifier.isEmpty()) {
|
|
251
348
|
// Handle error: productIdentifier is empty
|
|
349
|
+
Log.d(TAG, "Error: productIdentifier is empty");
|
|
252
350
|
call.reject("productIdentifier is empty");
|
|
253
351
|
return;
|
|
254
352
|
}
|
|
255
353
|
if (productType == null || productType.isEmpty()) {
|
|
256
354
|
// Handle error: productType is empty
|
|
355
|
+
Log.d(TAG, "Error: productType is empty");
|
|
257
356
|
call.reject("productType is empty");
|
|
258
357
|
return;
|
|
259
358
|
}
|
|
@@ -262,20 +361,29 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
262
361
|
(planIdentifier == null || planIdentifier.isEmpty())
|
|
263
362
|
) {
|
|
264
363
|
// Handle error: no planIdentifier with productType subs
|
|
364
|
+
Log.d(
|
|
365
|
+
TAG,
|
|
366
|
+
"Error: planIdentifier cannot be empty if productType is subs"
|
|
367
|
+
);
|
|
265
368
|
call.reject("planIdentifier cannot be empty if productType is subs");
|
|
266
369
|
return;
|
|
267
370
|
}
|
|
268
371
|
if (quantity.intValue() < 1) {
|
|
269
372
|
// Handle error: quantity is less than 1
|
|
373
|
+
Log.d(TAG, "Error: quantity is less than 1");
|
|
270
374
|
call.reject("quantity is less than 1");
|
|
271
375
|
return;
|
|
272
376
|
}
|
|
377
|
+
|
|
378
|
+
String productId = productType.equals("inapp")
|
|
379
|
+
? productIdentifier
|
|
380
|
+
: planIdentifier;
|
|
381
|
+
Log.d(TAG, "Using product ID for query: " + productId);
|
|
382
|
+
|
|
273
383
|
ImmutableList<QueryProductDetailsParams.Product> productList =
|
|
274
384
|
ImmutableList.of(
|
|
275
385
|
QueryProductDetailsParams.Product.newBuilder()
|
|
276
|
-
.setProductId(
|
|
277
|
-
productType.equals("inapp") ? productIdentifier : planIdentifier
|
|
278
|
-
)
|
|
386
|
+
.setProductId(productId)
|
|
279
387
|
.setProductType(
|
|
280
388
|
productType.equals("inapp")
|
|
281
389
|
? BillingClient.ProductType.INAPP
|
|
@@ -286,8 +394,10 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
286
394
|
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
|
|
287
395
|
.setProductList(productList)
|
|
288
396
|
.build();
|
|
397
|
+
Log.d(TAG, "Initializing billing client for purchase");
|
|
289
398
|
this.initBillingClient(call);
|
|
290
399
|
try {
|
|
400
|
+
Log.d(TAG, "Querying product details for purchase");
|
|
291
401
|
billingClient.queryProductDetailsAsync(
|
|
292
402
|
params,
|
|
293
403
|
new ProductDetailsResponseListener() {
|
|
@@ -295,7 +405,18 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
295
405
|
BillingResult billingResult,
|
|
296
406
|
List<ProductDetails> productDetailsList
|
|
297
407
|
) {
|
|
408
|
+
Log.d(TAG, "onProductDetailsResponse() called for purchase");
|
|
409
|
+
Log.d(
|
|
410
|
+
TAG,
|
|
411
|
+
"Query result: " +
|
|
412
|
+
billingResult.getResponseCode() +
|
|
413
|
+
" - " +
|
|
414
|
+
billingResult.getDebugMessage()
|
|
415
|
+
);
|
|
416
|
+
Log.d(TAG, "Product details count: " + productDetailsList.size());
|
|
417
|
+
|
|
298
418
|
if (productDetailsList.size() == 0) {
|
|
419
|
+
Log.d(TAG, "No products found");
|
|
299
420
|
closeBillingClient();
|
|
300
421
|
call.reject("Product not found");
|
|
301
422
|
return;
|
|
@@ -305,16 +426,28 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
305
426
|
BillingFlowParams.ProductDetailsParams
|
|
306
427
|
> productDetailsParamsList = new ArrayList<>();
|
|
307
428
|
for (ProductDetails productDetailsItem : productDetailsList) {
|
|
429
|
+
Log.d(
|
|
430
|
+
TAG,
|
|
431
|
+
"Processing product: " + productDetailsItem.getProductId()
|
|
432
|
+
);
|
|
308
433
|
BillingFlowParams.ProductDetailsParams.Builder productDetailsParams =
|
|
309
434
|
BillingFlowParams.ProductDetailsParams.newBuilder()
|
|
310
435
|
.setProductDetails(productDetailsItem);
|
|
311
436
|
if (productType.equals("subs")) {
|
|
437
|
+
Log.d(TAG, "Processing subscription product");
|
|
312
438
|
// list the SubscriptionOfferDetails and find the one who match the planIdentifier if not found get the first one
|
|
313
439
|
ProductDetails.SubscriptionOfferDetails selectedOfferDetails =
|
|
314
440
|
null;
|
|
441
|
+
Log.d(
|
|
442
|
+
TAG,
|
|
443
|
+
"Available offer details count: " +
|
|
444
|
+
productDetailsItem.getSubscriptionOfferDetails().size()
|
|
445
|
+
);
|
|
315
446
|
for (ProductDetails.SubscriptionOfferDetails offerDetails : productDetailsItem.getSubscriptionOfferDetails()) {
|
|
447
|
+
Log.d(TAG, "Checking offer: " + offerDetails.getBasePlanId());
|
|
316
448
|
if (offerDetails.getBasePlanId().equals(planIdentifier)) {
|
|
317
449
|
selectedOfferDetails = offerDetails;
|
|
450
|
+
Log.d(TAG, "Found matching plan: " + planIdentifier);
|
|
318
451
|
break;
|
|
319
452
|
}
|
|
320
453
|
}
|
|
@@ -322,10 +455,19 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
322
455
|
selectedOfferDetails = productDetailsItem
|
|
323
456
|
.getSubscriptionOfferDetails()
|
|
324
457
|
.get(0);
|
|
458
|
+
Log.d(
|
|
459
|
+
TAG,
|
|
460
|
+
"Using first available offer: " +
|
|
461
|
+
selectedOfferDetails.getBasePlanId()
|
|
462
|
+
);
|
|
325
463
|
}
|
|
326
464
|
productDetailsParams.setOfferToken(
|
|
327
465
|
selectedOfferDetails.getOfferToken()
|
|
328
466
|
);
|
|
467
|
+
Log.d(
|
|
468
|
+
TAG,
|
|
469
|
+
"Set offer token: " + selectedOfferDetails.getOfferToken()
|
|
470
|
+
);
|
|
329
471
|
}
|
|
330
472
|
productDetailsParamsList.add(productDetailsParams.build());
|
|
331
473
|
}
|
|
@@ -333,10 +475,18 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
333
475
|
.setProductDetailsParamsList(productDetailsParamsList)
|
|
334
476
|
.build();
|
|
335
477
|
// Launch the billing flow
|
|
478
|
+
Log.d(TAG, "Launching billing flow");
|
|
336
479
|
BillingResult billingResult2 = billingClient.launchBillingFlow(
|
|
337
480
|
getActivity(),
|
|
338
481
|
billingFlowParams
|
|
339
482
|
);
|
|
483
|
+
Log.d(
|
|
484
|
+
TAG,
|
|
485
|
+
"Billing flow launch result: " +
|
|
486
|
+
billingResult2.getResponseCode() +
|
|
487
|
+
" - " +
|
|
488
|
+
billingResult2.getDebugMessage()
|
|
489
|
+
);
|
|
340
490
|
Log.i(
|
|
341
491
|
NativePurchasesPlugin.TAG,
|
|
342
492
|
"onProductDetailsResponse2" + billingResult2
|
|
@@ -345,16 +495,19 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
345
495
|
}
|
|
346
496
|
);
|
|
347
497
|
} catch (Exception e) {
|
|
498
|
+
Log.d(TAG, "Exception during purchase: " + e.getMessage());
|
|
348
499
|
closeBillingClient();
|
|
349
500
|
call.reject(e.getMessage());
|
|
350
501
|
}
|
|
351
502
|
}
|
|
352
503
|
|
|
353
504
|
private void processUnfinishedPurchases() {
|
|
505
|
+
Log.d(TAG, "processUnfinishedPurchases() called");
|
|
354
506
|
QueryPurchasesParams queryInAppPurchasesParams =
|
|
355
507
|
QueryPurchasesParams.newBuilder()
|
|
356
508
|
.setProductType(BillingClient.ProductType.INAPP)
|
|
357
509
|
.build();
|
|
510
|
+
Log.d(TAG, "Querying unfinished in-app purchases");
|
|
358
511
|
billingClient.queryPurchasesAsync(
|
|
359
512
|
queryInAppPurchasesParams,
|
|
360
513
|
this::handlePurchases
|
|
@@ -364,6 +517,7 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
364
517
|
QueryPurchasesParams.newBuilder()
|
|
365
518
|
.setProductType(BillingClient.ProductType.SUBS)
|
|
366
519
|
.build();
|
|
520
|
+
Log.d(TAG, "Querying unfinished subscription purchases");
|
|
367
521
|
billingClient.queryPurchasesAsync(
|
|
368
522
|
querySubscriptionsParams,
|
|
369
523
|
this::handlePurchases
|
|
@@ -374,21 +528,40 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
374
528
|
BillingResult billingResult,
|
|
375
529
|
List<Purchase> purchases
|
|
376
530
|
) {
|
|
531
|
+
Log.d(TAG, "handlePurchases() called");
|
|
532
|
+
Log.d(
|
|
533
|
+
TAG,
|
|
534
|
+
"Query purchases result: " +
|
|
535
|
+
billingResult.getResponseCode() +
|
|
536
|
+
" - " +
|
|
537
|
+
billingResult.getDebugMessage()
|
|
538
|
+
);
|
|
539
|
+
Log.d(
|
|
540
|
+
TAG,
|
|
541
|
+
"Purchases count: " + (purchases != null ? purchases.size() : 0)
|
|
542
|
+
);
|
|
543
|
+
|
|
377
544
|
if (
|
|
378
545
|
billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
|
|
379
546
|
) {
|
|
380
547
|
for (Purchase purchase : purchases) {
|
|
548
|
+
Log.d(TAG, "Processing purchase: " + purchase.getOrderId());
|
|
549
|
+
Log.d(TAG, "Purchase state: " + purchase.getPurchaseState());
|
|
381
550
|
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
|
|
382
551
|
if (purchase.isAcknowledged()) {
|
|
552
|
+
Log.d(TAG, "Purchase already acknowledged, consuming");
|
|
383
553
|
ConsumeParams consumeParams = ConsumeParams.newBuilder()
|
|
384
554
|
.setPurchaseToken(purchase.getPurchaseToken())
|
|
385
555
|
.build();
|
|
386
556
|
billingClient.consumeAsync(consumeParams, this::onConsumeResponse);
|
|
387
557
|
} else {
|
|
558
|
+
Log.d(TAG, "Purchase not acknowledged, acknowledging");
|
|
388
559
|
acknowledgePurchase(purchase.getPurchaseToken());
|
|
389
560
|
}
|
|
390
561
|
}
|
|
391
562
|
}
|
|
563
|
+
} else {
|
|
564
|
+
Log.d(TAG, "Query purchases failed");
|
|
392
565
|
}
|
|
393
566
|
}
|
|
394
567
|
|
|
@@ -396,17 +569,29 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
396
569
|
BillingResult billingResult,
|
|
397
570
|
String purchaseToken
|
|
398
571
|
) {
|
|
572
|
+
Log.d(TAG, "onConsumeResponse() called");
|
|
573
|
+
Log.d(
|
|
574
|
+
TAG,
|
|
575
|
+
"Consume result: " +
|
|
576
|
+
billingResult.getResponseCode() +
|
|
577
|
+
" - " +
|
|
578
|
+
billingResult.getDebugMessage()
|
|
579
|
+
);
|
|
580
|
+
Log.d(TAG, "Purchase token: " + purchaseToken);
|
|
581
|
+
|
|
399
582
|
if (
|
|
400
583
|
billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
|
|
401
584
|
) {
|
|
402
585
|
// Handle the success of the consume operation.
|
|
403
586
|
// For example, you can update the UI to reflect that the item has been consumed.
|
|
587
|
+
Log.d(TAG, "Consume operation successful");
|
|
404
588
|
Log.i(
|
|
405
589
|
NativePurchasesPlugin.TAG,
|
|
406
590
|
"onConsumeResponse OK " + billingResult + purchaseToken
|
|
407
591
|
);
|
|
408
592
|
} else {
|
|
409
593
|
// Handle error responses.
|
|
594
|
+
Log.d(TAG, "Consume operation failed");
|
|
410
595
|
Log.i(
|
|
411
596
|
NativePurchasesPlugin.TAG,
|
|
412
597
|
"onConsumeResponse OTHER " + billingResult + purchaseToken
|
|
@@ -416,10 +601,12 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
416
601
|
|
|
417
602
|
@PluginMethod
|
|
418
603
|
public void restorePurchases(PluginCall call) {
|
|
604
|
+
Log.d(TAG, "restorePurchases() called");
|
|
419
605
|
Log.d(NativePurchasesPlugin.TAG, "restorePurchases");
|
|
420
606
|
this.initBillingClient(null);
|
|
421
607
|
this.processUnfinishedPurchases();
|
|
422
608
|
call.resolve();
|
|
609
|
+
Log.d(TAG, "restorePurchases() completed");
|
|
423
610
|
}
|
|
424
611
|
|
|
425
612
|
private void queryProductDetails(
|
|
@@ -427,24 +614,41 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
427
614
|
String productType,
|
|
428
615
|
PluginCall call
|
|
429
616
|
) {
|
|
617
|
+
Log.d(TAG, "queryProductDetails() called");
|
|
618
|
+
Log.d(TAG, "Product identifiers count: " + productIdentifiers.size());
|
|
619
|
+
Log.d(TAG, "Product type: " + productType);
|
|
620
|
+
for (String id : productIdentifiers) {
|
|
621
|
+
Log.d(TAG, "Product ID: " + id);
|
|
622
|
+
}
|
|
623
|
+
|
|
430
624
|
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
|
|
431
625
|
for (String productIdentifier : productIdentifiers) {
|
|
626
|
+
String productTypeForQuery = productType.equals("inapp")
|
|
627
|
+
? BillingClient.ProductType.INAPP
|
|
628
|
+
: BillingClient.ProductType.SUBS;
|
|
629
|
+
Log.d(
|
|
630
|
+
TAG,
|
|
631
|
+
"Creating query product: ID='" +
|
|
632
|
+
productIdentifier +
|
|
633
|
+
"', Type='" +
|
|
634
|
+
productTypeForQuery +
|
|
635
|
+
"'"
|
|
636
|
+
);
|
|
432
637
|
productList.add(
|
|
433
638
|
QueryProductDetailsParams.Product.newBuilder()
|
|
434
639
|
.setProductId(productIdentifier)
|
|
435
|
-
.setProductType(
|
|
436
|
-
productType.equals("inapp")
|
|
437
|
-
? BillingClient.ProductType.INAPP
|
|
438
|
-
: BillingClient.ProductType.SUBS
|
|
439
|
-
)
|
|
640
|
+
.setProductType(productTypeForQuery)
|
|
440
641
|
.build()
|
|
441
642
|
);
|
|
442
643
|
}
|
|
644
|
+
Log.d(TAG, "Total products in query list: " + productList.size());
|
|
443
645
|
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
|
|
444
646
|
.setProductList(productList)
|
|
445
647
|
.build();
|
|
648
|
+
Log.d(TAG, "Initializing billing client for product query");
|
|
446
649
|
this.initBillingClient(call);
|
|
447
650
|
try {
|
|
651
|
+
Log.d(TAG, "Querying product details");
|
|
448
652
|
billingClient.queryProductDetailsAsync(
|
|
449
653
|
params,
|
|
450
654
|
new ProductDetailsResponseListener() {
|
|
@@ -452,25 +656,54 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
452
656
|
BillingResult billingResult,
|
|
453
657
|
List<ProductDetails> productDetailsList
|
|
454
658
|
) {
|
|
659
|
+
Log.d(TAG, "onProductDetailsResponse() called for query");
|
|
660
|
+
Log.d(
|
|
661
|
+
TAG,
|
|
662
|
+
"Query result: " +
|
|
663
|
+
billingResult.getResponseCode() +
|
|
664
|
+
" - " +
|
|
665
|
+
billingResult.getDebugMessage()
|
|
666
|
+
);
|
|
667
|
+
Log.d(TAG, "Product details count: " + productDetailsList.size());
|
|
668
|
+
|
|
455
669
|
if (productDetailsList.size() == 0) {
|
|
670
|
+
Log.d(TAG, "No products found in query");
|
|
671
|
+
Log.d(TAG, "This usually means:");
|
|
672
|
+
Log.d(TAG, "1. Product doesn't exist in Google Play Console");
|
|
673
|
+
Log.d(TAG, "2. Product is not published/active");
|
|
674
|
+
Log.d(
|
|
675
|
+
TAG,
|
|
676
|
+
"3. App is not properly configured for the product type"
|
|
677
|
+
);
|
|
678
|
+
Log.d(TAG, "4. Wrong product ID or type");
|
|
456
679
|
closeBillingClient();
|
|
457
680
|
call.reject("Product not found");
|
|
458
681
|
return;
|
|
459
682
|
}
|
|
460
683
|
JSONArray products = new JSONArray();
|
|
461
684
|
for (ProductDetails productDetails : productDetailsList) {
|
|
685
|
+
Log.d(
|
|
686
|
+
TAG,
|
|
687
|
+
"Processing product details: " + productDetails.getProductId()
|
|
688
|
+
);
|
|
462
689
|
JSObject product = new JSObject();
|
|
463
690
|
product.put("title", productDetails.getName());
|
|
464
691
|
product.put("description", productDetails.getDescription());
|
|
692
|
+
Log.d(TAG, "Product title: " + productDetails.getName());
|
|
693
|
+
Log.d(
|
|
694
|
+
TAG,
|
|
695
|
+
"Product description: " + productDetails.getDescription()
|
|
696
|
+
);
|
|
697
|
+
|
|
465
698
|
if (productType.equals("inapp")) {
|
|
699
|
+
Log.d(TAG, "Processing as in-app product");
|
|
466
700
|
product.put("identifier", productDetails.getProductId());
|
|
467
|
-
|
|
468
|
-
"price",
|
|
701
|
+
double price =
|
|
469
702
|
productDetails
|
|
470
703
|
.getOneTimePurchaseOfferDetails()
|
|
471
704
|
.getPriceAmountMicros() /
|
|
472
|
-
1000000.0
|
|
473
|
-
);
|
|
705
|
+
1000000.0;
|
|
706
|
+
product.put("price", price);
|
|
474
707
|
product.put(
|
|
475
708
|
"priceString",
|
|
476
709
|
productDetails
|
|
@@ -483,20 +716,35 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
483
716
|
.getOneTimePurchaseOfferDetails()
|
|
484
717
|
.getPriceCurrencyCode()
|
|
485
718
|
);
|
|
719
|
+
Log.d(TAG, "Price: " + price);
|
|
720
|
+
Log.d(
|
|
721
|
+
TAG,
|
|
722
|
+
"Formatted price: " +
|
|
723
|
+
productDetails
|
|
724
|
+
.getOneTimePurchaseOfferDetails()
|
|
725
|
+
.getFormattedPrice()
|
|
726
|
+
);
|
|
727
|
+
Log.d(
|
|
728
|
+
TAG,
|
|
729
|
+
"Currency: " +
|
|
730
|
+
productDetails
|
|
731
|
+
.getOneTimePurchaseOfferDetails()
|
|
732
|
+
.getPriceCurrencyCode()
|
|
733
|
+
);
|
|
486
734
|
} else {
|
|
735
|
+
Log.d(TAG, "Processing as subscription product");
|
|
487
736
|
ProductDetails.SubscriptionOfferDetails selectedOfferDetails =
|
|
488
737
|
productDetails.getSubscriptionOfferDetails().get(0);
|
|
489
738
|
product.put("planIdentifier", productDetails.getProductId());
|
|
490
739
|
product.put("identifier", selectedOfferDetails.getBasePlanId());
|
|
491
|
-
|
|
492
|
-
"price",
|
|
740
|
+
double price =
|
|
493
741
|
selectedOfferDetails
|
|
494
742
|
.getPricingPhases()
|
|
495
743
|
.getPricingPhaseList()
|
|
496
744
|
.get(0)
|
|
497
745
|
.getPriceAmountMicros() /
|
|
498
|
-
1000000.0
|
|
499
|
-
);
|
|
746
|
+
1000000.0;
|
|
747
|
+
product.put("price", price);
|
|
500
748
|
product.put(
|
|
501
749
|
"priceString",
|
|
502
750
|
selectedOfferDetails
|
|
@@ -513,18 +761,44 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
513
761
|
.get(0)
|
|
514
762
|
.getPriceCurrencyCode()
|
|
515
763
|
);
|
|
764
|
+
Log.d(TAG, "Plan identifier: " + productDetails.getProductId());
|
|
765
|
+
Log.d(
|
|
766
|
+
TAG,
|
|
767
|
+
"Base plan ID: " + selectedOfferDetails.getBasePlanId()
|
|
768
|
+
);
|
|
769
|
+
Log.d(TAG, "Price: " + price);
|
|
770
|
+
Log.d(
|
|
771
|
+
TAG,
|
|
772
|
+
"Formatted price: " +
|
|
773
|
+
selectedOfferDetails
|
|
774
|
+
.getPricingPhases()
|
|
775
|
+
.getPricingPhaseList()
|
|
776
|
+
.get(0)
|
|
777
|
+
.getFormattedPrice()
|
|
778
|
+
);
|
|
779
|
+
Log.d(
|
|
780
|
+
TAG,
|
|
781
|
+
"Currency: " +
|
|
782
|
+
selectedOfferDetails
|
|
783
|
+
.getPricingPhases()
|
|
784
|
+
.getPricingPhaseList()
|
|
785
|
+
.get(0)
|
|
786
|
+
.getPriceCurrencyCode()
|
|
787
|
+
);
|
|
516
788
|
}
|
|
517
789
|
product.put("isFamilyShareable", false);
|
|
518
790
|
products.put(product);
|
|
519
791
|
}
|
|
520
792
|
JSObject ret = new JSObject();
|
|
521
793
|
ret.put("products", products);
|
|
794
|
+
Log.d(TAG, "Returning " + products.length() + " products");
|
|
522
795
|
closeBillingClient();
|
|
523
796
|
call.resolve(ret);
|
|
524
797
|
}
|
|
525
798
|
}
|
|
526
799
|
);
|
|
527
800
|
} catch (Exception e) {
|
|
801
|
+
Log.d(TAG, "Exception during product query: " + e.getMessage());
|
|
528
802
|
closeBillingClient();
|
|
529
803
|
call.reject(e.getMessage());
|
|
530
804
|
}
|
|
@@ -532,26 +806,51 @@ public class NativePurchasesPlugin extends Plugin {
|
|
|
532
806
|
|
|
533
807
|
@PluginMethod
|
|
534
808
|
public void getProducts(PluginCall call) {
|
|
809
|
+
Log.d(TAG, "getProducts() called");
|
|
535
810
|
JSONArray productIdentifiersArray = call.getArray("productIdentifiers");
|
|
536
811
|
String productType = call.getString("productType", "inapp");
|
|
812
|
+
Log.d(TAG, "Product type: " + productType);
|
|
813
|
+
Log.d(TAG, "Raw productIdentifiersArray: " + productIdentifiersArray);
|
|
814
|
+
Log.d(
|
|
815
|
+
TAG,
|
|
816
|
+
"productIdentifiersArray length: " +
|
|
817
|
+
(productIdentifiersArray != null
|
|
818
|
+
? productIdentifiersArray.length()
|
|
819
|
+
: "null")
|
|
820
|
+
);
|
|
821
|
+
|
|
537
822
|
if (
|
|
538
823
|
productIdentifiersArray == null || productIdentifiersArray.length() == 0
|
|
539
824
|
) {
|
|
825
|
+
Log.d(TAG, "Error: productIdentifiers array missing or empty");
|
|
540
826
|
call.reject("productIdentifiers array missing");
|
|
541
827
|
return;
|
|
542
828
|
}
|
|
829
|
+
|
|
543
830
|
List<String> productIdentifiers = new ArrayList<>();
|
|
544
831
|
for (int i = 0; i < productIdentifiersArray.length(); i++) {
|
|
545
|
-
|
|
832
|
+
String productId = productIdentifiersArray.optString(i, "");
|
|
833
|
+
Log.d(TAG, "Array index " + i + ": '" + productId + "'");
|
|
834
|
+
productIdentifiers.add(productId);
|
|
835
|
+
Log.d(TAG, "Added product identifier: " + productId);
|
|
546
836
|
}
|
|
837
|
+
Log.d(
|
|
838
|
+
TAG,
|
|
839
|
+
"Final productIdentifiers list: " + productIdentifiers.toString()
|
|
840
|
+
);
|
|
547
841
|
queryProductDetails(productIdentifiers, productType, call);
|
|
548
842
|
}
|
|
549
843
|
|
|
550
844
|
@PluginMethod
|
|
551
845
|
public void getProduct(PluginCall call) {
|
|
846
|
+
Log.d(TAG, "getProduct() called");
|
|
552
847
|
String productIdentifier = call.getString("productIdentifier");
|
|
553
848
|
String productType = call.getString("productType", "inapp");
|
|
849
|
+
Log.d(TAG, "Product identifier: " + productIdentifier);
|
|
850
|
+
Log.d(TAG, "Product type: " + productType);
|
|
851
|
+
|
|
554
852
|
if (productIdentifier.isEmpty()) {
|
|
853
|
+
Log.d(TAG, "Error: productIdentifier is empty");
|
|
555
854
|
call.reject("productIdentifier is empty");
|
|
556
855
|
return;
|
|
557
856
|
}
|
|
@@ -114,16 +114,18 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
114
114
|
@objc func getProducts(_ call: CAPPluginCall) {
|
|
115
115
|
if #available(iOS 15.0, *) {
|
|
116
116
|
let productIdentifiers = call.getArray("productIdentifiers", String.self) ?? []
|
|
117
|
+
print("productIdentifiers \(productIdentifiers)")
|
|
117
118
|
DispatchQueue.global().async {
|
|
118
119
|
Task {
|
|
119
120
|
do {
|
|
120
121
|
let products = try await Product.products(for: productIdentifiers)
|
|
122
|
+
print("products \(products)")
|
|
121
123
|
let productsJson: [[String: Any]] = products.map { $0.dictionary }
|
|
122
124
|
call.resolve([
|
|
123
125
|
"products": productsJson
|
|
124
126
|
])
|
|
125
127
|
} catch {
|
|
126
|
-
print(error)
|
|
128
|
+
print("error \(error)")
|
|
127
129
|
call.reject(error.localizedDescription)
|
|
128
130
|
}
|
|
129
131
|
}
|
|
@@ -137,6 +139,7 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
137
139
|
@objc func getProduct(_ call: CAPPluginCall) {
|
|
138
140
|
if #available(iOS 15.0, *) {
|
|
139
141
|
let productIdentifier = call.getString("productIdentifier") ?? ""
|
|
142
|
+
print("productIdentifier \(productIdentifier)")
|
|
140
143
|
if productIdentifier.isEmpty {
|
|
141
144
|
call.reject("productIdentifier is empty")
|
|
142
145
|
return
|
|
@@ -146,6 +149,7 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
146
149
|
Task {
|
|
147
150
|
do {
|
|
148
151
|
let products = try await Product.products(for: [productIdentifier])
|
|
152
|
+
print("products \(products)")
|
|
149
153
|
if let product = products.first {
|
|
150
154
|
let productJson = product.dictionary
|
|
151
155
|
call.resolve(["product": productJson])
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/native-purchases",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.29",
|
|
4
4
|
"description": "In-app Subscriptions Made Easy",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/Cap-go/native-purchases.git"
|
|
20
|
+
"url": "git+https://github.com/Cap-go/capacitor-native-purchases.git"
|
|
21
21
|
},
|
|
22
22
|
"bugs": {
|
|
23
|
-
"url": "https://github.com/Cap-go/native-purchases/issues"
|
|
23
|
+
"url": "https://github.com/Cap-go/capacitor-native-purchases/issues"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"capacitor",
|