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