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