capacitor-native-purchases 0.2.1 → 0.3.1
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.
|
@@ -3,14 +3,17 @@ package jok.purchases.capacitor;
|
|
|
3
3
|
import android.app.Activity;
|
|
4
4
|
import android.util.Log;
|
|
5
5
|
|
|
6
|
+
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
|
6
7
|
import com.android.billingclient.api.BillingClient;
|
|
7
|
-
|
|
8
8
|
import com.android.billingclient.api.BillingClientStateListener;
|
|
9
|
+
import com.android.billingclient.api.ConsumeParams;
|
|
9
10
|
import com.android.billingclient.api.BillingFlowParams;
|
|
10
11
|
import com.android.billingclient.api.BillingResult;
|
|
12
|
+
import com.android.billingclient.api.PendingPurchasesParams;
|
|
11
13
|
import com.android.billingclient.api.ProductDetails;
|
|
12
14
|
import com.android.billingclient.api.Purchase;
|
|
13
15
|
import com.android.billingclient.api.PurchaseHistoryRecord;
|
|
16
|
+
import com.android.billingclient.api.PurchasesUpdatedListener;
|
|
14
17
|
import com.android.billingclient.api.QueryProductDetailsParams;
|
|
15
18
|
import com.android.billingclient.api.QueryPurchaseHistoryParams;
|
|
16
19
|
import com.android.billingclient.api.QueryPurchasesParams;
|
|
@@ -20,8 +23,7 @@ import com.getcapacitor.PluginCall;
|
|
|
20
23
|
|
|
21
24
|
import android.content.Context;
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
import androidx.annotation.NonNull;
|
|
26
|
+
import androidx.appcompat.app.AlertDialog;
|
|
25
27
|
|
|
26
28
|
import java.io.BufferedReader;
|
|
27
29
|
import java.io.InputStreamReader;
|
|
@@ -31,370 +33,718 @@ import java.nio.charset.StandardCharsets;
|
|
|
31
33
|
import java.text.SimpleDateFormat;
|
|
32
34
|
import java.util.ArrayList;
|
|
33
35
|
import java.util.Calendar;
|
|
36
|
+
import java.util.HashMap;
|
|
34
37
|
import java.util.List;
|
|
35
38
|
import java.util.Locale;
|
|
39
|
+
import java.util.Map;
|
|
36
40
|
import java.util.Objects;
|
|
37
41
|
|
|
38
42
|
public class Purchases {
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
private final Activity activity;
|
|
45
|
+
public Context context;
|
|
46
|
+
private BillingClient billingClient;
|
|
47
|
+
private int billingClientIsConnected = 0;
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
// Reconnection configuration
|
|
50
|
+
private static final int MAX_RECONNECT_ATTEMPTS = 5;
|
|
51
|
+
private static final long RECONNECT_BASE_DELAY_MS = 1000; // 1 second
|
|
52
|
+
private int reconnectAttempts = 0;
|
|
53
|
+
private final android.os.Handler reconnectHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
private String googleVerifyEndpoint = "";
|
|
56
|
+
private String googleBid = "";
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@Override
|
|
53
|
-
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
|
|
54
|
-
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
|
55
|
-
billingClientIsConnected = 1;
|
|
56
|
-
} else {
|
|
57
|
-
billingClientIsConnected = billingResult.getResponseCode();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
58
|
+
// Cache to store product types (SUBS or INAPP) by productIdentifier
|
|
59
|
+
private final Map<String, String> productTypeCache = new HashMap<>();
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Try to restart the connection on the next request to
|
|
64
|
-
// Google Play by calling the startConnection() method.
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
this.activity = plugin.getActivity();
|
|
68
|
-
this.context = plugin.getContext();
|
|
61
|
+
// Store pending purchase call to resolve after acknowledgement
|
|
62
|
+
private PluginCall pendingPurchaseCall = null;
|
|
69
63
|
|
|
70
|
-
}
|
|
71
64
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
// This listener is fired upon completing the billing flow, it is vital to call the acknowledgePurchase
|
|
66
|
+
// method on the billingClient for SUBS, or consumeAsync for INAPP, otherwise Google will automatically
|
|
67
|
+
// cancel the subscription or refund the purchase shortly after
|
|
68
|
+
private final PurchasesUpdatedListener purchasesUpdatedListener = (billingResult, purchases) -> {
|
|
69
|
+
JSObject response = new JSObject();
|
|
70
|
+
try {
|
|
71
|
+
if (purchases != null && billingResult.getResponseCode() == 0) {
|
|
72
|
+
for (int i = 0; i < purchases.size(); i++) {
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.googleBid = bid;
|
|
74
|
+
Purchase currentPurchase = purchases.get(i);
|
|
75
|
+
String purchaseToken = currentPurchase.getPurchaseToken();
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
// Query INAPP purchases to check if this transaction is there
|
|
78
|
+
QueryPurchasesParams inAppQueryParams = QueryPurchasesParams.newBuilder()
|
|
79
|
+
.setProductType(BillingClient.ProductType.INAPP)
|
|
80
|
+
.build();
|
|
83
81
|
|
|
84
|
-
|
|
82
|
+
billingClient.queryPurchasesAsync(inAppQueryParams, (inAppResult, inAppPurchases) -> {
|
|
83
|
+
boolean foundInInApp = false;
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
if (inAppPurchases != null) {
|
|
86
|
+
for (Purchase p : inAppPurchases) {
|
|
87
|
+
if (p.getPurchaseToken().equals(purchaseToken)) {
|
|
88
|
+
foundInInApp = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
if (foundInInApp) {
|
|
95
|
+
// Found in INAPP, use consumeAsync
|
|
96
|
+
ConsumeParams consumeParams = ConsumeParams.newBuilder()
|
|
97
|
+
.setPurchaseToken(purchaseToken)
|
|
98
|
+
.build();
|
|
99
|
+
|
|
100
|
+
billingClient.consumeAsync(consumeParams, (consumeResult, consumedToken) -> {
|
|
101
|
+
Log.i("Purchase consumed", currentPurchase.getOriginalJson());
|
|
102
|
+
|
|
103
|
+
if (pendingPurchaseCall != null) {
|
|
104
|
+
response.put("responseCode", consumeResult.getResponseCode());
|
|
105
|
+
if (consumeResult.getResponseCode() == 0) {
|
|
106
|
+
response.put("responseMessage", "Purchase consumed successfully");
|
|
107
|
+
} else {
|
|
108
|
+
response.put("responseMessage", "Failed to consume purchase: " + consumeResult.getDebugMessage());
|
|
109
|
+
}
|
|
110
|
+
pendingPurchaseCall.resolve(response);
|
|
111
|
+
pendingPurchaseCall = null;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
// Not found in INAPP, query SUBS purchases
|
|
116
|
+
QueryPurchasesParams subsQueryParams = QueryPurchasesParams.newBuilder()
|
|
117
|
+
.setProductType(BillingClient.ProductType.SUBS)
|
|
118
|
+
.build();
|
|
119
|
+
|
|
120
|
+
billingClient.queryPurchasesAsync(subsQueryParams, (subsResult, subsPurchases) -> {
|
|
121
|
+
boolean foundInSubs = false;
|
|
122
|
+
|
|
123
|
+
if (subsPurchases != null) {
|
|
124
|
+
for (Purchase p : subsPurchases) {
|
|
125
|
+
if (p.getPurchaseToken().equals(purchaseToken)) {
|
|
126
|
+
foundInSubs = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
89
131
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
if (foundInSubs) {
|
|
133
|
+
// Found in SUBS, use acknowledgePurchase
|
|
134
|
+
AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
|
|
135
|
+
.setPurchaseToken(purchaseToken)
|
|
93
136
|
.build();
|
|
94
137
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
138
|
+
billingClient.acknowledgePurchase(acknowledgePurchaseParams, ackResult -> {
|
|
139
|
+
Log.i("Purchase ack", currentPurchase.getOriginalJson());
|
|
140
|
+
|
|
141
|
+
if (pendingPurchaseCall != null) {
|
|
142
|
+
response.put("responseCode", ackResult.getResponseCode());
|
|
143
|
+
if (ackResult.getResponseCode() == 0) {
|
|
144
|
+
response.put("responseMessage", "Purchase acknowledged successfully");
|
|
145
|
+
} else {
|
|
146
|
+
response.put("responseMessage", "Failed to acknowledge purchase: " + ackResult.getDebugMessage());
|
|
147
|
+
}
|
|
148
|
+
pendingPurchaseCall.resolve(response);
|
|
149
|
+
pendingPurchaseCall = null;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
// Not found in either INAPP or SUBS
|
|
154
|
+
Log.w("PurchasesUpdatedListener", "Purchase not found in INAPP or SUBS queries");
|
|
155
|
+
if (pendingPurchaseCall != null) {
|
|
156
|
+
response.put("responseCode", -1);
|
|
157
|
+
response.put("responseMessage", "Purchase not found in active purchases");
|
|
158
|
+
pendingPurchaseCall.resolve(response);
|
|
159
|
+
pendingPurchaseCall = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Purchase failed or was cancelled
|
|
168
|
+
if (pendingPurchaseCall != null) {
|
|
169
|
+
response.put("responseCode", billingResult.getResponseCode());
|
|
170
|
+
response.put("responseMessage", billingResult.getDebugMessage());
|
|
171
|
+
pendingPurchaseCall.resolve(response);
|
|
172
|
+
pendingPurchaseCall = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
99
175
|
|
|
100
|
-
billingClient.queryProductDetailsAsync(
|
|
101
|
-
queryProductDetailsParams,
|
|
102
|
-
(billingResult, productDetailsList) -> {
|
|
103
176
|
|
|
104
|
-
|
|
177
|
+
// if (pendingPurchaseCall != null) {
|
|
178
|
+
// response.put("responseCode", -2);
|
|
179
|
+
// response.put("responseMessage", "Not processed");
|
|
180
|
+
// pendingPurchaseCall.resolve(response);
|
|
181
|
+
// pendingPurchaseCall = null;
|
|
182
|
+
// }
|
|
183
|
+
}
|
|
184
|
+
catch(Error err){
|
|
185
|
+
new AlertDialog.Builder(this.context)
|
|
186
|
+
.setMessage("Error: " + err.toString())
|
|
187
|
+
.setPositiveButton("OK", null)
|
|
188
|
+
.show();
|
|
189
|
+
|
|
190
|
+
if (pendingPurchaseCall != null) {
|
|
191
|
+
response.put("responseCode", -1);
|
|
192
|
+
response.put("responseMessage", err.toString());
|
|
193
|
+
pendingPurchaseCall.resolve(response);
|
|
194
|
+
pendingPurchaseCall = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
Context savedContext;
|
|
200
|
+
|
|
201
|
+
public Purchases(PurchasesPlugin plugin) {
|
|
202
|
+
this.context = plugin.getContext();
|
|
203
|
+
this.activity = plugin.getActivity();
|
|
204
|
+
|
|
205
|
+
this.billingClient = BillingClient.newBuilder(context)
|
|
206
|
+
.setListener(purchasesUpdatedListener)
|
|
207
|
+
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().enablePrepaidPlans().build())
|
|
208
|
+
.build();
|
|
209
|
+
|
|
210
|
+
// Start initial connection
|
|
211
|
+
startBillingConnection();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Schedule a reconnection attempt with exponential backoff.
|
|
216
|
+
* Delay increases: 1s, 2s, 4s, 8s, 16s (max 5 attempts)
|
|
217
|
+
*/
|
|
218
|
+
private void scheduleReconnect() {
|
|
219
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
220
|
+
Log.e("Purchases", "Max reconnection attempts reached. Please restart the app.");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
105
223
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
String title = productDetails.getTitle();
|
|
109
|
-
String desc = productDetails.getDescription();
|
|
110
|
-
Log.i("productIdentifier", productId);
|
|
111
|
-
Log.i("displayName", title);
|
|
112
|
-
Log.i("desc", desc);
|
|
224
|
+
long delayMs = RECONNECT_BASE_DELAY_MS * (1L << reconnectAttempts); // Exponential backoff
|
|
225
|
+
reconnectAttempts++;
|
|
113
226
|
|
|
114
|
-
|
|
227
|
+
Log.i("Purchases", "Scheduling reconnect attempt " + reconnectAttempts + " in " + delayMs + "ms");
|
|
115
228
|
|
|
116
|
-
|
|
229
|
+
reconnectHandler.postDelayed(this::startBillingConnection, delayMs);
|
|
230
|
+
}
|
|
117
231
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Start or restart the billing client connection.
|
|
234
|
+
*/
|
|
235
|
+
private void startBillingConnection() {
|
|
236
|
+
if (billingClient.isReady()) {
|
|
237
|
+
Log.i("Purchases", "BillingClient is already connected");
|
|
238
|
+
billingClientIsConnected = 1;
|
|
239
|
+
reconnectAttempts = 0;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
123
242
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
243
|
+
billingClient.startConnection(new BillingClientStateListener() {
|
|
244
|
+
@Override
|
|
245
|
+
public void onBillingSetupFinished(BillingResult billingResult) {
|
|
246
|
+
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
|
247
|
+
billingClientIsConnected = 1;
|
|
248
|
+
reconnectAttempts = 0; // Reset on successful connection
|
|
249
|
+
Log.i("Purchases", "BillingClient connected successfully");
|
|
250
|
+
} else {
|
|
251
|
+
billingClientIsConnected = billingResult.getResponseCode();
|
|
252
|
+
Log.e("Purchases", "BillingClient connection failed: " + billingResult.getDebugMessage());
|
|
253
|
+
// Try again with backoff
|
|
254
|
+
scheduleReconnect();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@Override
|
|
259
|
+
public void onBillingServiceDisconnected() {
|
|
260
|
+
billingClientIsConnected = -1;
|
|
261
|
+
Log.w("Purchases", "BillingClient disconnected");
|
|
262
|
+
scheduleReconnect();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Ensure billing client is connected before executing an operation.
|
|
269
|
+
* Returns true if connected, false if reconnection was triggered.
|
|
270
|
+
*/
|
|
271
|
+
public boolean ensureConnected() {
|
|
272
|
+
if (billingClient.isReady()) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
// Trigger reconnection
|
|
276
|
+
reconnectAttempts = 0; // Reset for on-demand reconnection
|
|
277
|
+
startBillingConnection();
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
127
280
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
281
|
+
public String echo(String value) {
|
|
282
|
+
Log.i("Echo", value);
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
133
285
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
286
|
+
public void setGoogleVerificationDetails(String googleVerifyEndpoint, String bid) {
|
|
287
|
+
this.googleVerifyEndpoint = googleVerifyEndpoint;
|
|
288
|
+
this.googleBid = bid;
|
|
137
289
|
|
|
138
|
-
|
|
290
|
+
Log.i("SET-VERIFY", "Verification values updated");
|
|
291
|
+
}
|
|
139
292
|
|
|
140
|
-
|
|
141
|
-
response.put("responseMessage", "Android: BillingClient failed to initialise");
|
|
142
|
-
call.resolve(response);
|
|
293
|
+
public void getProductDetails(String productIdentifier, PluginCall call) {
|
|
143
294
|
|
|
144
|
-
|
|
295
|
+
JSObject response = new JSObject();
|
|
145
296
|
|
|
146
|
-
|
|
147
|
-
|
|
297
|
+
if (!ensureConnected()) {
|
|
298
|
+
response.put("responseCode", 503);
|
|
299
|
+
response.put("responseMessage", "Android: BillingClient is reconnecting, please retry");
|
|
300
|
+
call.resolve(response);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
148
303
|
|
|
149
|
-
|
|
150
|
-
|
|
304
|
+
// Check cache first
|
|
305
|
+
String cachedType = productTypeCache.get(productIdentifier);
|
|
306
|
+
|
|
307
|
+
if (cachedType != null) {
|
|
308
|
+
// Use cached type directly
|
|
309
|
+
if (cachedType.equals(BillingClient.ProductType.SUBS)) {
|
|
310
|
+
querySubsProduct(productIdentifier, call, response);
|
|
311
|
+
} else {
|
|
312
|
+
queryInAppProduct(productIdentifier, call, response);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
// Not cached - try SUBS first, then INAPP
|
|
316
|
+
querySubsProduct(productIdentifier, call, response);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private void querySubsProduct(String productIdentifier, PluginCall call, JSObject response) {
|
|
321
|
+
QueryProductDetailsParams.Product subsProductToFind = QueryProductDetailsParams.Product.newBuilder()
|
|
322
|
+
.setProductId(productIdentifier)
|
|
323
|
+
.setProductType(BillingClient.ProductType.SUBS)
|
|
324
|
+
.build();
|
|
325
|
+
|
|
326
|
+
QueryProductDetailsParams subsQueryParams =
|
|
327
|
+
QueryProductDetailsParams.newBuilder()
|
|
328
|
+
.setProductList(List.of(subsProductToFind))
|
|
329
|
+
.build();
|
|
330
|
+
|
|
331
|
+
billingClient.queryProductDetailsAsync(
|
|
332
|
+
subsQueryParams,
|
|
333
|
+
(billingResult, productDetailsList) -> {
|
|
334
|
+
|
|
335
|
+
if (productDetailsList != null && !productDetailsList.isEmpty()) {
|
|
336
|
+
// Found as SUBS
|
|
337
|
+
try {
|
|
338
|
+
ProductDetails productDetails = productDetailsList.get(0);
|
|
339
|
+
String productId = productDetails.getProductId();
|
|
340
|
+
String title = productDetails.getTitle();
|
|
341
|
+
String desc = productDetails.getDescription();
|
|
342
|
+
Log.i("productIdentifier", productId);
|
|
343
|
+
Log.i("displayName", title);
|
|
344
|
+
Log.i("desc", desc);
|
|
345
|
+
|
|
346
|
+
List<ProductDetails.SubscriptionOfferDetails> subscriptionOfferDetails = productDetails.getSubscriptionOfferDetails();
|
|
347
|
+
String price = Objects.requireNonNull(subscriptionOfferDetails).get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
|
|
348
|
+
|
|
349
|
+
// Cache product type as SUBS
|
|
350
|
+
productTypeCache.put(productId, BillingClient.ProductType.SUBS);
|
|
351
|
+
|
|
352
|
+
JSObject data = new JSObject();
|
|
353
|
+
data.put("productIdentifier", productId);
|
|
354
|
+
data.put("displayName", title);
|
|
355
|
+
data.put("description", desc);
|
|
356
|
+
data.put("price", price);
|
|
357
|
+
data.put("productType", "SUBS");
|
|
358
|
+
|
|
359
|
+
response.put("responseCode", 0);
|
|
360
|
+
response.put("responseMessage", "Successfully found the product details for given productIdentifier");
|
|
361
|
+
response.put("data", data);
|
|
151
362
|
call.resolve(response);
|
|
152
363
|
|
|
364
|
+
} catch (Exception e) {
|
|
365
|
+
Log.e("Err", e.toString());
|
|
366
|
+
// If error processing SUBS, try INAPP
|
|
367
|
+
queryInAppProduct(productIdentifier, call, response);
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
// Not found as SUBS, try INAPP
|
|
371
|
+
queryInAppProduct(productIdentifier, call, response);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private void queryInAppProduct(String productIdentifier, PluginCall call, JSObject response) {
|
|
378
|
+
QueryProductDetailsParams.Product inAppProductToFind = QueryProductDetailsParams.Product.newBuilder()
|
|
379
|
+
.setProductId(productIdentifier)
|
|
380
|
+
.setProductType(BillingClient.ProductType.INAPP)
|
|
381
|
+
.build();
|
|
382
|
+
|
|
383
|
+
QueryProductDetailsParams inAppQueryParams =
|
|
384
|
+
QueryProductDetailsParams.newBuilder()
|
|
385
|
+
.setProductList(List.of(inAppProductToFind))
|
|
386
|
+
.build();
|
|
387
|
+
|
|
388
|
+
billingClient.queryProductDetailsAsync(
|
|
389
|
+
inAppQueryParams,
|
|
390
|
+
(billingResult, productDetailsList) -> {
|
|
391
|
+
try {
|
|
392
|
+
if (productDetailsList != null && !productDetailsList.isEmpty()) {
|
|
393
|
+
ProductDetails productDetails = productDetailsList.get(0);
|
|
394
|
+
String productId = productDetails.getProductId();
|
|
395
|
+
String title = productDetails.getTitle();
|
|
396
|
+
String desc = productDetails.getDescription();
|
|
397
|
+
Log.i("productIdentifier", productId);
|
|
398
|
+
Log.i("displayName", title);
|
|
399
|
+
Log.i("desc", desc);
|
|
400
|
+
|
|
401
|
+
ProductDetails.OneTimePurchaseOfferDetails oneTimeOfferDetails = productDetails.getOneTimePurchaseOfferDetails();
|
|
402
|
+
String price = Objects.requireNonNull(oneTimeOfferDetails).getFormattedPrice();
|
|
403
|
+
|
|
404
|
+
// Cache product type as INAPP
|
|
405
|
+
productTypeCache.put(productId, BillingClient.ProductType.INAPP);
|
|
406
|
+
|
|
407
|
+
JSObject data = new JSObject();
|
|
408
|
+
data.put("productIdentifier", productId);
|
|
409
|
+
data.put("displayName", title);
|
|
410
|
+
data.put("description", desc);
|
|
411
|
+
data.put("price", price);
|
|
412
|
+
data.put("productType", "INAPP");
|
|
413
|
+
|
|
414
|
+
response.put("responseCode", 0);
|
|
415
|
+
response.put("responseMessage", "Successfully found the product details for given productIdentifier");
|
|
416
|
+
response.put("data", data);
|
|
417
|
+
} else {
|
|
418
|
+
response.put("responseCode", 1);
|
|
419
|
+
response.put("responseMessage", "Could not find a product matching the given productIdentifier");
|
|
420
|
+
}
|
|
421
|
+
} catch (Exception e) {
|
|
422
|
+
Log.e("Err", e.toString());
|
|
423
|
+
response.put("responseCode", 1);
|
|
424
|
+
response.put("responseMessage", "Could not find a product matching the given productIdentifier");
|
|
153
425
|
}
|
|
154
|
-
}
|
|
155
426
|
|
|
156
|
-
|
|
427
|
+
call.resolve(response);
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
}
|
|
157
431
|
|
|
158
|
-
|
|
432
|
+
/**
|
|
433
|
+
* Get product type from cache, defaults to SUBS if not cached
|
|
434
|
+
*/
|
|
435
|
+
private String getProductType(String productIdentifier) {
|
|
436
|
+
return productTypeCache.getOrDefault(productIdentifier, null);
|
|
437
|
+
}
|
|
159
438
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
QueryPurchaseHistoryParams queryPurchaseHistoryParams =
|
|
163
|
-
QueryPurchaseHistoryParams.newBuilder()
|
|
164
|
-
.setProductType(BillingClient.ProductType.SUBS)
|
|
165
|
-
.build();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
billingClient.queryPurchaseHistoryAsync(queryPurchaseHistoryParams, (BillingResult billingResult, List<PurchaseHistoryRecord> list) -> {
|
|
169
|
-
|
|
170
|
-
// Try to loop through the list until we find a purchase history record associated with the passed in productIdentifier.
|
|
171
|
-
// If we do, then set found to true to break out of the loop, then compile a response with necessary data. Otherwise compile
|
|
172
|
-
// a response saying that the there were not transactions for the given productIdentifier.
|
|
173
|
-
int i = 0;
|
|
174
|
-
boolean found = false;
|
|
175
|
-
while (list != null && (i < list.size() && !found)) {
|
|
176
|
-
try {
|
|
177
|
-
|
|
178
|
-
JSObject currentPurchaseHistoryRecord = new JSObject(list.get(i).getOriginalJson());
|
|
179
|
-
Log.i("PurchaseHistory", currentPurchaseHistoryRecord.toString());
|
|
180
|
-
|
|
181
|
-
if (currentPurchaseHistoryRecord.get("productId").equals(productIdentifier)) {
|
|
182
|
-
|
|
183
|
-
found = true;
|
|
184
|
-
|
|
185
|
-
JSObject data = new JSObject();
|
|
186
|
-
String expiryDate = getExpiryDateFromGoogle(productIdentifier, currentPurchaseHistoryRecord.get("purchaseToken").toString());
|
|
187
|
-
if (expiryDate != null) {
|
|
188
|
-
data.put("expiryDate", expiryDate);
|
|
189
|
-
}
|
|
190
|
-
Calendar calendar = Calendar.getInstance();
|
|
191
|
-
calendar.setTimeInMillis(Long.parseLong((currentPurchaseHistoryRecord.get("purchaseTime").toString())));
|
|
192
|
-
String orderId = currentPurchaseHistoryRecord.optString("orderId", ""); // Usamos optString para obtener un valor por defecto si la clave no existe
|
|
193
|
-
data.put("productIdentifier", currentPurchaseHistoryRecord.get("productId"));
|
|
194
|
-
data.put("originalId", orderId);
|
|
195
|
-
data.put("transactionId", orderId);
|
|
196
|
-
data.put("developerPayload",currentPurchaseHistoryRecord.optString("developerPayload", "")); // Usamos optString para obtener un valor por defecto si la clave no existe
|
|
197
|
-
data.put("purchaseToken", currentPurchaseHistoryRecord.get("purchaseToken").toString());
|
|
198
|
-
|
|
199
|
-
response.put("responseCode", 0);
|
|
200
|
-
response.put("responseMessage", "Successfully found the latest transaction matching given productIdentifier");
|
|
201
|
-
response.put("data", data);
|
|
202
|
-
}
|
|
203
|
-
} catch (Exception e) {
|
|
204
|
-
Logger.error(e.getMessage());
|
|
205
|
-
}
|
|
439
|
+
public void getLatestTransaction(String productIdentifier, PluginCall call) {
|
|
206
440
|
|
|
207
|
-
|
|
441
|
+
JSObject response = new JSObject();
|
|
208
442
|
|
|
209
|
-
|
|
443
|
+
if (!ensureConnected()) {
|
|
444
|
+
response.put("responseCode", 503);
|
|
445
|
+
response.put("responseMessage", "Android: BillingClient is reconnecting, please retry");
|
|
446
|
+
call.resolve(response);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
210
449
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
450
|
+
// Use cached product type, defaults to SUBS
|
|
451
|
+
String productType = getProductType(productIdentifier);
|
|
452
|
+
if (productType == null) {
|
|
453
|
+
response.put("responseCode", 1);
|
|
454
|
+
response.put("responseMessage", "Please load products first so type will be identified");
|
|
455
|
+
call.resolve(response);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
217
458
|
|
|
218
|
-
|
|
459
|
+
QueryPurchaseHistoryParams queryPurchaseHistoryParams =
|
|
460
|
+
QueryPurchaseHistoryParams.newBuilder()
|
|
461
|
+
.setProductType(productType)
|
|
462
|
+
.build();
|
|
219
463
|
|
|
220
|
-
|
|
464
|
+
billingClient.queryPurchaseHistoryAsync(queryPurchaseHistoryParams, (BillingResult billingResult, List<PurchaseHistoryRecord> list) -> {
|
|
221
465
|
|
|
222
|
-
|
|
466
|
+
// Try to loop through the list until we find a purchase history record associated with the passed in productIdentifier.
|
|
467
|
+
// If we do, then set found to true to break out of the loop, then compile a response with necessary data. Otherwise compile
|
|
468
|
+
// a response saying that the there were not transactions for the given productIdentifier.
|
|
469
|
+
int i = 0;
|
|
470
|
+
boolean found = false;
|
|
471
|
+
while (list != null && (i < list.size() && !found)) {
|
|
472
|
+
try {
|
|
223
473
|
|
|
224
|
-
|
|
474
|
+
JSObject currentPurchaseHistoryRecord = new JSObject(list.get(i).getOriginalJson());
|
|
475
|
+
Log.i("PurchaseHistory", currentPurchaseHistoryRecord.toString());
|
|
225
476
|
|
|
226
|
-
|
|
477
|
+
if (currentPurchaseHistoryRecord.get("productId").equals(productIdentifier)) {
|
|
227
478
|
|
|
228
|
-
|
|
479
|
+
found = true;
|
|
229
480
|
|
|
230
|
-
|
|
481
|
+
JSObject data = new JSObject();
|
|
482
|
+
String expiryDate = getExpiryDateFromGoogle(productIdentifier, currentPurchaseHistoryRecord.get("purchaseToken").toString());
|
|
483
|
+
if (expiryDate != null) {
|
|
484
|
+
data.put("expiryDate", expiryDate);
|
|
485
|
+
}
|
|
486
|
+
Calendar calendar = Calendar.getInstance();
|
|
487
|
+
calendar.setTimeInMillis(Long.parseLong((currentPurchaseHistoryRecord.get("purchaseTime").toString())));
|
|
488
|
+
String orderId = currentPurchaseHistoryRecord.optString("orderId", ""); // Usamos optString para obtener un valor por defecto si la clave no existe
|
|
489
|
+
data.put("productIdentifier", currentPurchaseHistoryRecord.get("productId"));
|
|
490
|
+
data.put("originalId", orderId);
|
|
491
|
+
data.put("transactionId", orderId);
|
|
492
|
+
data.put("developerPayload", currentPurchaseHistoryRecord.optString("developerPayload", "")); // Usamos optString para obtener un valor por defecto si la clave no existe
|
|
493
|
+
data.put("purchaseToken", currentPurchaseHistoryRecord.get("purchaseToken").toString());
|
|
494
|
+
|
|
495
|
+
response.put("responseCode", 0);
|
|
496
|
+
response.put("responseMessage", "Successfully found the latest transaction matching given productIdentifier");
|
|
497
|
+
response.put("data", data);
|
|
498
|
+
}
|
|
499
|
+
} catch (Exception e) {
|
|
500
|
+
Logger.error(e.getMessage());
|
|
501
|
+
}
|
|
231
502
|
|
|
232
|
-
|
|
233
|
-
QueryPurchasesParams.newBuilder()
|
|
234
|
-
.setProductType(BillingClient.ProductType.SUBS)
|
|
235
|
-
.build();
|
|
503
|
+
i++;
|
|
236
504
|
|
|
237
|
-
|
|
238
|
-
queryPurchasesParams,
|
|
239
|
-
(billingResult, purchaseList) -> {
|
|
505
|
+
}
|
|
240
506
|
|
|
241
|
-
|
|
507
|
+
// If after looping through the list of purchase history records, no records are found to be associated with
|
|
508
|
+
// the given product identifier, return a response saying no transactions found
|
|
509
|
+
if (!found) {
|
|
510
|
+
response.put("responseCode", 3);
|
|
511
|
+
response.put("responseMessage", "No transaction for given productIdentifier, or it could not be verified");
|
|
512
|
+
}
|
|
242
513
|
|
|
243
|
-
|
|
514
|
+
call.resolve(response);
|
|
244
515
|
|
|
245
|
-
|
|
516
|
+
});
|
|
517
|
+
}
|
|
246
518
|
|
|
247
|
-
|
|
248
|
-
for (int i = 0; i < purchaseList.size(); i++) {
|
|
519
|
+
public void getCurrentEntitlements(PluginCall call) {
|
|
249
520
|
|
|
250
|
-
|
|
521
|
+
JSObject response = new JSObject();
|
|
251
522
|
|
|
252
|
-
|
|
253
|
-
|
|
523
|
+
if (!ensureConnected()) {
|
|
524
|
+
response.put("responseCode", 503);
|
|
525
|
+
response.put("responseMessage", "Android: BillingClient is reconnecting, please retry");
|
|
526
|
+
call.resolve(response);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
254
529
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
530
|
+
ArrayList<JSObject> allEntitlements = new ArrayList<>();
|
|
531
|
+
|
|
532
|
+
// First query SUBS
|
|
533
|
+
QueryPurchasesParams subsQueryParams =
|
|
534
|
+
QueryPurchasesParams.newBuilder()
|
|
535
|
+
.setProductType(BillingClient.ProductType.SUBS)
|
|
536
|
+
.build();
|
|
537
|
+
|
|
538
|
+
billingClient.queryPurchasesAsync(
|
|
539
|
+
subsQueryParams,
|
|
540
|
+
(billingResult, subsPurchaseList) -> {
|
|
541
|
+
|
|
542
|
+
// Process SUBS purchases
|
|
543
|
+
processEntitlements(subsPurchaseList, allEntitlements, "SUBS");
|
|
544
|
+
|
|
545
|
+
// Then query INAPP
|
|
546
|
+
QueryPurchasesParams inAppQueryParams =
|
|
547
|
+
QueryPurchasesParams.newBuilder()
|
|
548
|
+
.setProductType(BillingClient.ProductType.INAPP)
|
|
549
|
+
.build();
|
|
550
|
+
|
|
551
|
+
billingClient.queryPurchasesAsync(
|
|
552
|
+
inAppQueryParams,
|
|
553
|
+
(billingResult2, inAppPurchaseList) -> {
|
|
554
|
+
|
|
555
|
+
// Process INAPP purchases
|
|
556
|
+
processEntitlements(inAppPurchaseList, allEntitlements, "INAPP");
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
if (!allEntitlements.isEmpty()) {
|
|
560
|
+
response.put("responseCode", 0);
|
|
561
|
+
response.put("responseMessage", "Successfully found all entitlements across all product types");
|
|
562
|
+
response.put("data", allEntitlements);
|
|
563
|
+
} else {
|
|
564
|
+
Log.i("No Purchases", "No active purchases found");
|
|
565
|
+
response.put("responseCode", 1);
|
|
566
|
+
response.put("responseMessage", "No entitlements were found");
|
|
567
|
+
}
|
|
568
|
+
} catch (Exception e) {
|
|
569
|
+
Log.e("Error", e.toString());
|
|
570
|
+
response.put("responseCode", 2);
|
|
571
|
+
response.put("responseMessage", e.toString());
|
|
572
|
+
}
|
|
259
573
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
574
|
+
call.resolve(response);
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private void processEntitlements(List<Purchase> purchaseList, ArrayList<JSObject> entitlements, String productType) {
|
|
582
|
+
if (purchaseList == null) return;
|
|
583
|
+
|
|
584
|
+
for (Purchase currentPurchase : purchaseList) {
|
|
585
|
+
try {
|
|
586
|
+
String productId = currentPurchase.getProducts().get(0);
|
|
587
|
+
String orderId = currentPurchase.getOrderId();
|
|
588
|
+
|
|
589
|
+
String dateFormat = "dd-MM-yyyy hh:mm";
|
|
590
|
+
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.getDefault());
|
|
591
|
+
Calendar calendar = Calendar.getInstance();
|
|
592
|
+
calendar.setTimeInMillis(currentPurchase.getPurchaseTime());
|
|
593
|
+
|
|
594
|
+
JSObject entitlement = new JSObject()
|
|
595
|
+
.put("productIdentifier", productId)
|
|
596
|
+
.put("originalStartDate", simpleDateFormat.format(calendar.getTime()))
|
|
597
|
+
.put("originalId", orderId)
|
|
598
|
+
.put("transactionId", orderId)
|
|
599
|
+
.put("purchaseToken", currentPurchase.getPurchaseToken())
|
|
600
|
+
.put("productType", productType);
|
|
601
|
+
|
|
602
|
+
// Only get expiry date for subscriptions
|
|
603
|
+
if (productType.equals("SUBS")) {
|
|
604
|
+
String expiryDate = this.getExpiryDateFromGoogle(productId, currentPurchase.getPurchaseToken());
|
|
605
|
+
entitlement.put("expiryDate", expiryDate);
|
|
606
|
+
}
|
|
270
607
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
response.put("data", entitlements);
|
|
608
|
+
// Cache the product type
|
|
609
|
+
// productTypeCache.put(productId, productType.equals("SUBS") ? BillingClient.ProductType.SUBS : BillingClient.ProductType.INAPP);
|
|
274
610
|
|
|
611
|
+
entitlements.add(entitlement);
|
|
612
|
+
} catch (Exception e) {
|
|
613
|
+
Log.e("Error", "Error processing entitlement: " + e.toString());
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
275
617
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
response.put("responseCode", 1);
|
|
279
|
-
response.put("responseMessage", "No entitlements were found");
|
|
280
|
-
}
|
|
618
|
+
public void purchaseProduct(String productIdentifier, String accountToken, PluginCall call) {
|
|
619
|
+
JSObject response = new JSObject();
|
|
281
620
|
|
|
621
|
+
if (!ensureConnected()) {
|
|
622
|
+
response.put("responseCode", 503);
|
|
623
|
+
response.put("responseMessage", "Android: BillingClient is reconnecting, please retry");
|
|
624
|
+
call.resolve(response);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
282
627
|
|
|
283
|
-
|
|
628
|
+
// Use cached product type, defaults to SUBS
|
|
629
|
+
String productType = getProductType(productIdentifier);
|
|
284
630
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
631
|
+
if (productType == null) {
|
|
632
|
+
response.put("responseCode", 1);
|
|
633
|
+
response.put("responseMessage", "Please load products first so type will be identified");
|
|
634
|
+
call.resolve(response);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
290
637
|
|
|
291
|
-
|
|
638
|
+
QueryProductDetailsParams.Product productToFind = QueryProductDetailsParams.Product.newBuilder()
|
|
639
|
+
.setProductId(productIdentifier)
|
|
640
|
+
.setProductType(productType)
|
|
641
|
+
.build();
|
|
292
642
|
|
|
293
|
-
|
|
294
|
-
|
|
643
|
+
QueryProductDetailsParams queryProductDetailsParams =
|
|
644
|
+
QueryProductDetailsParams.newBuilder()
|
|
645
|
+
.setProductList(List.of(productToFind))
|
|
646
|
+
.build();
|
|
295
647
|
|
|
648
|
+
billingClient.queryProductDetailsAsync(
|
|
649
|
+
queryProductDetailsParams,
|
|
650
|
+
(billingResult1, productDetailsList) -> {
|
|
651
|
+
|
|
652
|
+
if (productDetailsList != null && !productDetailsList.isEmpty()) {
|
|
653
|
+
launchBillingFlow(productDetailsList.get(0), accountToken, call);
|
|
654
|
+
} else {
|
|
655
|
+
JSObject resp = new JSObject();
|
|
656
|
+
resp.put("responseCode", 1);
|
|
657
|
+
resp.put("responseMessage", "Product not found. " + productIdentifier + " " + productType + " " + productTypeCache.size());
|
|
658
|
+
call.resolve(resp);
|
|
296
659
|
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
297
662
|
|
|
298
|
-
|
|
663
|
+
private void launchBillingFlow(ProductDetails productDetails, String accountToken, PluginCall call) {
|
|
664
|
+
try {
|
|
665
|
+
BillingFlowParams.ProductDetailsParams.Builder productParamsBuilder =
|
|
666
|
+
BillingFlowParams.ProductDetailsParams.newBuilder()
|
|
667
|
+
.setProductDetails(productDetails);
|
|
299
668
|
|
|
300
|
-
|
|
669
|
+
if (productDetails.getProductType().equals(BillingClient.ProductType.SUBS)) {
|
|
670
|
+
productParamsBuilder.setOfferToken(Objects.requireNonNull(productDetails.getSubscriptionOfferDetails()).get(0).getOfferToken());
|
|
671
|
+
}
|
|
301
672
|
|
|
302
|
-
|
|
673
|
+
BillingFlowParams.Builder paramsBuilder = BillingFlowParams.newBuilder()
|
|
674
|
+
.setProductDetailsParamsList(List.of(productParamsBuilder.build()));
|
|
303
675
|
|
|
304
|
-
|
|
676
|
+
if (accountToken != null && !accountToken.isEmpty()) {
|
|
677
|
+
paramsBuilder.setObfuscatedAccountId(accountToken);
|
|
678
|
+
}
|
|
305
679
|
|
|
306
|
-
|
|
307
|
-
.setProductId(productIdentifier)
|
|
308
|
-
.setProductType(BillingClient.ProductType.SUBS)
|
|
309
|
-
.build();
|
|
680
|
+
BillingFlowParams billingFlowParams = paramsBuilder.build();
|
|
310
681
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
.build();
|
|
315
|
-
|
|
316
|
-
billingClient.queryProductDetailsAsync(
|
|
317
|
-
queryProductDetailsParams,
|
|
318
|
-
(billingResult1, productDetailsList) -> {
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
ProductDetails productDetails = productDetailsList.get(0);
|
|
322
|
-
BillingFlowParams.Builder paramsBuilder = BillingFlowParams.newBuilder()
|
|
323
|
-
.setProductDetailsParamsList(
|
|
324
|
-
List.of(
|
|
325
|
-
BillingFlowParams.ProductDetailsParams.newBuilder()
|
|
326
|
-
.setProductDetails(productDetails)
|
|
327
|
-
.setOfferToken(Objects.requireNonNull(productDetails.getSubscriptionOfferDetails()).get(0).getOfferToken())
|
|
328
|
-
.build()
|
|
329
|
-
)
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
if (accountToken != null && !accountToken.isEmpty()) {
|
|
333
|
-
paramsBuilder.setObfuscatedAccountId(accountToken);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
BillingFlowParams billingFlowParams = paramsBuilder.build();
|
|
337
|
-
|
|
338
|
-
BillingResult result = billingClient.launchBillingFlow(this.activity, billingFlowParams);
|
|
339
|
-
|
|
340
|
-
Log.i("RESULT", result.toString());
|
|
341
|
-
response.put("responseCode", 0);
|
|
342
|
-
response.put("responseMessage", "Successfully opened native popover");
|
|
343
|
-
|
|
344
|
-
} catch (Exception e) {
|
|
345
|
-
Logger.error(e.getMessage());
|
|
346
|
-
response.put("responseCode", 1);
|
|
347
|
-
response.put("responseMessage", "Failed to open native popover");
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
call.resolve(response);
|
|
351
|
-
});
|
|
352
|
-
}
|
|
682
|
+
BillingResult result = billingClient.launchBillingFlow(this.activity, billingFlowParams);
|
|
683
|
+
|
|
684
|
+
Log.i("RESULT", result.toString());
|
|
353
685
|
|
|
686
|
+
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
|
687
|
+
// Save the call to resolve later in purchasesUpdatedListener
|
|
688
|
+
pendingPurchaseCall = call;
|
|
689
|
+
} else {
|
|
690
|
+
JSObject response = new JSObject();
|
|
691
|
+
// Billing flow failed to launch
|
|
692
|
+
response.put("responseCode", result.getResponseCode());
|
|
693
|
+
response.put("responseMessage", "Failed to launch billing flow: " + result.getDebugMessage());
|
|
694
|
+
call.resolve(response);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
} catch (Exception e) {
|
|
698
|
+
Logger.error(e.getMessage());
|
|
699
|
+
JSObject response = new JSObject();
|
|
700
|
+
response.put("responseCode", 1);
|
|
701
|
+
response.put("responseMessage", "Failed to open native popover. " + e.getMessage() + " " + productDetails.getProductType());
|
|
702
|
+
call.resolve(response);
|
|
354
703
|
}
|
|
704
|
+
}
|
|
355
705
|
|
|
356
|
-
|
|
706
|
+
private String getExpiryDateFromGoogle(String productIdentifier, String purchaseToken) {
|
|
357
707
|
|
|
358
|
-
|
|
708
|
+
try {
|
|
359
709
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
710
|
+
// Compile request to verify purchase token
|
|
711
|
+
URL obj = new URL(this.googleVerifyEndpoint + "?bid=" + this.googleBid + "&subId=" + productIdentifier + "&purchaseToken=" + purchaseToken);
|
|
712
|
+
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
|
|
713
|
+
con.setRequestMethod("GET");
|
|
364
714
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
715
|
+
// Try to receive response from server
|
|
716
|
+
try (BufferedReader br = new BufferedReader(
|
|
717
|
+
new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) {
|
|
368
718
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// If the response was successful, extract expiryDate and put it in our response data property
|
|
377
|
-
if (con.getResponseCode() == 200) {
|
|
378
|
-
JSObject postResponseJSON = new JSObject(googleResponse.toString());
|
|
379
|
-
JSObject googleResponseJSON = new JSObject(postResponseJSON.get("googleResponse").toString()); // <-- note the typo in response object from server
|
|
380
|
-
JSObject payloadJSON = new JSObject(googleResponseJSON.get("payload").toString());
|
|
381
|
-
String dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
|
|
382
|
-
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.getDefault());
|
|
383
|
-
Calendar calendar = Calendar.getInstance();
|
|
384
|
-
calendar.setTimeInMillis(Long.parseLong(payloadJSON.get("expiryTimeMillis").toString()));
|
|
385
|
-
return simpleDateFormat.format(calendar.getTime());
|
|
386
|
-
} else {
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
} catch (Exception e) {
|
|
390
|
-
Logger.error(e.getMessage());
|
|
391
|
-
}
|
|
392
|
-
} catch (Exception e) {
|
|
393
|
-
Logger.error(e.getMessage());
|
|
719
|
+
StringBuilder googleResponse = new StringBuilder();
|
|
720
|
+
String responseLine;
|
|
721
|
+
while ((responseLine = br.readLine()) != null) {
|
|
722
|
+
googleResponse.append(responseLine.trim());
|
|
723
|
+
Log.i("Response Line", responseLine);
|
|
394
724
|
}
|
|
395
725
|
|
|
396
|
-
// If the
|
|
397
|
-
|
|
398
|
-
|
|
726
|
+
// If the response was successful, extract expiryDate and put it in our response data property
|
|
727
|
+
if (con.getResponseCode() == 200) {
|
|
728
|
+
JSObject postResponseJSON = new JSObject(googleResponse.toString());
|
|
729
|
+
JSObject googleResponseJSON = new JSObject(postResponseJSON.get("googleResponse").toString()); // <-- note the typo in response object from server
|
|
730
|
+
JSObject payloadJSON = new JSObject(googleResponseJSON.get("payload").toString());
|
|
731
|
+
String dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
|
|
732
|
+
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.getDefault());
|
|
733
|
+
Calendar calendar = Calendar.getInstance();
|
|
734
|
+
calendar.setTimeInMillis(Long.parseLong(payloadJSON.get("expiryTimeMillis").toString()));
|
|
735
|
+
return simpleDateFormat.format(calendar.getTime());
|
|
736
|
+
} else {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
} catch (Exception e) {
|
|
740
|
+
Logger.error(e.getMessage());
|
|
741
|
+
}
|
|
742
|
+
} catch (Exception e) {
|
|
743
|
+
Logger.error(e.getMessage());
|
|
399
744
|
}
|
|
745
|
+
|
|
746
|
+
// If the method manages to each this far before already returning, just return null
|
|
747
|
+
// because something went wrong
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
400
750
|
}
|
|
@@ -8,162 +8,106 @@ import com.getcapacitor.annotation.CapacitorPlugin;
|
|
|
8
8
|
|
|
9
9
|
import android.content.Intent;
|
|
10
10
|
import android.net.Uri;
|
|
11
|
-
import android.util.Log;
|
|
12
|
-
|
|
13
|
-
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
|
14
|
-
import com.android.billingclient.api.BillingClient;
|
|
15
|
-
import com.android.billingclient.api.Purchase;
|
|
16
|
-
import com.android.billingclient.api.PurchasesUpdatedListener;
|
|
17
|
-
|
|
18
11
|
|
|
19
12
|
@CapacitorPlugin(name = "Purchases")
|
|
20
13
|
public class PurchasesPlugin extends Plugin {
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
private BillingClient billingClient;
|
|
25
|
-
|
|
26
|
-
public PurchasesPlugin () {
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// This listener is fired upon completing the billing flow, it is vital to call the acknowledgePurchase
|
|
31
|
-
// method on the billingClient, with the purchase token otherwise Google will automatically cancel the subscription
|
|
32
|
-
// shortly after the purchase
|
|
33
|
-
private final PurchasesUpdatedListener purchasesUpdatedListener = (billingResult, purchases) -> {
|
|
34
|
-
|
|
35
|
-
JSObject response = new JSObject();
|
|
36
|
-
|
|
37
|
-
if(purchases != null) {
|
|
38
|
-
for (int i = 0; i < purchases.size(); i++) {
|
|
39
|
-
|
|
40
|
-
Purchase currentPurchase = purchases.get(i);
|
|
41
|
-
if (!currentPurchase.isAcknowledged() && billingResult.getResponseCode() == 0 && currentPurchase.getPurchaseState() != 2) {
|
|
42
|
-
|
|
43
|
-
AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
|
|
44
|
-
.setPurchaseToken(currentPurchase.getPurchaseToken())
|
|
45
|
-
.build();
|
|
15
|
+
private Purchases implementation;
|
|
46
16
|
|
|
47
|
-
billingClient.acknowledgePurchase(acknowledgePurchaseParams, billingResult1 -> {
|
|
48
|
-
Log.i("Purchase ack", currentPurchase.getOriginalJson());
|
|
49
|
-
billingResult1.getResponseCode();
|
|
50
17
|
|
|
51
|
-
|
|
18
|
+
public PurchasesPlugin () {
|
|
19
|
+
}
|
|
52
20
|
|
|
53
|
-
// WARNING: Changed the notifyListeners method from protected to public in order to get the method call to work
|
|
54
|
-
// This may be a security issue in the future - in order to fix it, it may be best to move this listener + the billingClient
|
|
55
|
-
// initiation into the SubscriptionsPlugin.java, then pass it into this implementation class so we can still access the
|
|
56
|
-
// billingClient.
|
|
57
|
-
notifyListeners("ANDROID-PURCHASE-RESPONSE", response);
|
|
58
|
-
});
|
|
59
|
-
} else {
|
|
60
|
-
response.put("successful", false);
|
|
61
|
-
notifyListeners("ANDROID-PURCHASE-RESPONSE", response);
|
|
62
|
-
}
|
|
63
21
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
22
|
+
@Override
|
|
23
|
+
public void load() {
|
|
24
|
+
implementation = new Purchases(this);
|
|
25
|
+
}
|
|
69
26
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.billingClient = BillingClient.newBuilder(getContext())
|
|
76
|
-
.setListener(purchasesUpdatedListener)
|
|
77
|
-
.enablePendingPurchases()
|
|
78
|
-
.build();
|
|
79
|
-
implementation = new Purchases(this, billingClient);
|
|
27
|
+
@PluginMethod
|
|
28
|
+
public void setGoogleVerificationDetails(PluginCall call) {
|
|
29
|
+
String googleVerifyEndpoint = call.getString("googleVerifyEndpoint");
|
|
30
|
+
String bid = call.getString("bid");
|
|
80
31
|
|
|
32
|
+
if(googleVerifyEndpoint != null && bid != null) {
|
|
33
|
+
implementation.setGoogleVerificationDetails(googleVerifyEndpoint, bid);
|
|
34
|
+
} else {
|
|
35
|
+
call.reject("Missing required parameters");
|
|
81
36
|
}
|
|
37
|
+
}
|
|
82
38
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
String bid = call.getString("bid");
|
|
39
|
+
@PluginMethod
|
|
40
|
+
public void echo(PluginCall call) {
|
|
41
|
+
String value = call.getString("value");
|
|
87
42
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
}
|
|
43
|
+
JSObject ret = new JSObject();
|
|
44
|
+
ret.put("value", implementation.echo(value));
|
|
45
|
+
call.resolve(ret);
|
|
46
|
+
}
|
|
94
47
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
String value = call.getString("value");
|
|
48
|
+
@PluginMethod
|
|
49
|
+
public void getProductDetails(PluginCall call) {
|
|
98
50
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
51
|
+
String productIdentifier = call.getString("productIdentifier");
|
|
52
|
+
|
|
53
|
+
if (productIdentifier == null) {
|
|
54
|
+
call.reject("Must provide a productID");
|
|
102
55
|
}
|
|
103
56
|
|
|
104
|
-
|
|
105
|
-
public void getProductDetails(PluginCall call) {
|
|
57
|
+
implementation.getProductDetails(productIdentifier, call);
|
|
106
58
|
|
|
107
|
-
|
|
59
|
+
}
|
|
108
60
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
61
|
+
@PluginMethod
|
|
62
|
+
public void purchaseProduct(PluginCall call) {
|
|
112
63
|
|
|
113
|
-
|
|
64
|
+
String productIdentifier = call.getString("productIdentifier");
|
|
65
|
+
String accountToken = call.getString("accountToken", "");
|
|
114
66
|
|
|
67
|
+
if(productIdentifier == null) {
|
|
68
|
+
call.reject("Must provide a productID");
|
|
115
69
|
}
|
|
116
70
|
|
|
117
|
-
|
|
118
|
-
public void purchaseProduct(PluginCall call) {
|
|
71
|
+
implementation.purchaseProduct(productIdentifier, accountToken, call);
|
|
119
72
|
|
|
120
|
-
|
|
121
|
-
String accountToken = call.getString("accountToken", "");
|
|
73
|
+
}
|
|
122
74
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
75
|
+
@PluginMethod
|
|
76
|
+
public void getLatestTransaction(PluginCall call) {
|
|
126
77
|
|
|
127
|
-
|
|
78
|
+
String productIdentifier = call.getString("productIdentifier");
|
|
128
79
|
|
|
80
|
+
if(productIdentifier == null) {
|
|
81
|
+
call.reject("Must provide a productID");
|
|
129
82
|
}
|
|
130
83
|
|
|
131
|
-
|
|
132
|
-
public void getLatestTransaction(PluginCall call) {
|
|
84
|
+
implementation.getLatestTransaction(productIdentifier, call);
|
|
133
85
|
|
|
134
|
-
|
|
86
|
+
}
|
|
135
87
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
88
|
+
@PluginMethod
|
|
89
|
+
public void getCurrentEntitlements(PluginCall call) {
|
|
139
90
|
|
|
140
|
-
|
|
91
|
+
implementation.getCurrentEntitlements(call);
|
|
141
92
|
|
|
142
|
-
|
|
93
|
+
}
|
|
143
94
|
|
|
144
|
-
|
|
145
|
-
|
|
95
|
+
@PluginMethod
|
|
96
|
+
public void manageSubscriptions(PluginCall call) {
|
|
146
97
|
|
|
147
|
-
|
|
98
|
+
String productIdentifier = call.getString("productIdentifier");
|
|
99
|
+
String bid = call.getString("bid");
|
|
148
100
|
|
|
101
|
+
if(productIdentifier == null) {
|
|
102
|
+
call.reject("Must provide a productID");
|
|
149
103
|
}
|
|
150
104
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
String productIdentifier = call.getString("productIdentifier");
|
|
155
|
-
String bid = call.getString("bid");
|
|
156
|
-
|
|
157
|
-
if(productIdentifier == null) {
|
|
158
|
-
call.reject("Must provide a productID");
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if(bid == null) {
|
|
162
|
-
call.reject("Must provide a bundleID");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
Intent browserIntent = new Intent(Intent.ACTION_VIEW,
|
|
166
|
-
Uri.parse("https://play.google.com/store/account/subscriptions?sku=" + productIdentifier + "&package=" + bid));
|
|
167
|
-
getActivity().startActivity(browserIntent);
|
|
105
|
+
if(bid == null) {
|
|
106
|
+
call.reject("Must provide a bundleID");
|
|
168
107
|
}
|
|
108
|
+
|
|
109
|
+
Intent browserIntent = new Intent(Intent.ACTION_VIEW,
|
|
110
|
+
Uri.parse("https://play.google.com/store/account/subscriptions?sku=" + productIdentifier + "&package=" + bid));
|
|
111
|
+
getActivity().startActivity(browserIntent);
|
|
112
|
+
}
|
|
169
113
|
}
|