capacitor-native-purchases 0.1.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.
@@ -0,0 +1,400 @@
1
+ package jok.purchases.capacitor;
2
+
3
+ import android.app.Activity;
4
+ import android.util.Log;
5
+
6
+ import com.android.billingclient.api.BillingClient;
7
+
8
+ import com.android.billingclient.api.BillingClientStateListener;
9
+ import com.android.billingclient.api.BillingFlowParams;
10
+ import com.android.billingclient.api.BillingResult;
11
+ import com.android.billingclient.api.ProductDetails;
12
+ import com.android.billingclient.api.Purchase;
13
+ import com.android.billingclient.api.PurchaseHistoryRecord;
14
+ import com.android.billingclient.api.QueryProductDetailsParams;
15
+ import com.android.billingclient.api.QueryPurchaseHistoryParams;
16
+ import com.android.billingclient.api.QueryPurchasesParams;
17
+ import com.getcapacitor.JSObject;
18
+ import com.getcapacitor.Logger;
19
+ import com.getcapacitor.PluginCall;
20
+
21
+ import android.content.Context;
22
+
23
+
24
+ import androidx.annotation.NonNull;
25
+
26
+ import java.io.BufferedReader;
27
+ import java.io.InputStreamReader;
28
+ import java.net.HttpURLConnection;
29
+ import java.net.URL;
30
+ import java.nio.charset.StandardCharsets;
31
+ import java.text.SimpleDateFormat;
32
+ import java.util.ArrayList;
33
+ import java.util.Calendar;
34
+ import java.util.List;
35
+ import java.util.Locale;
36
+ import java.util.Objects;
37
+
38
+ public class Purchases {
39
+
40
+ private final Activity activity;
41
+ public Context context;
42
+ private final BillingClient billingClient;
43
+ private int billingClientIsConnected = 0;
44
+
45
+ private String googleVerifyEndpoint = "";
46
+ private String googleBid = "";
47
+
48
+ public Purchases(SubscriptionsPlugin plugin, BillingClient billingClient) {
49
+
50
+ this.billingClient = billingClient;
51
+ this.billingClient.startConnection(new BillingClientStateListener() {
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
+ }
60
+
61
+ @Override
62
+ public void onBillingServiceDisconnected() {
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();
69
+
70
+ }
71
+
72
+ public String echo(String value) {
73
+ Log.i("Echo", value);
74
+ return value;
75
+ }
76
+
77
+ public void setGoogleVerificationDetails(String googleVerifyEndpoint, String bid) {
78
+ this.googleVerifyEndpoint = googleVerifyEndpoint;
79
+ this.googleBid = bid;
80
+
81
+ Log.i("SET-VERIFY", "Verification values updated");
82
+ }
83
+
84
+ public void getProductDetails(String productIdentifier, PluginCall call) {
85
+
86
+ JSObject response = new JSObject();
87
+
88
+ if (billingClientIsConnected == 1) {
89
+
90
+ QueryProductDetailsParams.Product productToFind = QueryProductDetailsParams.Product.newBuilder()
91
+ .setProductId(productIdentifier)
92
+ .setProductType(BillingClient.ProductType.SUBS)
93
+ .build();
94
+
95
+ QueryProductDetailsParams queryProductDetailsParams =
96
+ QueryProductDetailsParams.newBuilder()
97
+ .setProductList(List.of(productToFind))
98
+ .build();
99
+
100
+ billingClient.queryProductDetailsAsync(
101
+ queryProductDetailsParams,
102
+ (billingResult, productDetailsList) -> {
103
+
104
+ try {
105
+
106
+ ProductDetails productDetails = productDetailsList.get(0);
107
+ String productId = productDetails.getProductId();
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);
113
+
114
+ List<ProductDetails.SubscriptionOfferDetails> subscriptionOfferDetails = productDetails.getSubscriptionOfferDetails();
115
+
116
+ String price = Objects.requireNonNull(subscriptionOfferDetails).get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
117
+
118
+ JSObject data = new JSObject();
119
+ data.put("productIdentifier", productId);
120
+ data.put("displayName", title);
121
+ data.put("description", desc);
122
+ data.put("price", price);
123
+
124
+ response.put("responseCode", 0);
125
+ response.put("responseMessage", "Successfully found the product details for given productIdentifier");
126
+ response.put("data", data);
127
+
128
+ } catch (Exception e) {
129
+ Log.e("Err", e.toString());
130
+ response.put("responseCode", 1);
131
+ response.put("responseMessage", "Could not find a product matching the given productIdentifier");
132
+ }
133
+
134
+ call.resolve(response);
135
+ }
136
+ );
137
+
138
+ } else if (billingClientIsConnected == 2) {
139
+
140
+ response.put("responseCode", 500);
141
+ response.put("responseMessage", "Android: BillingClient failed to initialise");
142
+ call.resolve(response);
143
+
144
+ } else {
145
+
146
+ response.put("responseCode", billingClientIsConnected);
147
+ response.put("responseMessage", "Android: BillingClient failed to initialise");
148
+
149
+ response.put("responseCode", 503);
150
+ response.put("responseMessage", "Android: BillingClient is still initialising");
151
+ call.resolve(response);
152
+
153
+ }
154
+ }
155
+
156
+ public void getLatestTransaction(String productIdentifier, PluginCall call) {
157
+
158
+ JSObject response = new JSObject();
159
+
160
+ if (billingClientIsConnected == 1) {
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
+ }
206
+
207
+ i++;
208
+
209
+ }
210
+
211
+ // If after looping through the list of purchase history records, no records are found to be associated with
212
+ // the given product identifier, return a response saying no transactions found
213
+ if (!found) {
214
+ response.put("responseCode", 3);
215
+ response.put("responseMessage", "No transaction for given productIdentifier, or it could not be verified");
216
+ }
217
+
218
+ call.resolve(response);
219
+
220
+ });
221
+
222
+ }
223
+
224
+ }
225
+
226
+ public void getCurrentEntitlements(PluginCall call) {
227
+
228
+ JSObject response = new JSObject();
229
+
230
+ if (billingClientIsConnected == 1) {
231
+
232
+ QueryPurchasesParams queryPurchasesParams =
233
+ QueryPurchasesParams.newBuilder()
234
+ .setProductType(BillingClient.ProductType.SUBS)
235
+ .build();
236
+
237
+ billingClient.queryPurchasesAsync(
238
+ queryPurchasesParams,
239
+ (billingResult, purchaseList) -> {
240
+
241
+ try {
242
+
243
+ int amountOfPurchases = purchaseList.size();
244
+
245
+ if (amountOfPurchases > 0) {
246
+
247
+ ArrayList<JSObject> entitlements = new ArrayList<>();
248
+ for (int i = 0; i < purchaseList.size(); i++) {
249
+
250
+ Purchase currentPurchase = purchaseList.get(i);
251
+
252
+ String expiryDate = this.getExpiryDateFromGoogle(currentPurchase.getProducts().get(0), currentPurchase.getPurchaseToken());
253
+ String orderId = currentPurchase.getOrderId();
254
+
255
+ String dateFormat = "dd-MM-yyyy hh:mm";
256
+ SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.getDefault());
257
+ Calendar calendar = Calendar.getInstance();
258
+ calendar.setTimeInMillis(Long.parseLong((String.valueOf(currentPurchase.getPurchaseTime()))));
259
+
260
+ entitlements.add(
261
+ new JSObject()
262
+ .put("productIdentifier", currentPurchase.getProducts().get(0))
263
+ .put("expiryDate", expiryDate)
264
+ .put("originalStartDate", simpleDateFormat.format(calendar.getTime()))
265
+ .put("originalId", orderId)
266
+ .put("transactionId", orderId)
267
+ .put("purchaseToken", currentPurchase.getPurchaseToken())
268
+ );
269
+ }
270
+
271
+ response.put("responseCode", 0);
272
+ response.put("responseMessage", "Successfully found all entitlements across all product types");
273
+ response.put("data", entitlements);
274
+
275
+
276
+ } else {
277
+ Log.i("No Purchases", "No active subscriptions found");
278
+ response.put("responseCode", 1);
279
+ response.put("responseMessage", "No entitlements were found");
280
+ }
281
+
282
+
283
+ call.resolve(response);
284
+
285
+ } catch (Exception e) {
286
+ Log.e("Error", e.toString());
287
+ response.put("responseCode", 2);
288
+ response.put("responseMessage", e.toString());
289
+ }
290
+
291
+ call.resolve(response);
292
+
293
+ }
294
+ );
295
+
296
+ }
297
+
298
+ }
299
+
300
+ public void purchaseProduct(String productIdentifier, String userId, PluginCall call) {
301
+
302
+ JSObject response = new JSObject();
303
+
304
+ if (billingClientIsConnected == 1) {
305
+
306
+ QueryProductDetailsParams.Product productToFind = QueryProductDetailsParams.Product.newBuilder()
307
+ .setProductId(productIdentifier)
308
+ .setProductType(BillingClient.ProductType.SUBS)
309
+ .build();
310
+
311
+ QueryProductDetailsParams queryProductDetailsParams =
312
+ QueryProductDetailsParams.newBuilder()
313
+ .setProductList(List.of(productToFind))
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 (userId != null && !userId.isEmpty()) {
333
+ paramsBuilder.setObfuscatedAccountId(userId);
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
+ }
353
+
354
+ }
355
+
356
+ private String getExpiryDateFromGoogle(String productIdentifier, String purchaseToken) {
357
+
358
+ try {
359
+
360
+ // Compile request to verify purchase token
361
+ URL obj = new URL(this.googleVerifyEndpoint + "?bid=" + this.googleBid + "&subId=" + productIdentifier + "&purchaseToken=" + purchaseToken);
362
+ HttpURLConnection con = (HttpURLConnection) obj.openConnection();
363
+ con.setRequestMethod("GET");
364
+
365
+ // Try to receive response from server
366
+ try (BufferedReader br = new BufferedReader(
367
+ new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) {
368
+
369
+ StringBuilder googleResponse = new StringBuilder();
370
+ String responseLine;
371
+ while ((responseLine = br.readLine()) != null) {
372
+ googleResponse.append(responseLine.trim());
373
+ Log.i("Response Line", responseLine);
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());
394
+ }
395
+
396
+ // If the method manages to each this far before already returning, just return null
397
+ // because something went wrong
398
+ return null;
399
+ }
400
+ }
@@ -0,0 +1,169 @@
1
+ package jok.purchases.capacitor;
2
+
3
+ import com.getcapacitor.JSObject;
4
+ import com.getcapacitor.Plugin;
5
+ import com.getcapacitor.PluginCall;
6
+ import com.getcapacitor.PluginMethod;
7
+ import com.getcapacitor.annotation.CapacitorPlugin;
8
+
9
+ import android.content.Intent;
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
+
19
+ @CapacitorPlugin(name = "Purchases")
20
+ public class PurchasesPlugin extends Plugin {
21
+
22
+ private Subscriptions implementation;
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();
46
+
47
+ billingClient.acknowledgePurchase(acknowledgePurchaseParams, billingResult1 -> {
48
+ Log.i("Purchase ack", currentPurchase.getOriginalJson());
49
+ billingResult1.getResponseCode();
50
+
51
+ response.put("successful", billingResult1.getResponseCode());
52
+
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
+
64
+ }
65
+ } else {
66
+ response.put("successful", false);
67
+ notifyListeners("ANDROID-PURCHASE-RESPONSE", response);
68
+ }
69
+
70
+ };
71
+
72
+ @Override
73
+ public void load() {
74
+
75
+ this.billingClient = BillingClient.newBuilder(getContext())
76
+ .setListener(purchasesUpdatedListener)
77
+ .enablePendingPurchases()
78
+ .build();
79
+ implementation = new Subscriptions(this, billingClient);
80
+
81
+ }
82
+
83
+ @PluginMethod
84
+ public void setGoogleVerificationDetails(PluginCall call) {
85
+ String googleVerifyEndpoint = call.getString("googleVerifyEndpoint");
86
+ String bid = call.getString("bid");
87
+
88
+ if(googleVerifyEndpoint != null && bid != null) {
89
+ implementation.setGoogleVerificationDetails(googleVerifyEndpoint, bid);
90
+ } else {
91
+ call.reject("Missing required parameters");
92
+ }
93
+ }
94
+
95
+ @PluginMethod
96
+ public void echo(PluginCall call) {
97
+ String value = call.getString("value");
98
+
99
+ JSObject ret = new JSObject();
100
+ ret.put("value", implementation.echo(value));
101
+ call.resolve(ret);
102
+ }
103
+
104
+ @PluginMethod
105
+ public void getProductDetails(PluginCall call) {
106
+
107
+ String productIdentifier = call.getString("productIdentifier");
108
+
109
+ if (productIdentifier == null) {
110
+ call.reject("Must provide a productID");
111
+ }
112
+
113
+ implementation.getProductDetails(productIdentifier, call);
114
+
115
+ }
116
+
117
+ @PluginMethod
118
+ public void purchaseProduct(PluginCall call) {
119
+
120
+ String productIdentifier = call.getString("productIdentifier");
121
+ String userId = call.getString("userId", "");
122
+
123
+ if(productIdentifier == null) {
124
+ call.reject("Must provide a productID");
125
+ }
126
+
127
+ implementation.purchaseProduct(productIdentifier, userId, call);
128
+
129
+ }
130
+
131
+ @PluginMethod
132
+ public void getLatestTransaction(PluginCall call) {
133
+
134
+ String productIdentifier = call.getString("productIdentifier");
135
+
136
+ if(productIdentifier == null) {
137
+ call.reject("Must provide a productID");
138
+ }
139
+
140
+ implementation.getLatestTransaction(productIdentifier, call);
141
+
142
+ }
143
+
144
+ @PluginMethod
145
+ public void getCurrentEntitlements(PluginCall call) {
146
+
147
+ implementation.getCurrentEntitlements(call);
148
+
149
+ }
150
+
151
+ @PluginMethod
152
+ public void manageSubscriptions(PluginCall call) {
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);
168
+ }
169
+ }
File without changes