@purchasely/cordova-plugin-purchasely 2.3.2 → 4.2.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,925 @@
1
+ package cordova.plugin.purchasely
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.net.Uri
6
+ import android.os.Build
7
+ import android.util.Log
8
+ import io.purchasely.billing.Store
9
+ import io.purchasely.ext.Attribute
10
+ import io.purchasely.ext.DistributionType
11
+ import io.purchasely.ext.EventListener
12
+ import io.purchasely.ext.LogLevel
13
+ import io.purchasely.ext.PLYAppTechnology
14
+ import io.purchasely.ext.PLYCompletionHandler
15
+ import io.purchasely.ext.PLYEvent
16
+ import io.purchasely.ext.PLYPresentation
17
+ import io.purchasely.ext.PLYPresentationAction
18
+ import io.purchasely.ext.PLYPresentationType
19
+ import io.purchasely.ext.PLYPresentationViewProperties
20
+ import io.purchasely.ext.PLYProductViewResult
21
+ import io.purchasely.ext.PLYRunningMode
22
+ import io.purchasely.ext.PLYRunningMode.Full
23
+ import io.purchasely.ext.PLYRunningMode.PaywallObserver
24
+ import io.purchasely.ext.PlanListener
25
+ import io.purchasely.ext.ProductListener
26
+ import io.purchasely.ext.ProductsListener
27
+ import io.purchasely.ext.PurchaseListener
28
+ import io.purchasely.ext.Purchasely
29
+ import io.purchasely.ext.State
30
+ import io.purchasely.ext.StoreType
31
+ import io.purchasely.ext.SubscriptionsListener
32
+ import io.purchasely.models.PLYError
33
+ import io.purchasely.models.PLYPlan
34
+ import io.purchasely.models.PLYPresentationPlan
35
+ import io.purchasely.models.PLYProduct
36
+ import io.purchasely.models.PLYSubscriptionData
37
+ import io.purchasely.views.presentation.PLYThemeMode
38
+ import org.apache.cordova.CallbackContext
39
+ import org.apache.cordova.CordovaInterface
40
+ import org.apache.cordova.CordovaPlugin
41
+ import org.apache.cordova.PluginResult
42
+ import org.json.JSONArray
43
+ import org.json.JSONException
44
+ import org.json.JSONObject
45
+ import java.lang.ref.WeakReference
46
+ import java.text.SimpleDateFormat
47
+ import java.util.Calendar
48
+ import java.util.Date
49
+ import java.util.Locale
50
+ import java.util.TimeZone
51
+
52
+ /**
53
+ * This class echoes a string called from JavaScript.
54
+ */
55
+ class PurchaselyPlugin : CordovaPlugin() {
56
+ private var paywallActionHandler: PLYCompletionHandler? = null
57
+ private var paywallAction: PLYPresentationAction? = null
58
+
59
+ override fun execute(
60
+ action: String,
61
+ args: JSONArray,
62
+ callbackContext: CallbackContext
63
+ ): Boolean {
64
+ try {
65
+ when (action) {
66
+ "start" -> start(
67
+ getStringFromJson(args.getString(0)),
68
+ args.getJSONArray(1),
69
+ args.getBoolean(2),
70
+ getStringFromJson(args.getString(3)),
71
+ args.getInt(4),
72
+ args.getInt(5),
73
+ getStringFromJson(args.getString(6)),
74
+ callbackContext
75
+ )
76
+
77
+ "close" -> close()
78
+ "addEventsListener" -> addEventsListener(callbackContext)
79
+ "removeEventsListener" -> removeEventsListener()
80
+ "getAnonymousUserId" -> getAnonymousUserId(callbackContext)
81
+ "userLogin" -> userLogin(getStringFromJson(args.getString(0)), callbackContext)
82
+ "userLogout" -> userLogout()
83
+ "setLanguage" -> setLanguage(getStringFromJson(args.getString(0)))
84
+ "setLogLevel" -> setLogLevel(args.getInt(0))
85
+ "setThemeMode" -> setThemeMode(args.getInt(0))
86
+ "setAttribute" -> setAttribute(args.getInt(0), getStringFromJson(args.getString(1)))
87
+ "setDefaultPresentationResultHandler" -> setDefaultPresentationResultHandler(
88
+ callbackContext
89
+ )
90
+
91
+ "purchasedSubscription" -> purchasedSubscription(callbackContext)
92
+ "readyToOpenDeeplink" -> readyToOpenDeeplink(args.getBoolean(0))
93
+ "synchronize" -> synchronize()
94
+ "presentPresentationWithIdentifier" -> presentPresentationWithIdentifier(
95
+ getStringFromJson(args.getString(0)),
96
+ getStringFromJson(args.getString(1)),
97
+ args.getBoolean(2),
98
+ callbackContext
99
+ )
100
+
101
+ "presentPresentationForPlacement" -> presentPresentationForPlacement(
102
+ getStringFromJson(args.getString(0)),
103
+ getStringFromJson(args.getString(1)),
104
+ args.getBoolean(2),
105
+ callbackContext
106
+ )
107
+
108
+ "presentProductWithIdentifier" -> presentProductWithIdentifier(
109
+ getStringFromJson(args.getString(0)),
110
+ getStringFromJson(args.getString(1)),
111
+ getStringFromJson(args.getString(2)),
112
+ args.getBoolean(3),
113
+ callbackContext
114
+ )
115
+
116
+ "presentPlanWithIdentifier" -> presentPlanWithIdentifier(
117
+ getStringFromJson(args.getString(0)),
118
+ getStringFromJson(args.getString(1)),
119
+ getStringFromJson(args.getString(2)),
120
+ args.getBoolean(3),
121
+ callbackContext
122
+ )
123
+ "fetchPresentation" -> fetchPresentation(
124
+ getStringFromJson(args.getString(0)),
125
+ getStringFromJson(args.getString(1)),
126
+ getStringFromJson(args.getString(2)),
127
+ callbackContext
128
+ )
129
+ "presentPresentation" -> presentPresentation(
130
+ args.getJSONObject(0),
131
+ args.getBoolean(1),
132
+ getStringFromJson(args.getString(2)),
133
+ callbackContext
134
+ )
135
+ "presentSubscriptions" -> presentSubscriptions()
136
+ "restoreAllProducts" -> restoreAllProducts(callbackContext)
137
+ "silentRestoreAllProducts" -> restoreAllProducts(callbackContext)
138
+ "userSubscriptions" -> userSubscriptions(callbackContext)
139
+ "isDeeplinkHandled" -> isDeeplinkHandled(
140
+ getStringFromJson(args.getString(0)),
141
+ callbackContext
142
+ )
143
+
144
+ "allProducts" -> allProducts(callbackContext)
145
+ "productWithIdentifier" -> productWithIdentifier(
146
+ getStringFromJson(args.getString(0)),
147
+ callbackContext
148
+ )
149
+
150
+ "planWithIdentifier" -> planWithIdentifier(
151
+ getStringFromJson(args.getString(0)),
152
+ callbackContext
153
+ )
154
+
155
+ "purchaseWithPlanVendorId" -> purchaseWithPlanVendorId(
156
+ getStringFromJson(args.getString(0)),
157
+ getStringFromJson(args.getString(1)),
158
+ getStringFromJson(args.getString(2)),
159
+ callbackContext
160
+ )
161
+ "setPaywallActionInterceptor" -> setPaywallActionInterceptor(callbackContext)
162
+ "onProcessAction" -> onProcessAction(args.getBoolean(0))
163
+ "closePresentation" -> closePresentation(callbackContext)
164
+ "hidePresentation" -> hidePresentation()
165
+ "showPresentation" -> showPresentation()
166
+ "userDidConsumeSubscriptionContent" -> userDidConsumeSubscriptionContent()
167
+ "setUserAttributeWithString" -> setUserAttributeWithString(getStringFromJson(args.getString(0)), getStringFromJson(args.getString(1)))
168
+ "setUserAttributeWithBoolean" -> setUserAttributeWithBoolean(getStringFromJson(args.getString(0)), args.getBoolean(1))
169
+ "setUserAttributeWithInt" -> setUserAttributeWithInt(getStringFromJson(args.getString(0)), args.getInt(1))
170
+ "setUserAttributeWithDouble" -> setUserAttributeWithDouble(getStringFromJson(args.getString(0)), args.getDouble(1))
171
+ "setUserAttributeWithDate" -> setUserAttributeWithDate(getStringFromJson(args.getString(0)), getStringFromJson(args.getString(1)))
172
+ "userAttribute" -> userAttribute(getStringFromJson(args.getString(0)), callbackContext)
173
+ "clearUserAttribute" -> clearUserAttribute(getStringFromJson(args.getString(0)))
174
+ "clearUserAttributes" -> clearUserAttributes()
175
+ "isEligibleForIntroOffer" -> isEligibleForIntroOffer(getStringFromJson(args.getString(0)), callbackContext)
176
+ "signPromotionalOffer" -> signPromotionalOffer(getStringFromJson(args.getString(0)), getStringFromJson(args.getString(1)), callbackContext)
177
+ else -> return false
178
+ }
179
+ } catch (e: JSONException) {
180
+ Log.e("Purchasely", String.format("Error executing action %s", action), e)
181
+ }
182
+ return true
183
+ }
184
+
185
+ private fun getStringFromJson(value: String?): String? {
186
+ return if (value == null || value == "null" || value.isEmpty()) {
187
+ null
188
+ } else value
189
+ }
190
+
191
+ private fun start(
192
+ apiKey: String?,
193
+ stores: JSONArray,
194
+ storeKit1: Boolean,
195
+ userId: String?,
196
+ logLevel: Int,
197
+ runningMode: Int,
198
+ cordovaSdkVersion: String?,
199
+ callbackContext: CallbackContext
200
+ ) {
201
+
202
+ if(apiKey == null) {
203
+ callbackContext.error("API Key is null")
204
+ return
205
+ }
206
+
207
+ val list = ArrayList<String>()
208
+ for (i in 0 until stores.length()) {
209
+ try {
210
+ list.add(stores.getString(i))
211
+ } catch (e: JSONException) {
212
+ Log.e("Purchasely", "Error in store array" + e.message, e)
213
+ }
214
+ }
215
+ val storesInstances = getStoresInstances(list)
216
+ var plyRunningMode: PLYRunningMode = Full
217
+ if (runningMode == runningModePaywallObserver) plyRunningMode = PaywallObserver
218
+ Purchasely.Builder(cordova.context)
219
+ .apiKey(apiKey)
220
+ .stores(storesInstances)
221
+ .userId(userId)
222
+ .runningMode(plyRunningMode)
223
+ .logLevel(LogLevel.values()[logLevel])
224
+ .build()
225
+ Purchasely.sdkBridgeVersion = cordovaSdkVersion
226
+ Purchasely.appTechnology = PLYAppTechnology.CORDOVA
227
+ Purchasely.start { isConfigured: Boolean, error: PLYError? ->
228
+ if (isConfigured) {
229
+ callbackContext.success()
230
+ } else {
231
+ callbackContext.error(
232
+ if (error != null) error.message else "Purchasely SDK not configured"
233
+ )
234
+ }
235
+ }
236
+ }
237
+
238
+ private fun getStoresInstances(stores: List<String>): ArrayList<Store> {
239
+ val result = ArrayList<Store>()
240
+ if (stores.contains("Google") && Package.getPackage("io.purchasely.google") != null) {
241
+ try {
242
+ result.add(Class.forName("io.purchasely.google.GoogleStore").newInstance() as Store)
243
+ } catch (e: Exception) {
244
+ Log.e("Purchasely", "Google Store not found :" + e.message, e)
245
+ }
246
+ }
247
+ if (stores.contains("Huawei") && Package.getPackage("io.purchasely.huawei") != null) {
248
+ try {
249
+ result.add(Class.forName("io.purchasely.huawei.HuaweiStore").newInstance() as Store)
250
+ } catch (e: Exception) {
251
+ Log.e("Purchasely", "Huawei Store not found :" + e.message, e)
252
+ }
253
+ }
254
+ if (stores.contains("Amazon") && Package.getPackage("io.purchasely.amazon") != null) {
255
+ try {
256
+ result.add(Class.forName("io.purchasely.amazon.AmazonStore").newInstance() as Store)
257
+ } catch (e: Exception) {
258
+ Log.e("Purchasely", "Amazon Store not found :" + e.message, e)
259
+ }
260
+ }
261
+ return result
262
+ }
263
+
264
+ private fun close() {
265
+ defaultCallback = null
266
+ purchaseCallback = null
267
+ paywallActionHandler = null
268
+ productActivity = null
269
+ Purchasely.close()
270
+ }
271
+
272
+ private fun addEventsListener(callbackContext: CallbackContext) {
273
+ eventsCallback = callbackContext
274
+ Purchasely.eventListener = object: EventListener {
275
+ override fun onEvent(event: PLYEvent) {
276
+ val map = HashMap<String?, Any?>()
277
+ map["name"] = event.name
278
+ map["properties"] = event.properties.toMap()
279
+ val pluginResult = PluginResult(PluginResult.Status.OK, JSONObject(map))
280
+ pluginResult.keepCallback = true
281
+ eventsCallback?.sendPluginResult(pluginResult)
282
+ }
283
+ }
284
+ }
285
+
286
+ private fun removeEventsListener() {
287
+ eventsCallback = null
288
+ Purchasely.eventListener = null
289
+ }
290
+
291
+ private fun getAnonymousUserId(callbackContext: CallbackContext) {
292
+ callbackContext.success(Purchasely.anonymousUserId)
293
+ }
294
+
295
+ private fun userLogin(userId: String?, callbackContext: CallbackContext) {
296
+ if(userId == null) {
297
+ callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, false))
298
+ return
299
+ }
300
+
301
+ Purchasely.userLogin(userId) { refresh ->
302
+ callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, refresh))
303
+ }
304
+ }
305
+
306
+ private fun userLogout() {
307
+ Purchasely.userLogout()
308
+ }
309
+
310
+ private fun setLogLevel(logLevel: Int) {
311
+ Purchasely.logLevel = LogLevel.values()[logLevel]
312
+ }
313
+
314
+ private fun setLanguage(language: String?) {
315
+ try {
316
+ Purchasely.language = Locale(language ?: "en")
317
+ } catch (e: Exception) {
318
+ Purchasely.language = Locale.getDefault()
319
+ }
320
+ }
321
+
322
+ private fun readyToOpenDeeplink(isReadyToPurchase: Boolean) {
323
+ Purchasely.readyToOpenDeeplink = isReadyToPurchase
324
+ }
325
+
326
+ private fun setThemeMode(mode: Int) {
327
+ Purchasely.setThemeMode(PLYThemeMode.values()[mode])
328
+ }
329
+
330
+ private fun setAttribute(attribute: Int, value: String?) {
331
+ if(value == null) return
332
+
333
+ Purchasely.setAttribute(Attribute.values()[attribute], value)
334
+ }
335
+
336
+ private fun setDefaultPresentationResultHandler(callbackContext: CallbackContext) {
337
+ defaultCallback = callbackContext
338
+ Purchasely.setDefaultPresentationResultHandler { result: PLYProductViewResult, plan: PLYPlan? ->
339
+ sendPurchaseResult(
340
+ result,
341
+ plan
342
+ )
343
+ }
344
+ }
345
+
346
+ private fun purchasedSubscription(callbackContext: CallbackContext) {
347
+ Purchasely.purchaseListener = object : PurchaseListener {
348
+ override fun onPurchaseStateChanged(state: State) {
349
+ if (state is State.PurchaseComplete || state is State.RestorationComplete) {
350
+ val pluginResult = PluginResult(PluginResult.Status.OK, "")
351
+ pluginResult.keepCallback = true
352
+ callbackContext.sendPluginResult(pluginResult)
353
+ }
354
+ }
355
+
356
+ }
357
+ }
358
+
359
+ private fun synchronize() {
360
+ Purchasely.synchronize()
361
+ }
362
+
363
+ private fun userDidConsumeSubscriptionContent() {
364
+ Purchasely.userDidConsumeSubscriptionContent()
365
+ }
366
+
367
+ private fun presentPresentationWithIdentifier(
368
+ presentationVendorId: String?,
369
+ contentId: String?,
370
+ isFullScreen: Boolean,
371
+ callbackContext: CallbackContext
372
+ ) {
373
+ purchaseCallback = callbackContext
374
+ val intent = PLYProductActivity.newIntent(cordova.activity)
375
+ intent.putExtra("presentationId", presentationVendorId)
376
+ intent.putExtra("contentId", contentId)
377
+ intent.putExtra("isFullScreen", isFullScreen)
378
+ cordova.activity.startActivity(intent)
379
+ }
380
+
381
+ private fun presentPresentationForPlacement(
382
+ placementVendorId: String?,
383
+ contentId: String?,
384
+ isFullScreen: Boolean,
385
+ callbackContext: CallbackContext
386
+ ) {
387
+ purchaseCallback = callbackContext
388
+ val intent = PLYProductActivity.newIntent(cordova.activity)
389
+ intent.putExtra("placementId", placementVendorId)
390
+ intent.putExtra("contentId", contentId)
391
+ intent.putExtra("isFullScreen", isFullScreen)
392
+ cordova.activity.startActivity(intent)
393
+ }
394
+
395
+ private fun presentProductWithIdentifier(
396
+ productVendorId: String?,
397
+ presentationVendorId: String?,
398
+ contentId: String?,
399
+ isFullScreen: Boolean,
400
+ callbackContext: CallbackContext
401
+ ) {
402
+ purchaseCallback = callbackContext
403
+ val intent = PLYProductActivity.newIntent(cordova.activity)
404
+ intent.putExtra("presentationId", presentationVendorId)
405
+ intent.putExtra("productId", productVendorId)
406
+ intent.putExtra("contentId", contentId)
407
+ intent.putExtra("isFullScreen", isFullScreen)
408
+ cordova.activity.startActivity(intent)
409
+ }
410
+
411
+ private fun presentPlanWithIdentifier(
412
+ planVendorId: String?,
413
+ presentationVendorId: String?,
414
+ contentId: String?,
415
+ isFullScreen: Boolean,
416
+ callbackContext: CallbackContext
417
+ ) {
418
+ purchaseCallback = callbackContext
419
+ val intent = PLYProductActivity.newIntent(cordova.activity)
420
+ intent.putExtra("presentationId", presentationVendorId)
421
+ intent.putExtra("planId", planVendorId)
422
+ intent.putExtra("contentId", contentId)
423
+ intent.putExtra("isFullScreen", isFullScreen)
424
+ cordova.activity.startActivity(intent)
425
+ }
426
+
427
+ private fun fetchPresentation(
428
+ placementId: String?,
429
+ presentationId: String?,
430
+ contentId: String?,
431
+ callbackContext: CallbackContext) {
432
+ val properties = PLYPresentationViewProperties(
433
+ placementId = placementId,
434
+ presentationId = presentationId,
435
+ contentId = contentId)
436
+
437
+ Purchasely.fetchPresentation(properties = properties) { presentation: PLYPresentation?, error: PLYError? ->
438
+ if(presentation != null) {
439
+ presentationsLoaded.removeAll { it.id == presentation.id && it.placementId == presentation.placementId }
440
+ presentationsLoaded.add(presentation)
441
+ val map = presentation.toMap().mapValues {
442
+ val value = it.value
443
+ if(value is PLYPresentationType) value.ordinal
444
+ else value
445
+ }
446
+
447
+ val mutableMap = map.toMutableMap().apply {
448
+ //this["metadata"] = presentation.metadata?.toMap()
449
+ this["plans"] = (this["plans"] as List<PLYPresentationPlan>).map { it.toMap() }
450
+ }
451
+ callbackContext.success(JSONObject(map))
452
+ }
453
+ if(error != null) callbackContext.error(error.message ?: "Unable to fetch presentation")
454
+ }
455
+ }
456
+
457
+ // Delete when available in Android SDK
458
+ fun PLYPresentationPlan.toMap() : Map<String, String?> {
459
+ return mapOf(
460
+ Pair("planVendorId", planVendorId),
461
+ Pair("storeProductId", storeProductId),
462
+ Pair("basePlanId", basePlanId),
463
+ Pair("offerId", offerId)
464
+ )
465
+ }
466
+
467
+ private fun presentPresentation(presentationMap: JSONObject?,
468
+ isFullScreen: Boolean,
469
+ loadingBackgroundColor: String?,
470
+ callbackContext: CallbackContext) {
471
+ if (presentationMap == null) {
472
+ callbackContext.error("presentation cannot be null")
473
+ return
474
+ }
475
+
476
+ val presentation = presentationsLoaded.lastOrNull {
477
+ it.id == presentationMap.getString("id")
478
+ && it.placementId == presentationMap.getString("placementId")
479
+ }
480
+ if(presentation == null) {
481
+ callbackContext.error("presentation cannot be found")
482
+ return
483
+ }
484
+
485
+ purchaseCallback = callbackContext
486
+
487
+ cordova.activity.let { activity ->
488
+ val intent = PLYProductActivity.newIntent(activity, PLYPresentationViewProperties(), isFullScreen, loadingBackgroundColor).apply {
489
+ putExtra("presentation", presentation)
490
+ }
491
+ activity.startActivity(intent)
492
+ }
493
+
494
+ }
495
+
496
+ private fun presentSubscriptions() {
497
+ val intent =
498
+ Intent(cordova.context, PLYSubscriptionsActivity::class.java)
499
+ cordova.activity.startActivity(intent)
500
+ }
501
+
502
+ private fun restoreAllProducts(callbackContext: CallbackContext) {
503
+ Purchasely.restoreAllProducts({ plyPlan: PLYPlan? ->
504
+ callbackContext.success(JSONObject(transformPlanToMap(plyPlan)))
505
+ }) { plyError: PLYError? ->
506
+ callbackContext.error(plyError?.message)
507
+ }
508
+ }
509
+
510
+ private fun userSubscriptions(callbackContext: CallbackContext) {
511
+ Purchasely.userSubscriptions(
512
+ false,
513
+ object : SubscriptionsListener {
514
+ override fun onSuccess(list: List<PLYSubscriptionData>) {
515
+ val result = JSONArray()
516
+ for (i in list.indices) {
517
+ val data = list[i]
518
+ val map = HashMap(data.toMap())
519
+ map["plan"] = transformPlanToMap(data.plan)
520
+ map["product"] = data.product.toMap()
521
+ if (data.data.storeType == StoreType.GOOGLE_PLAY_STORE) {
522
+ map["subscriptionSource"] = StoreType.GOOGLE_PLAY_STORE.ordinal
523
+ } else if (data.data.storeType == StoreType.AMAZON_APP_STORE) {
524
+ map["subscriptionSource"] = StoreType.AMAZON_APP_STORE.ordinal
525
+ } else if (data.data.storeType == StoreType.HUAWEI_APP_GALLERY) {
526
+ map["subscriptionSource"] = StoreType.HUAWEI_APP_GALLERY.ordinal
527
+ } else if (data.data.storeType == StoreType.APPLE_APP_STORE) {
528
+ map["subscriptionSource"] = StoreType.APPLE_APP_STORE.ordinal
529
+ }
530
+ result.put(JSONObject(map))
531
+ }
532
+ callbackContext.success(result)
533
+ }
534
+
535
+ override fun onFailure(throwable: Throwable) {
536
+ callbackContext.error(throwable.message)
537
+ }
538
+ }
539
+ )
540
+ }
541
+
542
+ private fun isDeeplinkHandled(deeplink: String?, callbackContext: CallbackContext) {
543
+ if (deeplink == null) {
544
+ callbackContext.error("Deeplink must not be null")
545
+ return
546
+ }
547
+ val uri = Uri.parse(deeplink)
548
+ callbackContext.sendPluginResult(
549
+ PluginResult(
550
+ PluginResult.Status.OK,
551
+ Purchasely.isDeeplinkHandled(uri)
552
+ )
553
+ )
554
+ }
555
+
556
+ private fun allProducts(callbackContext: CallbackContext) {
557
+ Purchasely.allProducts(object : ProductsListener {
558
+ override fun onSuccess(list: List<PLYProduct>) {
559
+ val result = JSONArray()
560
+ for (i in list.indices) {
561
+ result.put(JSONObject(list[i].toMap()))
562
+ }
563
+ callbackContext.success(result)
564
+ }
565
+
566
+ override fun onFailure(throwable: Throwable) {
567
+ callbackContext.error(throwable.message)
568
+ }
569
+ })
570
+ }
571
+
572
+ private fun productWithIdentifier(vendorId: String?, callbackContext: CallbackContext) {
573
+ if(vendorId == null) {
574
+ callbackContext.error("No product found with $vendorId")
575
+ return
576
+ }
577
+ Purchasely.product(vendorId, object : ProductListener {
578
+ override fun onSuccess(product: PLYProduct?) {
579
+ if (product != null) {
580
+ callbackContext.success(JSONObject(product.toMap()))
581
+ } else {
582
+ callbackContext.error("No product found with $vendorId")
583
+ }
584
+ }
585
+
586
+ override fun onFailure(throwable: Throwable) {
587
+ callbackContext.error(throwable.message)
588
+ }
589
+ })
590
+ }
591
+
592
+ private fun planWithIdentifier(vendorId: String?, callbackContext: CallbackContext) {
593
+ if(vendorId == null) {
594
+ callbackContext.error("No plan found with $vendorId")
595
+ return
596
+ }
597
+ Purchasely.plan(vendorId, object : PlanListener {
598
+ override fun onSuccess(plan: PLYPlan?) {
599
+ if (plan != null) {
600
+ callbackContext.success(JSONObject(transformPlanToMap(plan)))
601
+ } else {
602
+ callbackContext.error("No plan found with $vendorId")
603
+ }
604
+ }
605
+
606
+ override fun onFailure(throwable: Throwable) {
607
+ callbackContext.error(throwable.message)
608
+ }
609
+ })
610
+ }
611
+
612
+ private fun purchaseWithPlanVendorId(
613
+ planVendorId: String?,
614
+ offerId: String?,
615
+ contentId: String?,
616
+ callbackContext: CallbackContext
617
+ ) {
618
+ if(planVendorId == null) {
619
+ callbackContext.error("No plan found with $planVendorId")
620
+ return
621
+ }
622
+
623
+ Purchasely.plan(planVendorId, object : PlanListener {
624
+ override fun onSuccess(plyPlan: PLYPlan?) {
625
+ if (plyPlan != null) {
626
+ val offer = plyPlan.promoOffers.firstOrNull { it.vendorId == offerId }
627
+ Purchasely.purchase(cordova.activity, plyPlan, offer, contentId,
628
+ { plyPlan1: PLYPlan? ->
629
+ callbackContext.success(JSONObject(transformPlanToMap(plyPlan1)))
630
+ }) { plyError: PLYError? ->
631
+ callbackContext.error(plyError?.message)
632
+ }
633
+ } else {
634
+ callbackContext.error("No plan found with $planVendorId")
635
+ }
636
+ }
637
+
638
+ override fun onFailure(throwable: Throwable) {
639
+ callbackContext.error(throwable.message)
640
+ }
641
+ })
642
+ }
643
+
644
+ private fun setPaywallActionInterceptor(callbackContext: CallbackContext) {
645
+ Purchasely.setPaywallActionsInterceptor { info, action, parameters, processAction ->
646
+ paywallActionHandler = processAction
647
+ paywallAction = action
648
+
649
+ interceptorActivity = WeakReference(info?.activity)
650
+
651
+ val parametersForCordova = hashMapOf<String, Any?>();
652
+ parametersForCordova["title"] = parameters.title
653
+ parametersForCordova["url"] = parameters.url?.toString()
654
+ parametersForCordova["plan"] = transformPlanToMap(parameters.plan)
655
+ parametersForCordova["offer"] = mapOf(
656
+ "vendorId" to parameters.offer?.vendorId,
657
+ "storeOfferId" to parameters.offer?.storeOfferId
658
+ )
659
+ parametersForCordova["subscriptionOffer"] = parameters.subscriptionOffer?.toMap()
660
+ parametersForCordova["presentation"] = parameters.presentation
661
+ parametersForCordova["placement"] = parameters.placement
662
+
663
+ val result = PluginResult(
664
+ PluginResult.Status.OK,
665
+ JSONObject(
666
+ hashMapOf<String, Any?>(
667
+ Pair("info", mapOf(
668
+ Pair("contentId", info?.contentId),
669
+ Pair("presentationId", info?.presentationId),
670
+ Pair("placementId", info?.placementId),
671
+ Pair("abTestId", info?.abTestId),
672
+ Pair("abTestVariantId", info?.abTestVariantId)
673
+ )),
674
+ Pair("action", action.value),
675
+ Pair("parameters", parametersForCordova.filterNot { it.value == null })
676
+ )
677
+ )
678
+ )
679
+ result.keepCallback = true
680
+ callbackContext.sendPluginResult(result)
681
+ }
682
+ }
683
+
684
+ private fun closePresentation(callbackContext: CallbackContext) {
685
+ val openedPaywall = productActivity?.activity?.get()
686
+ openedPaywall?.finish()
687
+ productActivity = null
688
+ }
689
+
690
+ private fun onProcessAction(processAction: Boolean) {
691
+ val activityHandler = interceptorActivity?.get() ?: productActivity?.activity?.get() ?: cordova.activity
692
+ activityHandler?.runOnUiThread {
693
+ paywallActionHandler?.invoke(processAction)
694
+
695
+ interceptorActivity?.clear()
696
+ interceptorActivity = null
697
+ }
698
+ }
699
+
700
+ private fun showPresentation() {
701
+ val currentActivity = interceptorActivity?.get()
702
+
703
+ if (currentActivity != null && !currentActivity.isFinishing && !currentActivity.isDestroyed) {
704
+ cordova.activity?.let {
705
+ it.startActivity(
706
+ Intent(it, currentActivity::class.java).apply {
707
+ //flags = Intent.FLAG_ACTIVITY_NEW_TASK
708
+ flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
709
+ }
710
+ )
711
+ }
712
+ }
713
+ else {
714
+ productActivity?.relaunch(cordova)
715
+ }
716
+ }
717
+
718
+ private fun hidePresentation() {
719
+ val cordovaActivity = cordova.activity
720
+ val activity = productActivity?.activity?.get() ?: cordovaActivity
721
+ cordovaActivity?.startActivity(
722
+ Intent(activity, cordovaActivity::class.java).apply {
723
+ flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
724
+ }
725
+ )
726
+ }
727
+
728
+ fun setUserAttributeWithString(key: String?, value: String?) {
729
+ if(key == null || value == null) return
730
+ Purchasely.setUserAttribute(key, value)
731
+ }
732
+
733
+ fun setUserAttributeWithInt(key: String?, value: Int?) {
734
+ if(key == null || value == null) return
735
+ Purchasely.setUserAttribute(key, value)
736
+ }
737
+
738
+ fun setUserAttributeWithDouble(key: String?, value: Double?) {
739
+ if(key == null || value == null) return
740
+ Purchasely.setUserAttribute(key, value.toFloat())
741
+ }
742
+
743
+ fun setUserAttributeWithBoolean(key: String?, value: Boolean?) {
744
+ if(key == null || value == null) return
745
+ Purchasely.setUserAttribute(key, value)
746
+ }
747
+
748
+ fun setUserAttributeWithDate(key: String?, value: String?) {
749
+ if(key == null || value == null) return
750
+ val format = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
751
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX", Locale.getDefault())
752
+ } else {
753
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
754
+ }
755
+ format.timeZone = TimeZone.getTimeZone("GMT")
756
+ val calendar = Calendar.getInstance()
757
+ try {
758
+ format.parse(value)?.let {
759
+ calendar.time = it
760
+ }
761
+ Purchasely.setUserAttribute(key, calendar.time)
762
+ } catch (e: Exception) {
763
+ Log.e("Purchasely", "Cannot save date attribute $key", e)
764
+ }
765
+ }
766
+
767
+ fun userAttribute(key: String?, callbackContext: CallbackContext) {
768
+ if(key == null) return
769
+ val result = getUserAttributeValueForCordova(Purchasely.userAttribute(key))
770
+ if(result != null) {
771
+ callbackContext.success(getUserAttributeValueForCordova(result))
772
+ } else {
773
+ callbackContext.error("No user attribute found with $key")
774
+ }
775
+ }
776
+
777
+ private fun getUserAttributeValueForCordova(value: Any?): String? {
778
+ return when (value) {
779
+ is Date -> {
780
+ val format = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
781
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX", Locale.getDefault())
782
+ } else {
783
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
784
+ }
785
+ format.timeZone = TimeZone.getTimeZone("GMT")
786
+ try {
787
+ format.format(value)
788
+ } catch (e: Exception) {
789
+ ""
790
+ }
791
+ }
792
+ is Int -> value.toString()
793
+ //awful but to keep same precision so 1.2f = 1.2 double and not 1.20000056
794
+ is Float -> value.toString().toDouble().toString()
795
+ is String -> value
796
+ is Boolean -> value.toString()
797
+ else -> null
798
+ }
799
+ }
800
+
801
+ fun clearUserAttribute(key: String?) {
802
+ if(key == null) return
803
+ Purchasely.clearUserAttribute(key)
804
+ }
805
+
806
+ fun clearUserAttributes() {
807
+ Purchasely.clearUserAttributes()
808
+ }
809
+
810
+ private fun isEligibleForIntroOffer(planId: String?, callbackContext: CallbackContext) {
811
+ Purchasely.plan(planId,
812
+ onSuccess = { plan ->
813
+ callbackContext.sendPluginResult(PluginResult(PluginResult.Status.OK, plan?.isEligibleToIntroOffer(null) ?: false))
814
+ },
815
+ onError = { error ->
816
+ callbackContext.error(error.message ?: "Unable to fetch plan")
817
+ }
818
+ )
819
+ }
820
+
821
+ private fun signPromotionalOffer(storeProductId: String?, storeOfferId: String?, callbackContext: CallbackContext) {
822
+ callbackContext.error("No signing required on Android")
823
+ }
824
+
825
+
826
+ class ProductActivity {
827
+ var presentationId: String? = null
828
+ var placementId: String? = null
829
+ var productId: String? = null
830
+ var planId: String? = null
831
+ var contentId: String? = null
832
+ var presentation: PLYPresentation? = null
833
+ var isFullScreen = false
834
+ var loadingBackgroundColor: String? = null
835
+ var activity: WeakReference<PLYProductActivity>? = null
836
+ fun relaunch(cordova: CordovaInterface): Boolean {
837
+ var backgroundActivity: PLYProductActivity? = null
838
+ if (activity != null) {
839
+ backgroundActivity = activity?.get()
840
+ }
841
+ return if (backgroundActivity != null && !backgroundActivity.isFinishing
842
+ && !backgroundActivity.isDestroyed
843
+ ) {
844
+ val intent = Intent(cordova.activity, PLYProductActivity::class.java)
845
+ intent.putExtra("presentation", presentation)
846
+ intent.putExtra("presentationId", presentationId)
847
+ intent.putExtra("placementId", placementId)
848
+ intent.putExtra("productId", productId)
849
+ intent.putExtra("planId", planId)
850
+ intent.putExtra("contentId", contentId)
851
+ intent.putExtra("isFullScreen", isFullScreen)
852
+ intent.putExtra("background_color", loadingBackgroundColor)
853
+ intent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
854
+ cordova.activity.startActivity(intent)
855
+ true
856
+ } else {
857
+ val intent = PLYProductActivity.newIntent(cordova.activity)
858
+ intent.putExtra("presentation", presentation)
859
+ intent.putExtra("presentationId", presentationId)
860
+ intent.putExtra("placementId", placementId)
861
+ intent.putExtra("productId", productId)
862
+ intent.putExtra("planId", planId)
863
+ intent.putExtra("contentId", contentId)
864
+ intent.putExtra("isFullScreen", isFullScreen)
865
+ intent.putExtra("background_color", loadingBackgroundColor)
866
+ cordova.activity.startActivity(intent)
867
+ false
868
+ }
869
+ }
870
+ }
871
+
872
+ companion object {
873
+ var defaultCallback: CallbackContext? = null
874
+ var purchaseCallback: CallbackContext? = null
875
+ var eventsCallback: CallbackContext? = null
876
+ var productActivity: ProductActivity? = null
877
+
878
+ var interceptorActivity: WeakReference<Activity>? = null
879
+
880
+ val presentationsLoaded = mutableListOf<PLYPresentation>()
881
+
882
+ private const val runningModePaywallObserver = 2
883
+ private const val runningModeFull = 3
884
+
885
+ fun sendPurchaseResult(result: PLYProductViewResult, plan: PLYPlan?) {
886
+ var productViewResult = 0
887
+ if (result == PLYProductViewResult.PURCHASED) {
888
+ productViewResult = PLYProductViewResult.PURCHASED.ordinal
889
+ } else if (result == PLYProductViewResult.CANCELLED) {
890
+ productViewResult = PLYProductViewResult.CANCELLED.ordinal
891
+ } else if (result == PLYProductViewResult.RESTORED) {
892
+ productViewResult = PLYProductViewResult.RESTORED.ordinal
893
+ }
894
+ val map = HashMap<String?, Any?>()
895
+ map["result"] = productViewResult
896
+ map["plan"] = transformPlanToMap(plan)
897
+ if (purchaseCallback != null) {
898
+ purchaseCallback?.success(JSONObject(map))
899
+ purchaseCallback = null
900
+ } else if (defaultCallback != null) {
901
+ val pluginResult = PluginResult(PluginResult.Status.OK, JSONObject(map))
902
+ pluginResult.keepCallback = true
903
+ defaultCallback?.sendPluginResult(pluginResult)
904
+ }
905
+ }
906
+
907
+ private fun transformPlanToMap(plan: PLYPlan?): Map<String?, Any?> {
908
+ if (plan == null) return HashMap()
909
+ val map = HashMap(plan.toMap())
910
+ if (plan.type == DistributionType.CONSUMABLE) {
911
+ map["type"] = DistributionType.CONSUMABLE.ordinal
912
+ } else if (plan.type == DistributionType.CONSUMABLE) {
913
+ map["type"] = DistributionType.NON_CONSUMABLE.ordinal
914
+ } else if (plan.type == DistributionType.NON_CONSUMABLE) {
915
+ map["type"] = DistributionType.RENEWING_SUBSCRIPTION.ordinal
916
+ } else if (plan.type == DistributionType.NON_RENEWING_SUBSCRIPTION) {
917
+ map["type"] = DistributionType.NON_RENEWING_SUBSCRIPTION.ordinal
918
+ } else if (plan.type == DistributionType.UNKNOWN) {
919
+ map["type"] = DistributionType.UNKNOWN.ordinal
920
+ }
921
+ map["isEligibleForIntroOffer"] = plan.isEligibleToIntroOffer(null)
922
+ return map
923
+ }
924
+ }
925
+ }