@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.
- package/README.md +11 -10
- package/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java +690 -974
- package/dist/docs.json +18 -7
- package/dist/esm/definitions.d.ts +11 -5
- package/dist/esm/definitions.js +1 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +4 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +2 -2
- package/dist/esm/web.js +10 -10
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +11 -11
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +11 -11
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativePurchasesPlugin/NativePurchasesPlugin.swift +61 -44
- package/package.json +4 -4
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
187
|
-
TAG,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
Log.d(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
Log.d(TAG, "
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
.
|
|
567
|
-
.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
"
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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.
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|