@capgo/native-purchases 7.7.13 → 7.8.2

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.
@@ -2,6 +2,7 @@ package ee.forgr.nativepurchases;
2
2
 
3
3
  import android.util.Log;
4
4
  import androidx.annotation.NonNull;
5
+ import com.android.billingclient.api.AccountIdentifiers;
5
6
  import com.android.billingclient.api.AcknowledgePurchaseParams;
6
7
  import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
7
8
  import com.android.billingclient.api.BillingClient;
@@ -36,1030 +37,745 @@ import org.json.JSONArray;
36
37
  @CapacitorPlugin(name = "NativePurchases")
37
38
  public class NativePurchasesPlugin extends Plugin {
38
39
 
39
- public final String PLUGIN_VERSION = "0.0.25";
40
- public static final String TAG = "NativePurchases";
41
- private static final Phaser semaphoreReady = new Phaser(1);
42
- private BillingClient billingClient;
43
-
44
- @PluginMethod
45
- public void isBillingSupported(PluginCall call) {
46
- Log.d(TAG, "isBillingSupported() called");
47
- JSObject ret = new JSObject();
48
- ret.put("isBillingSupported", true);
49
- Log.d(TAG, "isBillingSupported() returning true");
50
- call.resolve(ret);
51
- }
52
-
53
- @Override
54
- public void load() {
55
- super.load();
56
- Log.d(TAG, "Plugin load() called");
57
- Log.i(NativePurchasesPlugin.TAG, "load");
58
- semaphoreDown();
59
- Log.d(TAG, "Plugin load() completed");
60
- }
61
-
62
- private void semaphoreWait() {
63
- Log.d(TAG, "semaphoreWait() called with waitTime: " + (Number) 10);
64
- Log.i(NativePurchasesPlugin.TAG, "semaphoreWait " + (Number) 10);
65
- try {
66
- // Log.i(CapacitorUpdater.TAG, "semaphoreReady count " + CapacitorUpdaterPlugin.this.semaphoreReady.getCount());
67
- semaphoreReady.awaitAdvanceInterruptibly(
68
- semaphoreReady.getPhase(),
69
- ((Number) 10).longValue(),
70
- TimeUnit.SECONDS
71
- );
72
- // Log.i(CapacitorUpdater.TAG, "semaphoreReady await " + res);
73
- Log.i(
74
- NativePurchasesPlugin.TAG,
75
- "semaphoreReady count " + semaphoreReady.getPhase()
76
- );
77
- Log.d(TAG, "semaphoreWait() completed successfully");
78
- } catch (InterruptedException e) {
79
- Log.d(TAG, "semaphoreWait() InterruptedException: " + e.getMessage());
80
- Log.i(NativePurchasesPlugin.TAG, "semaphoreWait InterruptedException");
81
- e.printStackTrace();
82
- } catch (TimeoutException e) {
83
- Log.d(TAG, "semaphoreWait() TimeoutException: " + e.getMessage());
84
- throw new RuntimeException(e);
40
+ public final String PLUGIN_VERSION = "0.0.25";
41
+ public static final String TAG = "NativePurchases";
42
+ private static final Phaser semaphoreReady = new Phaser(1);
43
+ private BillingClient billingClient;
44
+
45
+ @PluginMethod
46
+ public void isBillingSupported(PluginCall call) {
47
+ Log.d(TAG, "isBillingSupported() called");
48
+ JSObject ret = new JSObject();
49
+ ret.put("isBillingSupported", true);
50
+ Log.d(TAG, "isBillingSupported() returning true");
51
+ call.resolve(ret);
85
52
  }
86
- }
87
-
88
- private void semaphoreUp() {
89
- Log.d(TAG, "semaphoreUp() called");
90
- Log.i(NativePurchasesPlugin.TAG, "semaphoreUp");
91
- semaphoreReady.register();
92
- Log.d(TAG, "semaphoreUp() completed");
93
- }
94
53
 
95
- private void semaphoreDown() {
96
- Log.d(TAG, "semaphoreDown() called");
97
- Log.i(NativePurchasesPlugin.TAG, "semaphoreDown");
98
- Log.i(
99
- NativePurchasesPlugin.TAG,
100
- "semaphoreDown count " + semaphoreReady.getPhase()
101
- );
102
- semaphoreReady.arriveAndDeregister();
103
- Log.d(TAG, "semaphoreDown() completed");
104
- }
105
-
106
- private void closeBillingClient() {
107
- Log.d(TAG, "closeBillingClient() called");
108
- if (billingClient != null) {
109
- Log.d(TAG, "Ending billing client connection");
110
- billingClient.endConnection();
111
- billingClient = null;
112
- semaphoreDown();
113
- Log.d(TAG, "Billing client closed and set to null");
114
- } else {
115
- Log.d(TAG, "Billing client was already null");
54
+ @Override
55
+ public void load() {
56
+ super.load();
57
+ Log.d(TAG, "Plugin load() called");
58
+ Log.i(NativePurchasesPlugin.TAG, "load");
59
+ semaphoreDown();
60
+ Log.d(TAG, "Plugin load() completed");
116
61
  }
117
- }
118
-
119
- private void handlePurchase(Purchase purchase, PluginCall purchaseCall) {
120
- Log.d(TAG, "handlePurchase() called");
121
- Log.d(TAG, "Purchase details: " + purchase.toString());
122
- Log.i(NativePurchasesPlugin.TAG, "handlePurchase" + purchase);
123
- Log.i(
124
- NativePurchasesPlugin.TAG,
125
- "getPurchaseState" + purchase.getPurchaseState()
126
- );
127
- Log.d(TAG, "Purchase state: " + purchase.getPurchaseState());
128
- Log.d(TAG, "Purchase token: " + purchase.getPurchaseToken());
129
- Log.d(TAG, "Is acknowledged: " + purchase.isAcknowledged());
130
-
131
- if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
132
- Log.d(TAG, "Purchase state is PURCHASED");
133
- // Grant entitlement to the user, then acknowledge the purchase
134
- // if sub then acknowledgePurchase
135
- // if one time then consumePurchase
136
- if (purchase.isAcknowledged()) {
137
- Log.d(TAG, "Purchase already acknowledged, consuming...");
138
- ConsumeParams consumeParams = ConsumeParams.newBuilder()
139
- .setPurchaseToken(purchase.getPurchaseToken())
140
- .build();
141
- billingClient.consumeAsync(consumeParams, this::onConsumeResponse);
142
- } else {
143
- Log.d(TAG, "Purchase not acknowledged, acknowledging...");
144
- acknowledgePurchase(purchase.getPurchaseToken());
145
- }
146
-
147
- JSObject ret = new JSObject();
148
- ret.put("transactionId", purchase.getPurchaseToken());
149
- ret.put("productIdentifier", purchase.getProducts().get(0));
150
- ret.put(
151
- "purchaseDate",
152
- new java.text.SimpleDateFormat(
153
- "yyyy-MM-dd'T'HH:mm:ss'Z'",
154
- java.util.Locale.US
155
- ).format(new java.util.Date(purchase.getPurchaseTime()))
156
- );
157
- ret.put("quantity", purchase.getQuantity());
158
- ret.put(
159
- "productType",
160
- purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED
161
- ? "inapp"
162
- : "subs"
163
- );
164
- ret.put("orderId", purchase.getOrderId());
165
- ret.put("purchaseToken", purchase.getPurchaseToken());
166
- ret.put("isAcknowledged", purchase.isAcknowledged());
167
- ret.put("purchaseState", String.valueOf(purchase.getPurchaseState()));
168
62
 
169
- // Add cancellation information - ALWAYS set willCancel
170
- // Note: Android doesn't provide direct cancellation information in the Purchase object
171
- // This would require additional Google Play API calls to get detailed subscription status
172
- ret.put("willCancel", null); // Default to null, would need API call to determine actual cancellation date
63
+ private void semaphoreWait() {
64
+ Log.d(TAG, "semaphoreWait() called with waitTime: " + (Number) 10);
65
+ Log.i(NativePurchasesPlugin.TAG, "semaphoreWait " + (Number) 10);
66
+ try {
67
+ // Log.i(CapacitorUpdater.TAG, "semaphoreReady count " + CapacitorUpdaterPlugin.this.semaphoreReady.getCount());
68
+ semaphoreReady.awaitAdvanceInterruptibly(semaphoreReady.getPhase(), ((Number) 10).longValue(), TimeUnit.SECONDS);
69
+ // Log.i(CapacitorUpdater.TAG, "semaphoreReady await " + res);
70
+ Log.i(NativePurchasesPlugin.TAG, "semaphoreReady count " + semaphoreReady.getPhase());
71
+ Log.d(TAG, "semaphoreWait() completed successfully");
72
+ } catch (InterruptedException e) {
73
+ Log.d(TAG, "semaphoreWait() InterruptedException: " + e.getMessage());
74
+ Log.i(NativePurchasesPlugin.TAG, "semaphoreWait InterruptedException");
75
+ e.printStackTrace();
76
+ } catch (TimeoutException e) {
77
+ Log.d(TAG, "semaphoreWait() TimeoutException: " + e.getMessage());
78
+ throw new RuntimeException(e);
79
+ }
80
+ }
173
81
 
174
- // For subscriptions, try to get additional information
175
- if (
176
- purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED &&
177
- purchase.getProducts().get(0).contains("sub")
178
- ) {
179
- // Note: Android doesn't provide direct expiration date in Purchase object
180
- // This would need to be calculated based on subscription period or fetched from Google Play API
181
- ret.put("productType", "subs");
182
- // For subscriptions, we can't get expiration date directly from Purchase object
183
- // This would require additional Google Play API calls to get subscription details
184
- }
82
+ private void semaphoreUp() {
83
+ Log.d(TAG, "semaphoreUp() called");
84
+ Log.i(NativePurchasesPlugin.TAG, "semaphoreUp");
85
+ semaphoreReady.register();
86
+ Log.d(TAG, "semaphoreUp() completed");
87
+ }
185
88
 
186
- Log.d(
187
- TAG,
188
- "Resolving purchase call with transactionId: " +
189
- purchase.getPurchaseToken()
190
- );
191
- if (purchaseCall != null) {
192
- purchaseCall.resolve(ret);
193
- } else {
194
- Log.d(TAG, "purchaseCall is null, cannot resolve");
195
- }
196
- } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
197
- Log.d(TAG, "Purchase state is PENDING");
198
- // Here you can confirm to the user that they've started the pending
199
- // purchase, and to complete it, they should follow instructions that are
200
- // given to them. You can also choose to remind the user to complete the
201
- // purchase if you detect that it is still pending.
202
- if (purchaseCall != null) {
203
- purchaseCall.reject("Purchase is pending");
204
- } else {
205
- Log.d(TAG, "purchaseCall is null for pending purchase");
206
- }
207
- } else {
208
- Log.d(TAG, "Purchase state is OTHER: " + purchase.getPurchaseState());
209
- // Handle any other error codes.
210
- if (purchaseCall != null) {
211
- purchaseCall.reject("Purchase is not purchased");
212
- } else {
213
- Log.d(TAG, "purchaseCall is null for failed purchase");
214
- }
89
+ private void semaphoreDown() {
90
+ Log.d(TAG, "semaphoreDown() called");
91
+ Log.i(NativePurchasesPlugin.TAG, "semaphoreDown");
92
+ Log.i(NativePurchasesPlugin.TAG, "semaphoreDown count " + semaphoreReady.getPhase());
93
+ semaphoreReady.arriveAndDeregister();
94
+ Log.d(TAG, "semaphoreDown() completed");
215
95
  }
216
- }
217
96
 
218
- private void acknowledgePurchase(String purchaseToken) {
219
- Log.d(TAG, "acknowledgePurchase() called with token: " + purchaseToken);
220
- AcknowledgePurchaseParams acknowledgePurchaseParams =
221
- AcknowledgePurchaseParams.newBuilder()
222
- .setPurchaseToken(purchaseToken)
223
- .build();
224
- billingClient.acknowledgePurchase(
225
- acknowledgePurchaseParams,
226
- new AcknowledgePurchaseResponseListener() {
227
- @Override
228
- public void onAcknowledgePurchaseResponse(
229
- @NonNull BillingResult billingResult
230
- ) {
231
- // Handle the result of the acknowledge purchase
232
- Log.d(TAG, "onAcknowledgePurchaseResponse() called");
233
- Log.d(
234
- TAG,
235
- "Acknowledge result: " +
236
- billingResult.getResponseCode() +
237
- " - " +
238
- billingResult.getDebugMessage()
239
- );
240
- Log.i(
241
- NativePurchasesPlugin.TAG,
242
- "onAcknowledgePurchaseResponse" + billingResult
243
- );
97
+ private void closeBillingClient() {
98
+ Log.d(TAG, "closeBillingClient() called");
99
+ if (billingClient != null) {
100
+ Log.d(TAG, "Ending billing client connection");
101
+ billingClient.endConnection();
102
+ billingClient = null;
103
+ semaphoreDown();
104
+ Log.d(TAG, "Billing client closed and set to null");
105
+ } else {
106
+ Log.d(TAG, "Billing client was already null");
244
107
  }
245
- }
246
- );
247
- }
108
+ }
248
109
 
249
- private void initBillingClient(PluginCall purchaseCall) {
250
- Log.d(TAG, "initBillingClient() called");
251
- semaphoreWait();
252
- closeBillingClient();
253
- semaphoreUp();
254
- CountDownLatch semaphoreReady = new CountDownLatch(1);
255
- Log.d(TAG, "Creating new BillingClient");
256
- billingClient = BillingClient.newBuilder(getContext())
257
- .setListener(
258
- new PurchasesUpdatedListener() {
259
- @Override
260
- public void onPurchasesUpdated(
261
- @NonNull BillingResult billingResult,
262
- List<Purchase> purchases
263
- ) {
264
- Log.d(TAG, "onPurchasesUpdated() called");
265
- Log.d(
266
- TAG,
267
- "Billing result: " +
268
- billingResult.getResponseCode() +
269
- " - " +
270
- billingResult.getDebugMessage()
271
- );
272
- Log.d(
273
- TAG,
274
- "Purchases count: " + (purchases != null ? purchases.size() : 0)
275
- );
276
- Log.i(
277
- NativePurchasesPlugin.TAG,
278
- "onPurchasesUpdated" + billingResult
110
+ private void handlePurchase(Purchase purchase, PluginCall purchaseCall) {
111
+ Log.d(TAG, "handlePurchase() called");
112
+ Log.d(TAG, "Purchase details: " + purchase.toString());
113
+ Log.i(NativePurchasesPlugin.TAG, "handlePurchase" + purchase);
114
+ Log.i(NativePurchasesPlugin.TAG, "getPurchaseState" + purchase.getPurchaseState());
115
+ Log.d(TAG, "Purchase state: " + purchase.getPurchaseState());
116
+ Log.d(TAG, "Purchase token: " + purchase.getPurchaseToken());
117
+ Log.d(TAG, "Is acknowledged: " + purchase.isAcknowledged());
118
+
119
+ if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
120
+ Log.d(TAG, "Purchase state is PURCHASED");
121
+ // Grant entitlement to the user, then acknowledge the purchase
122
+ // if sub then acknowledgePurchase
123
+ // if one time then consumePurchase
124
+ AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
125
+ String purchaseAccountId = accountIdentifiers != null ? accountIdentifiers.getObfuscatedAccountId() : null;
126
+ Log.d(TAG, "Purchase account identifier present: " + (purchaseAccountId != null ? "[REDACTED]" : "none"));
127
+ if (purchase.isAcknowledged()) {
128
+ Log.d(TAG, "Purchase already acknowledged, consuming...");
129
+ ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
130
+ billingClient.consumeAsync(consumeParams, this::onConsumeResponse);
131
+ } else {
132
+ Log.d(TAG, "Purchase not acknowledged, acknowledging...");
133
+ acknowledgePurchase(purchase.getPurchaseToken());
134
+ }
135
+
136
+ JSObject ret = new JSObject();
137
+ ret.put("transactionId", purchase.getPurchaseToken());
138
+ ret.put("productIdentifier", purchase.getProducts().get(0));
139
+ ret.put(
140
+ "purchaseDate",
141
+ new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.US).format(
142
+ new java.util.Date(purchase.getPurchaseTime())
143
+ )
279
144
  );
280
- if (
281
- billingResult.getResponseCode() ==
282
- BillingClient.BillingResponseCode.OK &&
283
- purchases != null
284
- ) {
285
- Log.d(
286
- TAG,
287
- "Purchase update successful, processing first purchase"
288
- );
289
- // for (Purchase purchase : purchases) {
290
- // handlePurchase(purchase, purchaseCall);
291
- // }
292
- handlePurchase(purchases.get(0), purchaseCall);
145
+ ret.put("quantity", purchase.getQuantity());
146
+ ret.put("productType", purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED ? "inapp" : "subs");
147
+ ret.put("orderId", purchase.getOrderId());
148
+ ret.put("purchaseToken", purchase.getPurchaseToken());
149
+ ret.put("isAcknowledged", purchase.isAcknowledged());
150
+ ret.put("purchaseState", String.valueOf(purchase.getPurchaseState()));
151
+ ret.put("appAccountToken", purchaseAccountId);
152
+
153
+ // Add cancellation information - ALWAYS set willCancel
154
+ // Note: Android doesn't provide direct cancellation information in the Purchase object
155
+ // This would require additional Google Play API calls to get detailed subscription status
156
+ ret.put("willCancel", null); // Default to null, would need API call to determine actual cancellation date
157
+
158
+ // For subscriptions, try to get additional information
159
+ if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.getProducts().get(0).contains("sub")) {
160
+ // Note: Android doesn't provide direct expiration date in Purchase object
161
+ // This would need to be calculated based on subscription period or fetched from Google Play API
162
+ ret.put("productType", "subs");
163
+ // For subscriptions, we can't get expiration date directly from Purchase object
164
+ // This would require additional Google Play API calls to get subscription details
165
+ }
166
+
167
+ Log.d(TAG, "Resolving purchase call with transactionId: " + purchase.getPurchaseToken());
168
+ if (purchaseCall != null) {
169
+ purchaseCall.resolve(ret);
170
+ } else {
171
+ Log.d(TAG, "purchaseCall is null, cannot resolve");
172
+ }
173
+ } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
174
+ Log.d(TAG, "Purchase state is PENDING");
175
+ // Here you can confirm to the user that they've started the pending
176
+ // purchase, and to complete it, they should follow instructions that are
177
+ // given to them. You can also choose to remind the user to complete the
178
+ // purchase if you detect that it is still pending.
179
+ if (purchaseCall != null) {
180
+ purchaseCall.reject("Purchase is pending");
293
181
  } else {
294
- // Handle any other error codes.
295
- Log.d(TAG, "Purchase update failed or purchases is null");
296
- Log.i(
297
- NativePurchasesPlugin.TAG,
298
- "onPurchasesUpdated" + billingResult
299
- );
300
- if (purchaseCall != null) {
182
+ Log.d(TAG, "purchaseCall is null for pending purchase");
183
+ }
184
+ } else {
185
+ Log.d(TAG, "Purchase state is OTHER: " + purchase.getPurchaseState());
186
+ // Handle any other error codes.
187
+ if (purchaseCall != null) {
301
188
  purchaseCall.reject("Purchase is not purchased");
302
- }
189
+ } else {
190
+ Log.d(TAG, "purchaseCall is null for failed purchase");
303
191
  }
304
- closeBillingClient();
305
- return;
306
- }
307
192
  }
308
- )
309
- .enablePendingPurchases(
310
- PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()
311
- )
312
- .build();
313
- Log.d(TAG, "Starting billing client connection");
314
- billingClient.startConnection(
315
- new BillingClientStateListener() {
316
- @Override
317
- public void onBillingSetupFinished(
318
- @NonNull BillingResult billingResult
319
- ) {
320
- Log.d(TAG, "onBillingSetupFinished() called");
321
- Log.d(
322
- TAG,
323
- "Setup result: " +
324
- billingResult.getResponseCode() +
325
- " - " +
326
- billingResult.getDebugMessage()
327
- );
328
- if (
329
- billingResult.getResponseCode() ==
330
- BillingClient.BillingResponseCode.OK
331
- ) {
332
- Log.d(TAG, "Billing setup successful, client is ready");
333
- // The BillingClient is ready. You can query purchases here.
334
- semaphoreReady.countDown();
335
- } else {
336
- Log.d(TAG, "Billing setup failed");
337
- }
338
- }
339
-
340
- @Override
341
- public void onBillingServiceDisconnected() {
342
- Log.d(TAG, "onBillingServiceDisconnected() called");
343
- // Try to restart the connection on the next request to
344
- // Google Play by calling the startConnection() method.
345
- }
346
- }
347
- );
348
- try {
349
- Log.d(TAG, "Waiting for billing client setup to finish");
350
- semaphoreReady.await();
351
- Log.d(TAG, "Billing client setup completed");
352
- } catch (InterruptedException e) {
353
- Log.d(
354
- TAG,
355
- "InterruptedException while waiting for billing setup: " +
356
- e.getMessage()
357
- );
358
- e.printStackTrace();
359
- }
360
- }
361
-
362
- @PluginMethod
363
- public void getPluginVersion(final PluginCall call) {
364
- Log.d(TAG, "getPluginVersion() called");
365
- try {
366
- final JSObject ret = new JSObject();
367
- ret.put("version", this.PLUGIN_VERSION);
368
- Log.d(TAG, "Returning plugin version: " + this.PLUGIN_VERSION);
369
- call.resolve(ret);
370
- } catch (final Exception e) {
371
- Log.d(TAG, "Error getting plugin version: " + e.getMessage());
372
- call.reject("Could not get plugin version", e);
373
193
  }
374
- }
375
-
376
- @PluginMethod
377
- public void purchaseProduct(PluginCall call) {
378
- Log.d(TAG, "purchaseProduct() called");
379
- String productIdentifier = call.getString("productIdentifier");
380
- String planIdentifier = call.getString("planIdentifier");
381
- String productType = call.getString("productType", "inapp");
382
- Number quantity = call.getInt("quantity", 1);
383
194
 
384
- Log.d(TAG, "Product identifier: " + productIdentifier);
385
- Log.d(TAG, "Plan identifier: " + planIdentifier);
386
- Log.d(TAG, "Product type: " + productType);
387
- Log.d(TAG, "Quantity: " + quantity);
388
-
389
- // cannot use quantity, because it's done in native modal
390
- Log.d("CapacitorPurchases", "purchaseProduct: " + productIdentifier);
391
- if (productIdentifier == null || productIdentifier.isEmpty()) {
392
- // Handle error: productIdentifier is empty
393
- Log.d(TAG, "Error: productIdentifier is empty");
394
- call.reject("productIdentifier is empty");
395
- return;
396
- }
397
- if (productType == null || productType.isEmpty()) {
398
- // Handle error: productType is empty
399
- Log.d(TAG, "Error: productType is empty");
400
- call.reject("productType is empty");
401
- return;
402
- }
403
- if (
404
- productType.equals("subs") &&
405
- (planIdentifier == null || planIdentifier.isEmpty())
406
- ) {
407
- // Handle error: no planIdentifier with productType subs
408
- Log.d(
409
- TAG,
410
- "Error: planIdentifier cannot be empty if productType is subs"
411
- );
412
- call.reject("planIdentifier cannot be empty if productType is subs");
413
- return;
414
- }
415
- assert quantity != null;
416
- if (quantity.intValue() < 1) {
417
- // Handle error: quantity is less than 1
418
- Log.d(TAG, "Error: quantity is less than 1");
419
- call.reject("quantity is less than 1");
420
- return;
195
+ private void acknowledgePurchase(String purchaseToken) {
196
+ Log.d(TAG, "acknowledgePurchase() called with token: " + purchaseToken);
197
+ AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
198
+ .setPurchaseToken(purchaseToken)
199
+ .build();
200
+ billingClient.acknowledgePurchase(
201
+ acknowledgePurchaseParams,
202
+ new AcknowledgePurchaseResponseListener() {
203
+ @Override
204
+ public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
205
+ // Handle the result of the acknowledge purchase
206
+ Log.d(TAG, "onAcknowledgePurchaseResponse() called");
207
+ Log.d(TAG, "Acknowledge result: " + billingResult.getResponseCode() + " - " + billingResult.getDebugMessage());
208
+ Log.i(NativePurchasesPlugin.TAG, "onAcknowledgePurchaseResponse" + billingResult);
209
+ }
210
+ }
211
+ );
421
212
  }
422
213
 
423
- // For subscriptions, always use the productIdentifier (subscription ID) to query
424
- // The planIdentifier is used later when setting the offer token
425
- Log.d(TAG, "Using product ID for query: " + productIdentifier);
426
-
427
- ImmutableList<QueryProductDetailsParams.Product> productList =
428
- ImmutableList.of(
429
- QueryProductDetailsParams.Product.newBuilder()
430
- .setProductId(productIdentifier)
431
- .setProductType(
432
- productType.equals("inapp")
433
- ? BillingClient.ProductType.INAPP
434
- : BillingClient.ProductType.SUBS
435
- )
436
- .build()
437
- );
438
- QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
439
- .setProductList(productList)
440
- .build();
441
- Log.d(TAG, "Initializing billing client for purchase");
442
- this.initBillingClient(call);
443
- try {
444
- Log.d(TAG, "Querying product details for purchase");
445
- billingClient.queryProductDetailsAsync(
446
- params,
447
- new ProductDetailsResponseListener() {
448
- @Override
449
- public void onProductDetailsResponse(
450
- @NonNull BillingResult billingResult,
451
- @NonNull QueryProductDetailsResult queryProductDetailsResult
452
- ) {
453
- List<ProductDetails> productDetailsList =
454
- queryProductDetailsResult.getProductDetailsList();
455
- Log.d(TAG, "onProductDetailsResponse() called for purchase");
456
- Log.d(
457
- TAG,
458
- "Query result: " +
459
- billingResult.getResponseCode() +
460
- " - " +
461
- billingResult.getDebugMessage()
462
- );
463
- Log.d(TAG, "Product details count: " + productDetailsList.size());
464
-
465
- if (productDetailsList.isEmpty()) {
466
- Log.d(TAG, "No products found");
467
- closeBillingClient();
468
- call.reject("Product not found");
469
- return;
470
- }
471
- // Process the result
472
- List<
473
- BillingFlowParams.ProductDetailsParams
474
- > productDetailsParamsList = new ArrayList<>();
475
- for (ProductDetails productDetailsItem : productDetailsList) {
476
- Log.d(
477
- TAG,
478
- "Processing product: " + productDetailsItem.getProductId()
479
- );
480
- BillingFlowParams.ProductDetailsParams.Builder productDetailsParams =
481
- BillingFlowParams.ProductDetailsParams.newBuilder()
482
- .setProductDetails(productDetailsItem);
483
- if (productType.equals("subs")) {
484
- Log.d(TAG, "Processing subscription product");
485
- // list the SubscriptionOfferDetails and find the one who match the planIdentifier if not found get the first one
486
- ProductDetails.SubscriptionOfferDetails selectedOfferDetails =
487
- null;
488
- assert productDetailsItem.getSubscriptionOfferDetails() != null;
489
- Log.d(
490
- TAG,
491
- "Available offer details count: " +
492
- productDetailsItem.getSubscriptionOfferDetails().size()
493
- );
494
- for (ProductDetails.SubscriptionOfferDetails offerDetails : productDetailsItem.getSubscriptionOfferDetails()) {
495
- Log.d(TAG, "Checking offer: " + offerDetails.getBasePlanId());
496
- if (offerDetails.getBasePlanId().equals(planIdentifier)) {
497
- selectedOfferDetails = offerDetails;
498
- Log.d(TAG, "Found matching plan: " + planIdentifier);
499
- break;
500
- }
214
+ private void initBillingClient(PluginCall purchaseCall) {
215
+ Log.d(TAG, "initBillingClient() called");
216
+ semaphoreWait();
217
+ closeBillingClient();
218
+ semaphoreUp();
219
+ CountDownLatch semaphoreReady = new CountDownLatch(1);
220
+ Log.d(TAG, "Creating new BillingClient");
221
+ billingClient = BillingClient.newBuilder(getContext())
222
+ .setListener(
223
+ new PurchasesUpdatedListener() {
224
+ @Override
225
+ public void onPurchasesUpdated(@NonNull BillingResult billingResult, List<Purchase> purchases) {
226
+ Log.d(TAG, "onPurchasesUpdated() called");
227
+ Log.d(TAG, "Billing result: " + billingResult.getResponseCode() + " - " + billingResult.getDebugMessage());
228
+ Log.d(TAG, "Purchases count: " + (purchases != null ? purchases.size() : 0));
229
+ Log.i(NativePurchasesPlugin.TAG, "onPurchasesUpdated" + billingResult);
230
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
231
+ Log.d(TAG, "Purchase update successful, processing first purchase");
232
+ // for (Purchase purchase : purchases) {
233
+ // handlePurchase(purchase, purchaseCall);
234
+ // }
235
+ handlePurchase(purchases.get(0), purchaseCall);
236
+ } else {
237
+ // Handle any other error codes.
238
+ Log.d(TAG, "Purchase update failed or purchases is null");
239
+ Log.i(NativePurchasesPlugin.TAG, "onPurchasesUpdated" + billingResult);
240
+ if (purchaseCall != null) {
241
+ purchaseCall.reject("Purchase is not purchased");
242
+ }
243
+ }
244
+ closeBillingClient();
245
+ return;
246
+ }
501
247
  }
502
- if (selectedOfferDetails == null) {
503
- selectedOfferDetails = productDetailsItem
504
- .getSubscriptionOfferDetails()
505
- .get(0);
506
- Log.d(
507
- TAG,
508
- "Using first available offer: " +
509
- selectedOfferDetails.getBasePlanId()
510
- );
248
+ )
249
+ .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
250
+ .build();
251
+ Log.d(TAG, "Starting billing client connection");
252
+ billingClient.startConnection(
253
+ new BillingClientStateListener() {
254
+ @Override
255
+ public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
256
+ Log.d(TAG, "onBillingSetupFinished() called");
257
+ Log.d(TAG, "Setup result: " + billingResult.getResponseCode() + " - " + billingResult.getDebugMessage());
258
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
259
+ Log.d(TAG, "Billing setup successful, client is ready");
260
+ // The BillingClient is ready. You can query purchases here.
261
+ semaphoreReady.countDown();
262
+ } else {
263
+ Log.d(TAG, "Billing setup failed");
264
+ }
265
+ }
266
+
267
+ @Override
268
+ public void onBillingServiceDisconnected() {
269
+ Log.d(TAG, "onBillingServiceDisconnected() called");
270
+ // Try to restart the connection on the next request to
271
+ // Google Play by calling the startConnection() method.
511
272
  }
512
- productDetailsParams.setOfferToken(
513
- selectedOfferDetails.getOfferToken()
514
- );
515
- Log.d(
516
- TAG,
517
- "Set offer token: " + selectedOfferDetails.getOfferToken()
518
- );
519
- }
520
- productDetailsParamsList.add(productDetailsParams.build());
521
273
  }
522
- BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
523
- .setProductDetailsParamsList(productDetailsParamsList)
524
- .build();
525
- // Launch the billing flow
526
- Log.d(TAG, "Launching billing flow");
527
- BillingResult billingResult2 = billingClient.launchBillingFlow(
528
- getActivity(),
529
- billingFlowParams
530
- );
531
- Log.d(
532
- TAG,
533
- "Billing flow launch result: " +
534
- billingResult2.getResponseCode() +
535
- " - " +
536
- billingResult2.getDebugMessage()
537
- );
538
- Log.i(
539
- NativePurchasesPlugin.TAG,
540
- "onProductDetailsResponse2" + billingResult2
541
- );
542
- }
274
+ );
275
+ try {
276
+ Log.d(TAG, "Waiting for billing client setup to finish");
277
+ semaphoreReady.await();
278
+ Log.d(TAG, "Billing client setup completed");
279
+ } catch (InterruptedException e) {
280
+ Log.d(TAG, "InterruptedException while waiting for billing setup: " + e.getMessage());
281
+ e.printStackTrace();
543
282
  }
544
- );
545
- } catch (Exception e) {
546
- Log.d(TAG, "Exception during purchase: " + e.getMessage());
547
- closeBillingClient();
548
- call.reject(e.getMessage());
549
283
  }
550
- }
551
284
 
552
- private void processUnfinishedPurchases() {
553
- Log.d(TAG, "processUnfinishedPurchases() called");
554
- QueryPurchasesParams queryInAppPurchasesParams =
555
- QueryPurchasesParams.newBuilder()
556
- .setProductType(BillingClient.ProductType.INAPP)
557
- .build();
558
- Log.d(TAG, "Querying unfinished in-app purchases");
559
- billingClient.queryPurchasesAsync(
560
- queryInAppPurchasesParams,
561
- this::handlePurchases
562
- );
285
+ @PluginMethod
286
+ public void getPluginVersion(final PluginCall call) {
287
+ Log.d(TAG, "getPluginVersion() called");
288
+ try {
289
+ final JSObject ret = new JSObject();
290
+ ret.put("version", this.PLUGIN_VERSION);
291
+ Log.d(TAG, "Returning plugin version: " + this.PLUGIN_VERSION);
292
+ call.resolve(ret);
293
+ } catch (final Exception e) {
294
+ Log.d(TAG, "Error getting plugin version: " + e.getMessage());
295
+ call.reject("Could not get plugin version", e);
296
+ }
297
+ }
563
298
 
564
- QueryPurchasesParams querySubscriptionsParams =
565
- QueryPurchasesParams.newBuilder()
566
- .setProductType(BillingClient.ProductType.SUBS)
567
- .build();
568
- Log.d(TAG, "Querying unfinished subscription purchases");
569
- billingClient.queryPurchasesAsync(
570
- querySubscriptionsParams,
571
- this::handlePurchases
572
- );
573
- }
299
+ @PluginMethod
300
+ public void purchaseProduct(PluginCall call) {
301
+ Log.d(TAG, "purchaseProduct() called");
302
+ String productIdentifier = call.getString("productIdentifier");
303
+ String planIdentifier = call.getString("planIdentifier");
304
+ String productType = call.getString("productType", "inapp");
305
+ Number quantity = call.getInt("quantity", 1);
306
+ String appAccountToken = call.getString("appAccountToken");
307
+ final String accountIdentifier = appAccountToken != null && !appAccountToken.isEmpty() ? appAccountToken : null;
308
+
309
+ Log.d(TAG, "Product identifier: " + productIdentifier);
310
+ Log.d(TAG, "Plan identifier: " + planIdentifier);
311
+ Log.d(TAG, "Product type: " + productType);
312
+ Log.d(TAG, "Quantity: " + quantity);
313
+ Log.d(TAG, "Account identifier provided: " + (accountIdentifier != null ? "[REDACTED]" : "none"));
314
+
315
+ // cannot use quantity, because it's done in native modal
316
+ Log.d("CapacitorPurchases", "purchaseProduct: " + productIdentifier);
317
+ if (productIdentifier == null || productIdentifier.isEmpty()) {
318
+ // Handle error: productIdentifier is empty
319
+ Log.d(TAG, "Error: productIdentifier is empty");
320
+ call.reject("productIdentifier is empty");
321
+ return;
322
+ }
323
+ if (productType == null || productType.isEmpty()) {
324
+ // Handle error: productType is empty
325
+ Log.d(TAG, "Error: productType is empty");
326
+ call.reject("productType is empty");
327
+ return;
328
+ }
329
+ if (productType.equals("subs") && (planIdentifier == null || planIdentifier.isEmpty())) {
330
+ // Handle error: no planIdentifier with productType subs
331
+ Log.d(TAG, "Error: planIdentifier cannot be empty if productType is subs");
332
+ call.reject("planIdentifier cannot be empty if productType is subs");
333
+ return;
334
+ }
335
+ assert quantity != null;
336
+ if (quantity.intValue() < 1) {
337
+ // Handle error: quantity is less than 1
338
+ Log.d(TAG, "Error: quantity is less than 1");
339
+ call.reject("quantity is less than 1");
340
+ return;
341
+ }
574
342
 
575
- private void handlePurchases(
576
- BillingResult billingResult,
577
- List<Purchase> purchases
578
- ) {
579
- Log.d(TAG, "handlePurchases() called");
580
- Log.d(
581
- TAG,
582
- "Query purchases result: " +
583
- billingResult.getResponseCode() +
584
- " - " +
585
- billingResult.getDebugMessage()
586
- );
587
- Log.d(
588
- TAG,
589
- "Purchases count: " + (purchases != null ? purchases.size() : 0)
590
- );
343
+ // For subscriptions, always use the productIdentifier (subscription ID) to query
344
+ // The planIdentifier is used later when setting the offer token
345
+ Log.d(TAG, "Using product ID for query: " + productIdentifier);
591
346
 
592
- if (
593
- billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
594
- ) {
595
- assert purchases != null;
596
- for (Purchase purchase : purchases) {
597
- Log.d(TAG, "Processing purchase: " + purchase.getOrderId());
598
- Log.d(TAG, "Purchase state: " + purchase.getPurchaseState());
599
- if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
600
- if (purchase.isAcknowledged()) {
601
- Log.d(TAG, "Purchase already acknowledged, consuming");
602
- ConsumeParams consumeParams = ConsumeParams.newBuilder()
603
- .setPurchaseToken(purchase.getPurchaseToken())
604
- .build();
605
- billingClient.consumeAsync(consumeParams, this::onConsumeResponse);
606
- } else {
607
- Log.d(TAG, "Purchase not acknowledged, acknowledging");
608
- acknowledgePurchase(purchase.getPurchaseToken());
609
- }
347
+ ImmutableList<QueryProductDetailsParams.Product> productList = ImmutableList.of(
348
+ QueryProductDetailsParams.Product.newBuilder()
349
+ .setProductId(productIdentifier)
350
+ .setProductType(productType.equals("inapp") ? BillingClient.ProductType.INAPP : BillingClient.ProductType.SUBS)
351
+ .build()
352
+ );
353
+ QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder().setProductList(productList).build();
354
+ Log.d(TAG, "Initializing billing client for purchase");
355
+ this.initBillingClient(call);
356
+ try {
357
+ Log.d(TAG, "Querying product details for purchase");
358
+ billingClient.queryProductDetailsAsync(
359
+ params,
360
+ new ProductDetailsResponseListener() {
361
+ @Override
362
+ public void onProductDetailsResponse(
363
+ @NonNull BillingResult billingResult,
364
+ @NonNull QueryProductDetailsResult queryProductDetailsResult
365
+ ) {
366
+ List<ProductDetails> productDetailsList = queryProductDetailsResult.getProductDetailsList();
367
+ Log.d(TAG, "onProductDetailsResponse() called for purchase");
368
+ Log.d(TAG, "Query result: " + billingResult.getResponseCode() + " - " + billingResult.getDebugMessage());
369
+ Log.d(TAG, "Product details count: " + productDetailsList.size());
370
+
371
+ if (productDetailsList.isEmpty()) {
372
+ Log.d(TAG, "No products found");
373
+ closeBillingClient();
374
+ call.reject("Product not found");
375
+ return;
376
+ }
377
+ // Process the result
378
+ List<BillingFlowParams.ProductDetailsParams> productDetailsParamsList = new ArrayList<>();
379
+ for (ProductDetails productDetailsItem : productDetailsList) {
380
+ Log.d(TAG, "Processing product: " + productDetailsItem.getProductId());
381
+ BillingFlowParams.ProductDetailsParams.Builder productDetailsParams =
382
+ BillingFlowParams.ProductDetailsParams.newBuilder().setProductDetails(productDetailsItem);
383
+ if (productType.equals("subs")) {
384
+ Log.d(TAG, "Processing subscription product");
385
+ // list the SubscriptionOfferDetails and find the one who match the planIdentifier if not found get the first one
386
+ ProductDetails.SubscriptionOfferDetails selectedOfferDetails = null;
387
+ assert productDetailsItem.getSubscriptionOfferDetails() != null;
388
+ Log.d(TAG, "Available offer details count: " + productDetailsItem.getSubscriptionOfferDetails().size());
389
+ for (ProductDetails.SubscriptionOfferDetails offerDetails : productDetailsItem.getSubscriptionOfferDetails()) {
390
+ Log.d(TAG, "Checking offer: " + offerDetails.getBasePlanId());
391
+ if (offerDetails.getBasePlanId().equals(planIdentifier)) {
392
+ selectedOfferDetails = offerDetails;
393
+ Log.d(TAG, "Found matching plan: " + planIdentifier);
394
+ break;
395
+ }
396
+ }
397
+ if (selectedOfferDetails == null) {
398
+ selectedOfferDetails = productDetailsItem.getSubscriptionOfferDetails().get(0);
399
+ Log.d(TAG, "Using first available offer: " + selectedOfferDetails.getBasePlanId());
400
+ }
401
+ productDetailsParams.setOfferToken(selectedOfferDetails.getOfferToken());
402
+ Log.d(TAG, "Set offer token: " + selectedOfferDetails.getOfferToken());
403
+ }
404
+ productDetailsParamsList.add(productDetailsParams.build());
405
+ }
406
+ BillingFlowParams.Builder billingFlowBuilder = BillingFlowParams.newBuilder()
407
+ .setProductDetailsParamsList(productDetailsParamsList);
408
+ if (accountIdentifier != null && !accountIdentifier.isEmpty()) {
409
+ billingFlowBuilder.setObfuscatedAccountId(accountIdentifier);
410
+ }
411
+ BillingFlowParams billingFlowParams = billingFlowBuilder.build();
412
+ // Launch the billing flow
413
+ Log.d(TAG, "Launching billing flow");
414
+ BillingResult billingResult2 = billingClient.launchBillingFlow(getActivity(), billingFlowParams);
415
+ Log.d(
416
+ TAG,
417
+ "Billing flow launch result: " + billingResult2.getResponseCode() + " - " + billingResult2.getDebugMessage()
418
+ );
419
+ Log.i(NativePurchasesPlugin.TAG, "onProductDetailsResponse2" + billingResult2);
420
+ }
421
+ }
422
+ );
423
+ } catch (Exception e) {
424
+ Log.d(TAG, "Exception during purchase: " + e.getMessage());
425
+ closeBillingClient();
426
+ call.reject(e.getMessage());
610
427
  }
611
- }
612
- } else {
613
- Log.d(TAG, "Query purchases failed");
614
428
  }
615
- }
616
429
 
617
- private void onConsumeResponse(
618
- BillingResult billingResult,
619
- String purchaseToken
620
- ) {
621
- Log.d(TAG, "onConsumeResponse() called");
622
- Log.d(
623
- TAG,
624
- "Consume result: " +
625
- billingResult.getResponseCode() +
626
- " - " +
627
- billingResult.getDebugMessage()
628
- );
629
- Log.d(TAG, "Purchase token: " + purchaseToken);
430
+ private void processUnfinishedPurchases() {
431
+ Log.d(TAG, "processUnfinishedPurchases() called");
432
+ QueryPurchasesParams queryInAppPurchasesParams = QueryPurchasesParams.newBuilder()
433
+ .setProductType(BillingClient.ProductType.INAPP)
434
+ .build();
435
+ Log.d(TAG, "Querying unfinished in-app purchases");
436
+ billingClient.queryPurchasesAsync(queryInAppPurchasesParams, this::handlePurchases);
630
437
 
631
- if (
632
- billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
633
- ) {
634
- // Handle the success of the consume operation.
635
- // For example, you can update the UI to reflect that the item has been consumed.
636
- Log.d(TAG, "Consume operation successful");
637
- Log.i(
638
- NativePurchasesPlugin.TAG,
639
- "onConsumeResponse OK " + billingResult + purchaseToken
640
- );
641
- } else {
642
- // Handle error responses.
643
- Log.d(TAG, "Consume operation failed");
644
- Log.i(
645
- NativePurchasesPlugin.TAG,
646
- "onConsumeResponse OTHER " + billingResult + purchaseToken
647
- );
438
+ QueryPurchasesParams querySubscriptionsParams = QueryPurchasesParams.newBuilder()
439
+ .setProductType(BillingClient.ProductType.SUBS)
440
+ .build();
441
+ Log.d(TAG, "Querying unfinished subscription purchases");
442
+ billingClient.queryPurchasesAsync(querySubscriptionsParams, this::handlePurchases);
648
443
  }
649
- }
650
444
 
651
- @PluginMethod
652
- public void restorePurchases(PluginCall call) {
653
- Log.d(TAG, "restorePurchases() called");
654
- Log.d(NativePurchasesPlugin.TAG, "restorePurchases");
655
- this.initBillingClient(null);
656
- this.processUnfinishedPurchases();
657
- call.resolve();
658
- Log.d(TAG, "restorePurchases() completed");
659
- }
445
+ private void handlePurchases(BillingResult billingResult, List<Purchase> purchases) {
446
+ Log.d(TAG, "handlePurchases() called");
447
+ Log.d(TAG, "Query purchases result: " + billingResult.getResponseCode() + " - " + billingResult.getDebugMessage());
448
+ Log.d(TAG, "Purchases count: " + (purchases != null ? purchases.size() : 0));
449
+
450
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
451
+ assert purchases != null;
452
+ for (Purchase purchase : purchases) {
453
+ Log.d(TAG, "Processing purchase: " + purchase.getOrderId());
454
+ Log.d(TAG, "Purchase state: " + purchase.getPurchaseState());
455
+ if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
456
+ if (purchase.isAcknowledged()) {
457
+ Log.d(TAG, "Purchase already acknowledged, consuming");
458
+ ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
459
+ billingClient.consumeAsync(consumeParams, this::onConsumeResponse);
460
+ } else {
461
+ Log.d(TAG, "Purchase not acknowledged, acknowledging");
462
+ acknowledgePurchase(purchase.getPurchaseToken());
463
+ }
464
+ }
465
+ }
466
+ } else {
467
+ Log.d(TAG, "Query purchases failed");
468
+ }
469
+ }
660
470
 
661
- private void queryProductDetails(
662
- List<String> productIdentifiers,
663
- String productType,
664
- PluginCall call
665
- ) {
666
- Log.d(TAG, "queryProductDetails() called");
667
- Log.d(TAG, "Product identifiers count: " + productIdentifiers.size());
668
- Log.d(TAG, "Product type: " + productType);
669
- for (String id : productIdentifiers) {
670
- Log.d(TAG, "Product ID: " + id);
471
+ private void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
472
+ Log.d(TAG, "onConsumeResponse() called");
473
+ Log.d(TAG, "Consume result: " + billingResult.getResponseCode() + " - " + billingResult.getDebugMessage());
474
+ Log.d(TAG, "Purchase token: " + purchaseToken);
475
+
476
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
477
+ // Handle the success of the consume operation.
478
+ // For example, you can update the UI to reflect that the item has been consumed.
479
+ Log.d(TAG, "Consume operation successful");
480
+ Log.i(NativePurchasesPlugin.TAG, "onConsumeResponse OK " + billingResult + purchaseToken);
481
+ } else {
482
+ // Handle error responses.
483
+ Log.d(TAG, "Consume operation failed");
484
+ Log.i(NativePurchasesPlugin.TAG, "onConsumeResponse OTHER " + billingResult + purchaseToken);
485
+ }
671
486
  }
672
487
 
673
- List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
674
- for (String productIdentifier : productIdentifiers) {
675
- String productTypeForQuery = productType.equals("inapp")
676
- ? BillingClient.ProductType.INAPP
677
- : BillingClient.ProductType.SUBS;
678
- Log.d(
679
- TAG,
680
- "Creating query product: ID='" +
681
- productIdentifier +
682
- "', Type='" +
683
- productTypeForQuery +
684
- "'"
685
- );
686
- productList.add(
687
- QueryProductDetailsParams.Product.newBuilder()
688
- .setProductId(productIdentifier)
689
- .setProductType(productTypeForQuery)
690
- .build()
691
- );
488
+ @PluginMethod
489
+ public void restorePurchases(PluginCall call) {
490
+ Log.d(TAG, "restorePurchases() called");
491
+ Log.d(NativePurchasesPlugin.TAG, "restorePurchases");
492
+ this.initBillingClient(null);
493
+ this.processUnfinishedPurchases();
494
+ call.resolve();
495
+ Log.d(TAG, "restorePurchases() completed");
692
496
  }
693
- Log.d(TAG, "Total products in query list: " + productList.size());
694
- QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
695
- .setProductList(productList)
696
- .build();
697
- Log.d(TAG, "Initializing billing client for product query");
698
- this.initBillingClient(call);
699
- try {
700
- Log.d(TAG, "Querying product details");
701
- billingClient.queryProductDetailsAsync(
702
- params,
703
- new ProductDetailsResponseListener() {
704
- @Override
705
- public void onProductDetailsResponse(
706
- @NonNull BillingResult billingResult,
707
- @NonNull QueryProductDetailsResult queryProductDetailsResult
708
- ) {
709
- List<ProductDetails> productDetailsList =
710
- queryProductDetailsResult.getProductDetailsList();
711
- Log.d(TAG, "onProductDetailsResponse() called for query");
712
- Log.d(
713
- TAG,
714
- "Query result: " +
715
- billingResult.getResponseCode() +
716
- " - " +
717
- billingResult.getDebugMessage()
718
- );
719
- Log.d(TAG, "Product details count: " + productDetailsList.size());
720
497
 
721
- if (productDetailsList.isEmpty()) {
722
- Log.d(TAG, "No products found in query");
723
- Log.d(TAG, "This usually means:");
724
- Log.d(TAG, "1. Product doesn't exist in Google Play Console");
725
- Log.d(TAG, "2. Product is not published/active");
726
- Log.d(
727
- TAG,
728
- "3. App is not properly configured for the product type"
729
- );
730
- Log.d(TAG, "4. Wrong product ID or type");
731
- closeBillingClient();
732
- call.reject("Product not found");
733
- return;
734
- }
735
- JSONArray products = new JSONArray();
736
- for (ProductDetails productDetails : productDetailsList) {
737
- Log.d(
738
- TAG,
739
- "Processing product details: " + productDetails.getProductId()
740
- );
741
- JSObject product = new JSObject();
742
- product.put("title", productDetails.getName());
743
- product.put("description", productDetails.getDescription());
744
- Log.d(TAG, "Product title: " + productDetails.getName());
745
- Log.d(
746
- TAG,
747
- "Product description: " + productDetails.getDescription()
748
- );
498
+ private void queryProductDetails(List<String> productIdentifiers, String productType, PluginCall call) {
499
+ Log.d(TAG, "queryProductDetails() called");
500
+ Log.d(TAG, "Product identifiers count: " + productIdentifiers.size());
501
+ Log.d(TAG, "Product type: " + productType);
502
+ for (String id : productIdentifiers) {
503
+ Log.d(TAG, "Product ID: " + id);
504
+ }
749
505
 
750
- if (productType.equals("inapp")) {
751
- Log.d(TAG, "Processing as in-app product");
752
- product.put("identifier", productDetails.getProductId());
753
- double price =
754
- Objects.requireNonNull(
755
- productDetails.getOneTimePurchaseOfferDetails()
756
- ).getPriceAmountMicros() /
757
- 1000000.0;
758
- product.put("price", price);
759
- product.put(
760
- "priceString",
761
- productDetails
762
- .getOneTimePurchaseOfferDetails()
763
- .getFormattedPrice()
764
- );
765
- product.put(
766
- "currencyCode",
767
- productDetails
768
- .getOneTimePurchaseOfferDetails()
769
- .getPriceCurrencyCode()
770
- );
771
- Log.d(TAG, "Price: " + price);
772
- Log.d(
773
- TAG,
774
- "Formatted price: " +
775
- productDetails
776
- .getOneTimePurchaseOfferDetails()
777
- .getFormattedPrice()
778
- );
779
- Log.d(
780
- TAG,
781
- "Currency: " +
782
- productDetails
783
- .getOneTimePurchaseOfferDetails()
784
- .getPriceCurrencyCode()
785
- );
786
- } else {
787
- Log.d(TAG, "Processing as subscription product");
788
- ProductDetails.SubscriptionOfferDetails selectedOfferDetails =
789
- productDetails.getSubscriptionOfferDetails().get(0);
790
- product.put("planIdentifier", productDetails.getProductId());
791
- product.put("identifier", selectedOfferDetails.getBasePlanId());
792
- double price =
793
- selectedOfferDetails
794
- .getPricingPhases()
795
- .getPricingPhaseList()
796
- .get(0)
797
- .getPriceAmountMicros() /
798
- 1000000.0;
799
- product.put("price", price);
800
- product.put(
801
- "priceString",
802
- selectedOfferDetails
803
- .getPricingPhases()
804
- .getPricingPhaseList()
805
- .get(0)
806
- .getFormattedPrice()
807
- );
808
- product.put(
809
- "currencyCode",
810
- selectedOfferDetails
811
- .getPricingPhases()
812
- .getPricingPhaseList()
813
- .get(0)
814
- .getPriceCurrencyCode()
815
- );
816
- Log.d(TAG, "Plan identifier: " + productDetails.getProductId());
817
- Log.d(
818
- TAG,
819
- "Base plan ID: " + selectedOfferDetails.getBasePlanId()
820
- );
821
- Log.d(TAG, "Price: " + price);
822
- Log.d(
823
- TAG,
824
- "Formatted price: " +
825
- selectedOfferDetails
826
- .getPricingPhases()
827
- .getPricingPhaseList()
828
- .get(0)
829
- .getFormattedPrice()
830
- );
831
- Log.d(
832
- TAG,
833
- "Currency: " +
834
- selectedOfferDetails
835
- .getPricingPhases()
836
- .getPricingPhaseList()
837
- .get(0)
838
- .getPriceCurrencyCode()
839
- );
840
- }
841
- product.put("isFamilyShareable", false);
842
- products.put(product);
843
- }
844
- JSObject ret = new JSObject();
845
- ret.put("products", products);
846
- Log.d(TAG, "Returning " + products.length() + " products");
506
+ List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
507
+ for (String productIdentifier : productIdentifiers) {
508
+ String productTypeForQuery = productType.equals("inapp") ? BillingClient.ProductType.INAPP : BillingClient.ProductType.SUBS;
509
+ Log.d(TAG, "Creating query product: ID='" + productIdentifier + "', Type='" + productTypeForQuery + "'");
510
+ productList.add(
511
+ QueryProductDetailsParams.Product.newBuilder().setProductId(productIdentifier).setProductType(productTypeForQuery).build()
512
+ );
513
+ }
514
+ Log.d(TAG, "Total products in query list: " + productList.size());
515
+ QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder().setProductList(productList).build();
516
+ Log.d(TAG, "Initializing billing client for product query");
517
+ this.initBillingClient(call);
518
+ try {
519
+ Log.d(TAG, "Querying product details");
520
+ billingClient.queryProductDetailsAsync(
521
+ params,
522
+ new ProductDetailsResponseListener() {
523
+ @Override
524
+ public void onProductDetailsResponse(
525
+ @NonNull BillingResult billingResult,
526
+ @NonNull QueryProductDetailsResult queryProductDetailsResult
527
+ ) {
528
+ List<ProductDetails> productDetailsList = queryProductDetailsResult.getProductDetailsList();
529
+ Log.d(TAG, "onProductDetailsResponse() called for query");
530
+ Log.d(TAG, "Query result: " + billingResult.getResponseCode() + " - " + billingResult.getDebugMessage());
531
+ Log.d(TAG, "Product details count: " + productDetailsList.size());
532
+
533
+ if (productDetailsList.isEmpty()) {
534
+ Log.d(TAG, "No products found in query");
535
+ Log.d(TAG, "This usually means:");
536
+ Log.d(TAG, "1. Product doesn't exist in Google Play Console");
537
+ Log.d(TAG, "2. Product is not published/active");
538
+ Log.d(TAG, "3. App is not properly configured for the product type");
539
+ Log.d(TAG, "4. Wrong product ID or type");
540
+ closeBillingClient();
541
+ call.reject("Product not found");
542
+ return;
543
+ }
544
+ JSONArray products = new JSONArray();
545
+ for (ProductDetails productDetails : productDetailsList) {
546
+ Log.d(TAG, "Processing product details: " + productDetails.getProductId());
547
+ JSObject product = new JSObject();
548
+ product.put("title", productDetails.getName());
549
+ product.put("description", productDetails.getDescription());
550
+ Log.d(TAG, "Product title: " + productDetails.getName());
551
+ Log.d(TAG, "Product description: " + productDetails.getDescription());
552
+
553
+ if (productType.equals("inapp")) {
554
+ Log.d(TAG, "Processing as in-app product");
555
+ product.put("identifier", productDetails.getProductId());
556
+ double price =
557
+ Objects.requireNonNull(productDetails.getOneTimePurchaseOfferDetails()).getPriceAmountMicros() /
558
+ 1000000.0;
559
+ product.put("price", price);
560
+ product.put("priceString", productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
561
+ product.put("currencyCode", productDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
562
+ Log.d(TAG, "Price: " + price);
563
+ Log.d(TAG, "Formatted price: " + productDetails.getOneTimePurchaseOfferDetails().getFormattedPrice());
564
+ Log.d(TAG, "Currency: " + productDetails.getOneTimePurchaseOfferDetails().getPriceCurrencyCode());
565
+ } else {
566
+ Log.d(TAG, "Processing as subscription product");
567
+ ProductDetails.SubscriptionOfferDetails selectedOfferDetails = productDetails
568
+ .getSubscriptionOfferDetails()
569
+ .get(0);
570
+ product.put("planIdentifier", productDetails.getProductId());
571
+ product.put("identifier", selectedOfferDetails.getBasePlanId());
572
+ double price =
573
+ selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceAmountMicros() / 1000000.0;
574
+ product.put("price", price);
575
+ product.put(
576
+ "priceString",
577
+ selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
578
+ );
579
+ product.put(
580
+ "currencyCode",
581
+ selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
582
+ );
583
+ Log.d(TAG, "Plan identifier: " + productDetails.getProductId());
584
+ Log.d(TAG, "Base plan ID: " + selectedOfferDetails.getBasePlanId());
585
+ Log.d(TAG, "Price: " + price);
586
+ Log.d(
587
+ TAG,
588
+ "Formatted price: " +
589
+ selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice()
590
+ );
591
+ Log.d(
592
+ TAG,
593
+ "Currency: " +
594
+ selectedOfferDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode()
595
+ );
596
+ }
597
+ product.put("isFamilyShareable", false);
598
+ products.put(product);
599
+ }
600
+ JSObject ret = new JSObject();
601
+ ret.put("products", products);
602
+ Log.d(TAG, "Returning " + products.length() + " products");
603
+ closeBillingClient();
604
+ call.resolve(ret);
605
+ }
606
+ }
607
+ );
608
+ } catch (Exception e) {
609
+ Log.d(TAG, "Exception during product query: " + e.getMessage());
847
610
  closeBillingClient();
848
- call.resolve(ret);
849
- }
611
+ call.reject(e.getMessage());
850
612
  }
851
- );
852
- } catch (Exception e) {
853
- Log.d(TAG, "Exception during product query: " + e.getMessage());
854
- closeBillingClient();
855
- call.reject(e.getMessage());
856
613
  }
857
- }
858
614
 
859
- @PluginMethod
860
- public void getProducts(PluginCall call) {
861
- Log.d(TAG, "getProducts() called");
862
- JSONArray productIdentifiersArray = call.getArray("productIdentifiers");
863
- String productType = call.getString("productType", "inapp");
864
- Log.d(TAG, "Product type: " + productType);
865
- Log.d(TAG, "Raw productIdentifiersArray: " + productIdentifiersArray);
866
- Log.d(
867
- TAG,
868
- "productIdentifiersArray length: " +
869
- (productIdentifiersArray != null
870
- ? productIdentifiersArray.length()
871
- : "null")
872
- );
873
-
874
- if (
875
- productIdentifiersArray == null || productIdentifiersArray.length() == 0
876
- ) {
877
- Log.d(TAG, "Error: productIdentifiers array missing or empty");
878
- call.reject("productIdentifiers array missing");
879
- return;
880
- }
615
+ @PluginMethod
616
+ public void getProducts(PluginCall call) {
617
+ Log.d(TAG, "getProducts() called");
618
+ JSONArray productIdentifiersArray = call.getArray("productIdentifiers");
619
+ String productType = call.getString("productType", "inapp");
620
+ Log.d(TAG, "Product type: " + productType);
621
+ Log.d(TAG, "Raw productIdentifiersArray: " + productIdentifiersArray);
622
+ Log.d(TAG, "productIdentifiersArray length: " + (productIdentifiersArray != null ? productIdentifiersArray.length() : "null"));
623
+
624
+ if (productIdentifiersArray == null || productIdentifiersArray.length() == 0) {
625
+ Log.d(TAG, "Error: productIdentifiers array missing or empty");
626
+ call.reject("productIdentifiers array missing");
627
+ return;
628
+ }
881
629
 
882
- List<String> productIdentifiers = new ArrayList<>();
883
- for (int i = 0; i < productIdentifiersArray.length(); i++) {
884
- String productId = productIdentifiersArray.optString(i, "");
885
- Log.d(TAG, "Array index " + i + ": '" + productId + "'");
886
- productIdentifiers.add(productId);
887
- Log.d(TAG, "Added product identifier: " + productId);
630
+ List<String> productIdentifiers = new ArrayList<>();
631
+ for (int i = 0; i < productIdentifiersArray.length(); i++) {
632
+ String productId = productIdentifiersArray.optString(i, "");
633
+ Log.d(TAG, "Array index " + i + ": '" + productId + "'");
634
+ productIdentifiers.add(productId);
635
+ Log.d(TAG, "Added product identifier: " + productId);
636
+ }
637
+ Log.d(TAG, "Final productIdentifiers list: " + productIdentifiers.toString());
638
+ queryProductDetails(productIdentifiers, productType, call);
888
639
  }
889
- Log.d(
890
- TAG,
891
- "Final productIdentifiers list: " + productIdentifiers.toString()
892
- );
893
- queryProductDetails(productIdentifiers, productType, call);
894
- }
895
640
 
896
- @PluginMethod
897
- public void getProduct(PluginCall call) {
898
- Log.d(TAG, "getProduct() called");
899
- String productIdentifier = call.getString("productIdentifier");
900
- String productType = call.getString("productType", "inapp");
901
- Log.d(TAG, "Product identifier: " + productIdentifier);
902
- Log.d(TAG, "Product type: " + productType);
903
-
904
- assert productIdentifier != null;
905
- if (productIdentifier.isEmpty()) {
906
- Log.d(TAG, "Error: productIdentifier is empty");
907
- call.reject("productIdentifier is empty");
908
- return;
641
+ @PluginMethod
642
+ public void getProduct(PluginCall call) {
643
+ Log.d(TAG, "getProduct() called");
644
+ String productIdentifier = call.getString("productIdentifier");
645
+ String productType = call.getString("productType", "inapp");
646
+ Log.d(TAG, "Product identifier: " + productIdentifier);
647
+ Log.d(TAG, "Product type: " + productType);
648
+
649
+ assert productIdentifier != null;
650
+ if (productIdentifier.isEmpty()) {
651
+ Log.d(TAG, "Error: productIdentifier is empty");
652
+ call.reject("productIdentifier is empty");
653
+ return;
654
+ }
655
+ queryProductDetails(Collections.singletonList(productIdentifier), productType, call);
909
656
  }
910
- queryProductDetails(
911
- Collections.singletonList(productIdentifier),
912
- productType,
913
- call
914
- );
915
- }
916
-
917
- @PluginMethod
918
- public void getPurchases(PluginCall call) {
919
- Log.d(TAG, "getPurchases() called");
920
- String productType = call.getString("productType");
921
- Log.d(TAG, "Product type filter: " + productType);
922
-
923
- this.initBillingClient(null);
924
-
925
- JSONArray allPurchases = new JSONArray();
926
-
927
- try {
928
- // Query in-app purchases if no filter or if filter is "inapp"
929
- if (productType == null || productType.equals("inapp")) {
930
- Log.d(TAG, "Querying in-app purchases");
931
- QueryPurchasesParams queryInAppParams =
932
- QueryPurchasesParams.newBuilder()
933
- .setProductType(BillingClient.ProductType.INAPP)
934
- .build();
935
-
936
- billingClient.queryPurchasesAsync(
937
- queryInAppParams,
938
- (billingResult, purchases) -> {
939
- Log.d(
940
- TAG,
941
- "In-app purchases query result: " +
942
- billingResult.getResponseCode()
943
- );
944
- if (
945
- billingResult.getResponseCode() ==
946
- BillingClient.BillingResponseCode.OK
947
- ) {
948
- for (Purchase purchase : purchases) {
949
- Log.d(
950
- TAG,
951
- "Processing in-app purchase: " + purchase.getOrderId()
952
- );
953
- JSObject purchaseData = new JSObject();
954
- purchaseData.put("transactionId", purchase.getPurchaseToken());
955
- purchaseData.put(
956
- "productIdentifier",
957
- purchase.getProducts().get(0)
958
- );
959
- purchaseData.put(
960
- "purchaseDate",
961
- new java.text.SimpleDateFormat(
962
- "yyyy-MM-dd'T'HH:mm:ss'Z'",
963
- java.util.Locale.US
964
- ).format(new java.util.Date(purchase.getPurchaseTime()))
965
- );
966
- purchaseData.put("quantity", purchase.getQuantity());
967
- purchaseData.put("productType", "inapp");
968
- purchaseData.put("orderId", purchase.getOrderId());
969
- purchaseData.put("purchaseToken", purchase.getPurchaseToken());
970
- purchaseData.put("isAcknowledged", purchase.isAcknowledged());
971
- purchaseData.put(
972
- "purchaseState",
973
- String.valueOf(purchase.getPurchaseState())
974
- );
975
- // Add cancellation information - ALWAYS set willCancel
976
- // Note: Android doesn't provide direct cancellation information in the Purchase object
977
- purchaseData.put("willCancel", null); // Default to null, would need API call to determine actual cancellation date
978
- allPurchases.put(purchaseData);
979
- }
980
- }
981
-
982
- // Query subscriptions if no filter or if filter is "subs"
983
- assert productType != null;
984
- // Only querying in-app, return result now
985
- JSObject result = new JSObject();
986
- result.put("purchases", allPurchases);
987
- Log.d(
988
- TAG,
989
- "Returning " + allPurchases.length() + " in-app purchases"
990
- );
991
- closeBillingClient();
992
- call.resolve(result);
993
- }
994
- );
995
- } else if (productType.equals("subs")) {
996
- // Only query subscriptions
997
- Log.d(TAG, "Querying only subscription purchases");
998
- QueryPurchasesParams querySubsParams = QueryPurchasesParams.newBuilder()
999
- .setProductType(BillingClient.ProductType.SUBS)
1000
- .build();
1001
657
 
1002
- billingClient.queryPurchasesAsync(
1003
- querySubsParams,
1004
- (billingResult, purchases) -> {
1005
- Log.d(
1006
- TAG,
1007
- "Subscription purchases query result: " +
1008
- billingResult.getResponseCode()
1009
- );
1010
- if (
1011
- billingResult.getResponseCode() ==
1012
- BillingClient.BillingResponseCode.OK
1013
- ) {
1014
- for (Purchase purchase : purchases) {
1015
- Log.d(
1016
- TAG,
1017
- "Processing subscription purchase: " + purchase.getOrderId()
1018
- );
1019
- JSObject purchaseData = new JSObject();
1020
- purchaseData.put("transactionId", purchase.getPurchaseToken());
1021
- purchaseData.put(
1022
- "productIdentifier",
1023
- purchase.getProducts().get(0)
1024
- );
1025
- purchaseData.put(
1026
- "purchaseDate",
1027
- new java.text.SimpleDateFormat(
1028
- "yyyy-MM-dd'T'HH:mm:ss'Z'",
1029
- java.util.Locale.US
1030
- ).format(new java.util.Date(purchase.getPurchaseTime()))
1031
- );
1032
- purchaseData.put("quantity", purchase.getQuantity());
1033
- purchaseData.put("productType", "subs");
1034
- purchaseData.put("orderId", purchase.getOrderId());
1035
- purchaseData.put("purchaseToken", purchase.getPurchaseToken());
1036
- purchaseData.put("isAcknowledged", purchase.isAcknowledged());
1037
- purchaseData.put(
1038
- "purchaseState",
1039
- String.valueOf(purchase.getPurchaseState())
1040
- );
1041
- // Add cancellation information - ALWAYS set willCancel
1042
- // Note: Android doesn't provide direct cancellation information in the Purchase object
1043
- purchaseData.put("willCancel", null); // Default to null, would need API call to determine actual cancellation date
1044
- allPurchases.put(purchaseData);
1045
- }
658
+ @PluginMethod
659
+ public void getPurchases(PluginCall call) {
660
+ Log.d(TAG, "getPurchases() called");
661
+ String productType = call.getString("productType");
662
+ Log.d(TAG, "Product type filter: " + productType);
663
+ String appAccountToken = call.getString("appAccountToken");
664
+ final String accountFilter = appAccountToken != null && !appAccountToken.isEmpty() ? appAccountToken : null;
665
+ final boolean hasAccountFilter = accountFilter != null && !accountFilter.isEmpty();
666
+ Log.d(TAG, "Account filter provided: " + (hasAccountFilter ? "[REDACTED]" : "none"));
667
+
668
+ this.initBillingClient(null);
669
+
670
+ JSONArray allPurchases = new JSONArray();
671
+
672
+ try {
673
+ // Query in-app purchases if no filter or if filter is "inapp"
674
+ if (productType == null || productType.equals("inapp")) {
675
+ Log.d(TAG, "Querying in-app purchases");
676
+ QueryPurchasesParams queryInAppParams = QueryPurchasesParams.newBuilder()
677
+ .setProductType(BillingClient.ProductType.INAPP)
678
+ .build();
679
+
680
+ billingClient.queryPurchasesAsync(queryInAppParams, (billingResult, purchases) -> {
681
+ Log.d(TAG, "In-app purchases query result: " + billingResult.getResponseCode());
682
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
683
+ for (Purchase purchase : purchases) {
684
+ Log.d(TAG, "Processing in-app purchase: " + purchase.getOrderId());
685
+ AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
686
+ String purchaseAccountId = accountIdentifiers != null ? accountIdentifiers.getObfuscatedAccountId() : null;
687
+ if (hasAccountFilter) {
688
+ if (purchaseAccountId == null || !purchaseAccountId.equals(accountFilter)) {
689
+ Log.d(TAG, "Skipping in-app purchase due to account filter mismatch");
690
+ continue;
691
+ }
692
+ }
693
+ JSObject purchaseData = new JSObject();
694
+ purchaseData.put("transactionId", purchase.getPurchaseToken());
695
+ purchaseData.put("productIdentifier", purchase.getProducts().get(0));
696
+ purchaseData.put(
697
+ "purchaseDate",
698
+ new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.US).format(
699
+ new java.util.Date(purchase.getPurchaseTime())
700
+ )
701
+ );
702
+ purchaseData.put("quantity", purchase.getQuantity());
703
+ purchaseData.put("productType", "inapp");
704
+ purchaseData.put("orderId", purchase.getOrderId());
705
+ purchaseData.put("purchaseToken", purchase.getPurchaseToken());
706
+ purchaseData.put("isAcknowledged", purchase.isAcknowledged());
707
+ purchaseData.put("purchaseState", String.valueOf(purchase.getPurchaseState()));
708
+ purchaseData.put("appAccountToken", purchaseAccountId);
709
+ // Add cancellation information - ALWAYS set willCancel
710
+ // Note: Android doesn't provide direct cancellation information in the Purchase object
711
+ purchaseData.put("willCancel", null); // Default to null, would need API call to determine actual cancellation date
712
+ allPurchases.put(purchaseData);
713
+ }
714
+ }
715
+
716
+ // Query subscriptions if no filter or if filter is "subs"
717
+ assert productType != null;
718
+ // Only querying in-app, return result now
719
+ JSObject result = new JSObject();
720
+ result.put("purchases", allPurchases);
721
+ Log.d(TAG, "Returning " + allPurchases.length() + " in-app purchases");
722
+ closeBillingClient();
723
+ call.resolve(result);
724
+ });
725
+ } else if (productType.equals("subs")) {
726
+ // Only query subscriptions
727
+ Log.d(TAG, "Querying only subscription purchases");
728
+ QueryPurchasesParams querySubsParams = QueryPurchasesParams.newBuilder()
729
+ .setProductType(BillingClient.ProductType.SUBS)
730
+ .build();
731
+
732
+ billingClient.queryPurchasesAsync(querySubsParams, (billingResult, purchases) -> {
733
+ Log.d(TAG, "Subscription purchases query result: " + billingResult.getResponseCode());
734
+ if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
735
+ for (Purchase purchase : purchases) {
736
+ Log.d(TAG, "Processing subscription purchase: " + purchase.getOrderId());
737
+ AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
738
+ String purchaseAccountId = accountIdentifiers != null ? accountIdentifiers.getObfuscatedAccountId() : null;
739
+ if (hasAccountFilter) {
740
+ if (purchaseAccountId == null || !purchaseAccountId.equals(accountFilter)) {
741
+ Log.d(TAG, "Skipping subscription purchase due to account filter mismatch");
742
+ continue;
743
+ }
744
+ }
745
+ JSObject purchaseData = new JSObject();
746
+ purchaseData.put("transactionId", purchase.getPurchaseToken());
747
+ purchaseData.put("productIdentifier", purchase.getProducts().get(0));
748
+ purchaseData.put(
749
+ "purchaseDate",
750
+ new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.US).format(
751
+ new java.util.Date(purchase.getPurchaseTime())
752
+ )
753
+ );
754
+ purchaseData.put("quantity", purchase.getQuantity());
755
+ purchaseData.put("productType", "subs");
756
+ purchaseData.put("orderId", purchase.getOrderId());
757
+ purchaseData.put("purchaseToken", purchase.getPurchaseToken());
758
+ purchaseData.put("isAcknowledged", purchase.isAcknowledged());
759
+ purchaseData.put("purchaseState", String.valueOf(purchase.getPurchaseState()));
760
+ purchaseData.put("appAccountToken", purchaseAccountId);
761
+ // Add cancellation information - ALWAYS set willCancel
762
+ // Note: Android doesn't provide direct cancellation information in the Purchase object
763
+ purchaseData.put("willCancel", null); // Default to null, would need API call to determine actual cancellation date
764
+ allPurchases.put(purchaseData);
765
+ }
766
+ }
767
+
768
+ JSObject result = new JSObject();
769
+ result.put("purchases", allPurchases);
770
+ Log.d(TAG, "Returning " + allPurchases.length() + " subscription purchases");
771
+ closeBillingClient();
772
+ call.resolve(result);
773
+ });
1046
774
  }
1047
-
1048
- JSObject result = new JSObject();
1049
- result.put("purchases", allPurchases);
1050
- Log.d(
1051
- TAG,
1052
- "Returning " + allPurchases.length() + " subscription purchases"
1053
- );
775
+ } catch (Exception e) {
776
+ Log.d(TAG, "Exception during getPurchases: " + e.getMessage());
1054
777
  closeBillingClient();
1055
- call.resolve(result);
1056
- }
1057
- );
1058
- }
1059
- } catch (Exception e) {
1060
- Log.d(TAG, "Exception during getPurchases: " + e.getMessage());
1061
- closeBillingClient();
1062
- call.reject(e.getMessage());
778
+ call.reject(e.getMessage());
779
+ }
1063
780
  }
1064
- }
1065
781
  }