@capivv/capacitor-sdk 0.1.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.
@@ -0,0 +1,708 @@
1
+ package com.capivv.capacitor
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.net.Uri
6
+ import com.android.billingclient.api.*
7
+ import com.getcapacitor.*
8
+ import com.getcapacitor.annotation.CapacitorPlugin
9
+ import com.getcapacitor.annotation.PluginMethod
10
+ import kotlinx.coroutines.*
11
+ import okhttp3.*
12
+ import okhttp3.MediaType.Companion.toMediaType
13
+ import okhttp3.RequestBody.Companion.toRequestBody
14
+ import org.json.JSONArray
15
+ import org.json.JSONObject
16
+ import java.io.IOException
17
+ import kotlin.coroutines.resume
18
+ import kotlin.coroutines.resumeWithException
19
+ import kotlin.coroutines.suspendCoroutine
20
+
21
+ @CapacitorPlugin(name = "Capivv")
22
+ class CapivvPlugin : Plugin() {
23
+
24
+ private var apiKey: String? = null
25
+ private var apiUrl: String = "https://api.capivv.com"
26
+ private var userId: String? = null
27
+ private var debug: Boolean = false
28
+
29
+ private var billingClient: BillingClient? = null
30
+ private val httpClient = OkHttpClient()
31
+ private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
32
+
33
+ private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
34
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
35
+ for (purchase in purchases) {
36
+ handlePurchase(purchase)
37
+ }
38
+ }
39
+ }
40
+
41
+ override fun load() {
42
+ super.load()
43
+ initBillingClient()
44
+ }
45
+
46
+ private fun initBillingClient() {
47
+ billingClient = BillingClient.newBuilder(context)
48
+ .setListener(purchasesUpdatedListener)
49
+ .enablePendingPurchases()
50
+ .build()
51
+
52
+ billingClient?.startConnection(object : BillingClientStateListener {
53
+ override fun onBillingSetupFinished(billingResult: BillingResult) {
54
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
55
+ log("Billing client connected")
56
+ } else {
57
+ log("Billing client setup failed: ${billingResult.debugMessage}")
58
+ }
59
+ }
60
+
61
+ override fun onBillingServiceDisconnected() {
62
+ log("Billing client disconnected")
63
+ }
64
+ })
65
+ }
66
+
67
+ private fun handlePurchase(purchase: Purchase) {
68
+ if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
69
+ scope.launch {
70
+ try {
71
+ userId?.let { uid ->
72
+ verifyPurchase(purchase, uid)
73
+ }
74
+
75
+ // Acknowledge if not already
76
+ if (!purchase.isAcknowledged) {
77
+ val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
78
+ .setPurchaseToken(purchase.purchaseToken)
79
+ .build()
80
+
81
+ billingClient?.acknowledgePurchase(acknowledgePurchaseParams) { result ->
82
+ if (result.responseCode == BillingClient.BillingResponseCode.OK) {
83
+ log("Purchase acknowledged")
84
+ }
85
+ }
86
+ }
87
+
88
+ val transactionData = mapPurchaseToTransaction(purchase)
89
+ notifyListeners("purchaseCompleted", JSObject().apply {
90
+ put("transaction", transactionData)
91
+ })
92
+ } catch (e: Exception) {
93
+ log("Error handling purchase: ${e.message}")
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ // MARK: - Configuration
100
+
101
+ @PluginMethod
102
+ fun configure(call: PluginCall) {
103
+ val key = call.getString("apiKey")
104
+ if (key == null) {
105
+ call.reject("apiKey is required")
106
+ return
107
+ }
108
+
109
+ apiKey = key
110
+ call.getString("apiUrl")?.let { apiUrl = it }
111
+ debug = call.getBoolean("debug") ?: false
112
+
113
+ log("Configured with API URL: $apiUrl")
114
+ call.resolve()
115
+ }
116
+
117
+ // MARK: - User Management
118
+
119
+ @PluginMethod
120
+ fun identify(call: PluginCall) {
121
+ if (apiKey == null) {
122
+ call.reject("Not configured. Call configure() first.")
123
+ return
124
+ }
125
+
126
+ val uid = call.getString("userId")
127
+ if (uid == null) {
128
+ call.reject("userId is required")
129
+ return
130
+ }
131
+
132
+ userId = uid
133
+ val attributes = call.getObject("attributes") ?: JSObject()
134
+
135
+ scope.launch {
136
+ try {
137
+ val body = JSONObject().apply {
138
+ put("attributes", attributes)
139
+ }
140
+
141
+ val response = apiRequest("POST", "/v1/users/$uid/login", body)
142
+
143
+ call.resolve(JSObject().apply {
144
+ put("userId", uid)
145
+ put("entitlements", response.optJSONArray("entitlements") ?: JSONArray())
146
+ put("originalPurchaseDate", response.optString("original_purchase_date", null))
147
+ put("latestPurchaseDate", response.optString("latest_purchase_date", null))
148
+ })
149
+ } catch (e: Exception) {
150
+ call.reject("Failed to identify user: ${e.message}")
151
+ }
152
+ }
153
+ }
154
+
155
+ @PluginMethod
156
+ fun logout(call: PluginCall) {
157
+ userId = null
158
+ call.resolve()
159
+ }
160
+
161
+ @PluginMethod
162
+ fun getUserInfo(call: PluginCall) {
163
+ val uid = userId
164
+ if (uid == null) {
165
+ call.reject("Not identified. Call identify() first.")
166
+ return
167
+ }
168
+
169
+ scope.launch {
170
+ try {
171
+ val response = apiRequest("GET", "/v1/users/$uid/entitlements")
172
+
173
+ call.resolve(JSObject().apply {
174
+ put("userId", uid)
175
+ put("entitlements", response.optJSONArray("entitlements") ?: JSONArray())
176
+ })
177
+ } catch (e: Exception) {
178
+ call.reject("Failed to get user info: ${e.message}")
179
+ }
180
+ }
181
+ }
182
+
183
+ // MARK: - Billing
184
+
185
+ @PluginMethod
186
+ fun isBillingSupported(call: PluginCall) {
187
+ val isReady = billingClient?.isReady ?: false
188
+ call.resolve(JSObject().apply {
189
+ put("isSupported", isReady)
190
+ })
191
+ }
192
+
193
+ // MARK: - Products & Offerings
194
+
195
+ @PluginMethod
196
+ fun getOfferings(call: PluginCall) {
197
+ if (apiKey == null) {
198
+ call.reject("Not configured. Call configure() first.")
199
+ return
200
+ }
201
+
202
+ scope.launch {
203
+ try {
204
+ val response = apiRequest("GET", "/v1/offerings")
205
+ val offerings = response.optJSONArray("offerings") ?: JSONArray()
206
+
207
+ val enrichedOfferings = JSONArray()
208
+
209
+ for (i in 0 until offerings.length()) {
210
+ val offering = offerings.getJSONObject(i)
211
+ val products = offering.optJSONArray("products") ?: JSONArray()
212
+
213
+ val productIds = mutableListOf<String>()
214
+ for (j in 0 until products.length()) {
215
+ val product = products.getJSONObject(j)
216
+ product.optString("store_product_id")?.let { productIds.add(it) }
217
+ }
218
+
219
+ val enrichedProducts = queryProducts(productIds)
220
+
221
+ val enrichedOffering = JSONObject(offering.toString())
222
+ enrichedOffering.put("products", enrichedProducts)
223
+ enrichedOfferings.put(enrichedOffering)
224
+ }
225
+
226
+ call.resolve(JSObject().apply {
227
+ put("offerings", enrichedOfferings)
228
+ })
229
+ } catch (e: Exception) {
230
+ call.reject("Failed to get offerings: ${e.message}")
231
+ }
232
+ }
233
+ }
234
+
235
+ @PluginMethod
236
+ fun getProduct(call: PluginCall) {
237
+ val productIdentifier = call.getString("productIdentifier")
238
+ if (productIdentifier == null) {
239
+ call.reject("productIdentifier is required")
240
+ return
241
+ }
242
+
243
+ scope.launch {
244
+ try {
245
+ val products = queryProducts(listOf(productIdentifier))
246
+
247
+ if (products.length() == 0) {
248
+ call.reject("Product not found: $productIdentifier")
249
+ return@launch
250
+ }
251
+
252
+ call.resolve(JSObject().apply {
253
+ put("product", products.getJSONObject(0))
254
+ })
255
+ } catch (e: Exception) {
256
+ call.reject("Failed to get product: ${e.message}")
257
+ }
258
+ }
259
+ }
260
+
261
+ @PluginMethod
262
+ fun getProducts(call: PluginCall) {
263
+ val productIdentifiers = call.getArray("productIdentifiers")?.toList<String>()
264
+ if (productIdentifiers == null) {
265
+ call.reject("productIdentifiers is required")
266
+ return
267
+ }
268
+
269
+ scope.launch {
270
+ try {
271
+ val products = queryProducts(productIdentifiers)
272
+
273
+ call.resolve(JSObject().apply {
274
+ put("products", products)
275
+ })
276
+ } catch (e: Exception) {
277
+ call.reject("Failed to get products: ${e.message}")
278
+ }
279
+ }
280
+ }
281
+
282
+ private suspend fun queryProducts(productIds: List<String>): JSONArray = suspendCoroutine { cont ->
283
+ val client = billingClient
284
+ if (client == null || !client.isReady) {
285
+ cont.resume(JSONArray())
286
+ return@suspendCoroutine
287
+ }
288
+
289
+ val productList = productIds.map { productId ->
290
+ QueryProductDetailsParams.Product.newBuilder()
291
+ .setProductId(productId)
292
+ .setProductType(BillingClient.ProductType.SUBS)
293
+ .build()
294
+ }
295
+
296
+ val params = QueryProductDetailsParams.newBuilder()
297
+ .setProductList(productList)
298
+ .build()
299
+
300
+ client.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
301
+ if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
302
+ val products = JSONArray()
303
+ for (details in productDetailsList) {
304
+ products.put(mapProductDetails(details))
305
+ }
306
+ cont.resume(products)
307
+ } else {
308
+ cont.resume(JSONArray())
309
+ }
310
+ }
311
+ }
312
+
313
+ // MARK: - Purchases
314
+
315
+ @PluginMethod
316
+ fun purchase(call: PluginCall) {
317
+ if (apiKey == null) {
318
+ call.reject("Not configured. Call configure() first.")
319
+ return
320
+ }
321
+
322
+ if (userId == null) {
323
+ call.reject("Not identified. Call identify() first.")
324
+ return
325
+ }
326
+
327
+ val productIdentifier = call.getString("productIdentifier")
328
+ if (productIdentifier == null) {
329
+ call.reject("productIdentifier is required")
330
+ return
331
+ }
332
+
333
+ val client = billingClient
334
+ if (client == null || !client.isReady) {
335
+ call.resolve(JSObject().apply {
336
+ put("success", false)
337
+ put("error", "Billing client not ready")
338
+ })
339
+ return
340
+ }
341
+
342
+ scope.launch {
343
+ try {
344
+ val productList = listOf(
345
+ QueryProductDetailsParams.Product.newBuilder()
346
+ .setProductId(productIdentifier)
347
+ .setProductType(BillingClient.ProductType.SUBS)
348
+ .build()
349
+ )
350
+
351
+ val params = QueryProductDetailsParams.newBuilder()
352
+ .setProductList(productList)
353
+ .build()
354
+
355
+ client.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
356
+ if (billingResult.responseCode != BillingClient.BillingResponseCode.OK || productDetailsList.isEmpty()) {
357
+ call.resolve(JSObject().apply {
358
+ put("success", false)
359
+ put("error", "Product not found: $productIdentifier")
360
+ })
361
+ return@queryProductDetailsAsync
362
+ }
363
+
364
+ val productDetails = productDetailsList[0]
365
+
366
+ // Get the offer token for subscriptions
367
+ val offerToken = productDetails.subscriptionOfferDetails?.firstOrNull()?.offerToken
368
+
369
+ val productDetailsParamsList = listOf(
370
+ BillingFlowParams.ProductDetailsParams.newBuilder()
371
+ .setProductDetails(productDetails)
372
+ .apply {
373
+ offerToken?.let { setOfferToken(it) }
374
+ }
375
+ .build()
376
+ )
377
+
378
+ val billingFlowParams = BillingFlowParams.newBuilder()
379
+ .setProductDetailsParamsList(productDetailsParamsList)
380
+ .build()
381
+
382
+ val launchResult = client.launchBillingFlow(activity, billingFlowParams)
383
+
384
+ if (launchResult.responseCode != BillingClient.BillingResponseCode.OK) {
385
+ call.resolve(JSObject().apply {
386
+ put("success", false)
387
+ put("error", "Failed to launch billing flow: ${launchResult.debugMessage}")
388
+ })
389
+ }
390
+ // Purchase result will be handled by purchasesUpdatedListener
391
+ // We don't resolve here - the listener will handle it
392
+ }
393
+ } catch (e: Exception) {
394
+ call.resolve(JSObject().apply {
395
+ put("success", false)
396
+ put("error", e.message)
397
+ })
398
+ }
399
+ }
400
+ }
401
+
402
+ @PluginMethod
403
+ fun restorePurchases(call: PluginCall) {
404
+ val uid = userId
405
+ if (uid == null) {
406
+ call.reject("Not identified. Call identify() first.")
407
+ return
408
+ }
409
+
410
+ val client = billingClient
411
+ if (client == null || !client.isReady) {
412
+ call.reject("Billing client not ready")
413
+ return
414
+ }
415
+
416
+ scope.launch {
417
+ try {
418
+ val params = QueryPurchasesParams.newBuilder()
419
+ .setProductType(BillingClient.ProductType.SUBS)
420
+ .build()
421
+
422
+ val purchasesResult = client.queryPurchasesAsync(params)
423
+
424
+ if (purchasesResult.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
425
+ for (purchase in purchasesResult.purchasesList) {
426
+ if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
427
+ verifyPurchase(purchase, uid)
428
+ }
429
+ }
430
+ }
431
+
432
+ // Fetch updated entitlements
433
+ val response = apiRequest("GET", "/v1/users/$uid/entitlements")
434
+ val entitlements = response.optJSONArray("entitlements") ?: JSONArray()
435
+
436
+ notifyListeners("entitlementsUpdated", JSObject().apply {
437
+ put("entitlements", entitlements)
438
+ })
439
+
440
+ call.resolve(JSObject().apply {
441
+ put("entitlements", entitlements)
442
+ })
443
+ } catch (e: Exception) {
444
+ call.reject("Failed to restore purchases: ${e.message}")
445
+ }
446
+ }
447
+ }
448
+
449
+ // MARK: - Entitlements
450
+
451
+ @PluginMethod
452
+ fun checkEntitlement(call: PluginCall) {
453
+ val uid = userId
454
+ if (uid == null) {
455
+ call.reject("Not identified. Call identify() first.")
456
+ return
457
+ }
458
+
459
+ val entitlementIdentifier = call.getString("entitlementIdentifier")
460
+ if (entitlementIdentifier == null) {
461
+ call.reject("entitlementIdentifier is required")
462
+ return
463
+ }
464
+
465
+ scope.launch {
466
+ try {
467
+ val response = apiRequest("GET", "/v1/users/$uid/entitlements")
468
+ val entitlements = response.optJSONArray("entitlements") ?: JSONArray()
469
+
470
+ for (i in 0 until entitlements.length()) {
471
+ val entitlement = entitlements.getJSONObject(i)
472
+ if (entitlement.optString("identifier") == entitlementIdentifier) {
473
+ val isActive = entitlement.optBoolean("is_active", false)
474
+ call.resolve(JSObject().apply {
475
+ put("hasAccess", isActive)
476
+ put("entitlement", entitlement.toString())
477
+ })
478
+ return@launch
479
+ }
480
+ }
481
+
482
+ call.resolve(JSObject().apply {
483
+ put("hasAccess", false)
484
+ })
485
+ } catch (e: Exception) {
486
+ call.reject("Failed to check entitlement: ${e.message}")
487
+ }
488
+ }
489
+ }
490
+
491
+ @PluginMethod
492
+ fun getEntitlements(call: PluginCall) {
493
+ val uid = userId
494
+ if (uid == null) {
495
+ call.reject("Not identified. Call identify() first.")
496
+ return
497
+ }
498
+
499
+ scope.launch {
500
+ try {
501
+ val response = apiRequest("GET", "/v1/users/$uid/entitlements")
502
+
503
+ call.resolve(JSObject().apply {
504
+ put("entitlements", response.optJSONArray("entitlements") ?: JSONArray())
505
+ })
506
+ } catch (e: Exception) {
507
+ call.reject("Failed to get entitlements: ${e.message}")
508
+ }
509
+ }
510
+ }
511
+
512
+ @PluginMethod
513
+ fun syncPurchases(call: PluginCall) {
514
+ val uid = userId
515
+ if (uid == null) {
516
+ call.reject("Not identified. Call identify() first.")
517
+ return
518
+ }
519
+
520
+ val client = billingClient
521
+ if (client == null || !client.isReady) {
522
+ // If billing client is not ready, just fetch from server
523
+ scope.launch {
524
+ try {
525
+ val response = apiRequest("GET", "/v1/users/$uid/entitlements")
526
+
527
+ call.resolve(JSObject().apply {
528
+ put("entitlements", response.optJSONArray("entitlements") ?: JSONArray())
529
+ })
530
+ } catch (e: Exception) {
531
+ call.reject("Failed to sync purchases: ${e.message}")
532
+ }
533
+ }
534
+ return
535
+ }
536
+
537
+ scope.launch {
538
+ try {
539
+ val params = QueryPurchasesParams.newBuilder()
540
+ .setProductType(BillingClient.ProductType.SUBS)
541
+ .build()
542
+
543
+ val purchasesResult = client.queryPurchasesAsync(params)
544
+
545
+ if (purchasesResult.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
546
+ for (purchase in purchasesResult.purchasesList) {
547
+ if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
548
+ verifyPurchase(purchase, uid)
549
+ }
550
+ }
551
+ }
552
+
553
+ // Fetch updated entitlements
554
+ val response = apiRequest("GET", "/v1/users/$uid/entitlements")
555
+ val entitlements = response.optJSONArray("entitlements") ?: JSONArray()
556
+
557
+ notifyListeners("entitlementsUpdated", JSObject().apply {
558
+ put("entitlements", entitlements)
559
+ })
560
+
561
+ call.resolve(JSObject().apply {
562
+ put("entitlements", entitlements)
563
+ })
564
+ } catch (e: Exception) {
565
+ call.reject("Failed to sync purchases: ${e.message}")
566
+ }
567
+ }
568
+ }
569
+
570
+ @PluginMethod
571
+ fun manageSubscriptions(call: PluginCall) {
572
+ try {
573
+ val intent = Intent(Intent.ACTION_VIEW).apply {
574
+ data = Uri.parse("https://play.google.com/store/account/subscriptions")
575
+ }
576
+ activity.startActivity(intent)
577
+ call.resolve()
578
+ } catch (e: Exception) {
579
+ call.reject("Failed to open subscription management: ${e.message}")
580
+ }
581
+ }
582
+
583
+ // MARK: - Private Helpers
584
+
585
+ private suspend fun verifyPurchase(purchase: Purchase, userId: String) {
586
+ try {
587
+ val body = JSONObject().apply {
588
+ put("user_id", userId)
589
+ put("purchase_token", purchase.purchaseToken)
590
+ put("product_ids", JSONArray(purchase.products))
591
+ put("order_id", purchase.orderId)
592
+ }
593
+
594
+ apiRequest("POST", "/v1/purchases/google/verify", body)
595
+ log("Purchase verified: ${purchase.orderId}")
596
+ } catch (e: Exception) {
597
+ log("Failed to verify purchase: ${e.message}")
598
+ }
599
+ }
600
+
601
+ private fun mapProductDetails(details: ProductDetails): JSONObject {
602
+ val result = JSONObject().apply {
603
+ put("identifier", details.productId)
604
+ put("title", details.title)
605
+ put("description", details.description)
606
+ }
607
+
608
+ details.subscriptionOfferDetails?.firstOrNull()?.let { offer ->
609
+ val pricingPhase = offer.pricingPhases.pricingPhaseList.firstOrNull()
610
+ pricingPhase?.let { phase ->
611
+ result.put("priceString", phase.formattedPrice)
612
+ result.put("priceAmountMicros", phase.priceAmountMicros)
613
+ result.put("currencyCode", phase.priceCurrencyCode)
614
+ result.put("productType", "SUBSCRIPTION")
615
+ result.put("subscriptionPeriod", phase.billingPeriod)
616
+ }
617
+
618
+ // Check for free trial
619
+ offer.pricingPhases.pricingPhaseList.find { it.priceAmountMicros == 0L }?.let { trial ->
620
+ result.put("trialPeriod", trial.billingPeriod)
621
+ }
622
+ }
623
+
624
+ details.oneTimePurchaseOfferDetails?.let { offer ->
625
+ result.put("priceString", offer.formattedPrice)
626
+ result.put("priceAmountMicros", offer.priceAmountMicros)
627
+ result.put("currencyCode", offer.priceCurrencyCode)
628
+ result.put("productType", "INAPP")
629
+ }
630
+
631
+ return result
632
+ }
633
+
634
+ private fun mapPurchaseToTransaction(purchase: Purchase): JSObject {
635
+ return JSObject().apply {
636
+ put("transactionId", purchase.orderId ?: "")
637
+ put("productIdentifier", purchase.products.firstOrNull() ?: "")
638
+ put("purchaseDate", java.time.Instant.ofEpochMilli(purchase.purchaseTime).toString())
639
+ put("isAcknowledged", purchase.isAcknowledged)
640
+ put("state", when (purchase.purchaseState) {
641
+ Purchase.PurchaseState.PURCHASED -> "PURCHASED"
642
+ Purchase.PurchaseState.PENDING -> "PENDING"
643
+ else -> "FAILED"
644
+ })
645
+ put("purchaseToken", purchase.purchaseToken)
646
+ }
647
+ }
648
+
649
+ private suspend fun apiRequest(method: String, path: String, body: JSONObject? = null): JSONObject =
650
+ suspendCoroutine { cont ->
651
+ val key = apiKey
652
+ if (key == null) {
653
+ cont.resumeWithException(Exception("Not configured"))
654
+ return@suspendCoroutine
655
+ }
656
+
657
+ val url = "$apiUrl$path"
658
+
659
+ val requestBuilder = Request.Builder()
660
+ .url(url)
661
+ .addHeader("Content-Type", "application/json")
662
+ .addHeader("X-Capivv-Api-Key", key)
663
+
664
+ when (method) {
665
+ "GET" -> requestBuilder.get()
666
+ "POST" -> requestBuilder.post(
667
+ (body?.toString() ?: "{}").toRequestBody("application/json".toMediaType())
668
+ )
669
+ "PUT" -> requestBuilder.put(
670
+ (body?.toString() ?: "{}").toRequestBody("application/json".toMediaType())
671
+ )
672
+ "DELETE" -> requestBuilder.delete()
673
+ }
674
+
675
+ httpClient.newCall(requestBuilder.build()).enqueue(object : Callback {
676
+ override fun onFailure(call: Call, e: IOException) {
677
+ cont.resumeWithException(e)
678
+ }
679
+
680
+ override fun onResponse(call: Call, response: Response) {
681
+ val responseBody = response.body?.string() ?: "{}"
682
+
683
+ if (!response.isSuccessful) {
684
+ cont.resumeWithException(Exception("API error (${response.code}): $responseBody"))
685
+ return
686
+ }
687
+
688
+ try {
689
+ cont.resume(JSONObject(responseBody))
690
+ } catch (e: Exception) {
691
+ cont.resume(JSONObject())
692
+ }
693
+ }
694
+ })
695
+ }
696
+
697
+ private fun log(message: String) {
698
+ if (debug) {
699
+ android.util.Log.d("Capivv", message)
700
+ }
701
+ }
702
+
703
+ override fun handleOnDestroy() {
704
+ super.handleOnDestroy()
705
+ billingClient?.endConnection()
706
+ scope.cancel()
707
+ }
708
+ }
@@ -0,0 +1 @@
1
+ export {};