@virex-tech/paywallo-sdk 2.2.4 → 2.2.5

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.
@@ -8,7 +8,7 @@ buildscript {
8
8
  }
9
9
  dependencies {
10
10
  classpath("com.android.tools.build:gradle:${safeExtGet('androidGradlePluginVersion', '7.4.2')}")
11
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.9.22')}")
11
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '2.0.21')}")
12
12
  }
13
13
  }
14
14
 
@@ -48,12 +48,14 @@ repositories {
48
48
 
49
49
  dependencies {
50
50
  implementation "com.facebook.react:react-native:+"
51
- implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '1.9.22')}"
51
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '2.0.21')}"
52
52
  implementation "com.android.billingclient:billing-ktx:7.0.0"
53
53
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
54
54
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
55
55
  compileOnly "com.facebook.android:facebook-android-sdk:17.+"
56
56
  compileOnly "com.google.firebase:firebase-messaging:24.0.0"
57
- implementation "androidx.security:security-crypto:1.0.0"
57
+ // 1.1.0+ required for MasterKey.Builder API used in PaywalloStorageModule.
58
+ // 1.0.0 only ships the deprecated MasterKeys.getOrCreate(...) static helper.
59
+ implementation "androidx.security:security-crypto:1.1.0"
58
60
  implementation "com.android.installreferrer:installreferrer:2.2"
59
61
  }
@@ -2,6 +2,7 @@ package com.paywallo.sdk
2
2
 
3
3
  import com.google.firebase.messaging.FirebaseMessagingService
4
4
  import com.google.firebase.messaging.RemoteMessage
5
+ import com.facebook.react.bridge.ReactContext
5
6
  import com.facebook.react.bridge.WritableNativeMap
6
7
  import com.facebook.react.modules.core.DeviceEventManagerModule
7
8
  import com.facebook.react.ReactApplication
@@ -46,10 +47,7 @@ class PaywalloFirebaseMessagingService : FirebaseMessagingService() {
46
47
 
47
48
  private fun emitEvent(eventName: String, data: WritableNativeMap) {
48
49
  try {
49
- val reactContext = (application as? ReactApplication)
50
- ?.reactNativeHost
51
- ?.reactInstanceManager
52
- ?.currentReactContext
50
+ val reactContext = resolveReactContext()
53
51
  if (reactContext != null) {
54
52
  reactContext
55
53
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
@@ -58,10 +56,40 @@ class PaywalloFirebaseMessagingService : FirebaseMessagingService() {
58
56
  // Cold start: React context not ready — buffer for getInitialNotification()
59
57
  pendingMessage = data
60
58
  }
61
- } catch (_: Exception) {
59
+ } catch (_: Throwable) {
62
60
  if (eventName == MESSAGE_RECEIVED_EVENT) {
63
61
  pendingMessage = data
64
62
  }
65
63
  }
66
64
  }
65
+
66
+ /**
67
+ * Resolves the active ReactContext across both architectures:
68
+ * - New Architecture (bridgeless): ReactApplication.reactHost.currentReactContext
69
+ * - Legacy bridge: ReactApplication.reactNativeHost.reactInstanceManager.currentReactContext
70
+ *
71
+ * Note: in the New Architecture, accessing `reactNativeHost` throws
72
+ * RuntimeException("You should not use ReactNativeHost directly in the New Architecture"),
73
+ * so we MUST try `reactHost` first and only fall back to the legacy path when reactHost is null.
74
+ * Each path is guarded by try/catch so a throw on one doesn't break the other.
75
+ */
76
+ private fun resolveReactContext(): ReactContext? {
77
+ val app = application as? ReactApplication ?: return null
78
+
79
+ // New Architecture path (RN 0.74+ bridgeless)
80
+ try {
81
+ val ctx = app.reactHost?.currentReactContext
82
+ if (ctx != null) return ctx
83
+ } catch (_: Throwable) {
84
+ // fall through to legacy
85
+ }
86
+
87
+ // Legacy bridge path (Old Architecture)
88
+ return try {
89
+ @Suppress("DEPRECATION")
90
+ app.reactNativeHost.reactInstanceManager.currentReactContext
91
+ } catch (_: Throwable) {
92
+ null
93
+ }
94
+ }
67
95
  }
@@ -3,10 +3,10 @@ package com.paywallo.sdk
3
3
  import android.Manifest
4
4
  import android.content.pm.PackageManager
5
5
  import android.os.Build
6
- import androidx.core.app.ActivityCompat
7
6
  import androidx.core.content.ContextCompat
8
7
  import com.facebook.react.bridge.*
9
- import com.facebook.react.modules.core.DeviceEventManagerModule
8
+ import com.facebook.react.modules.core.PermissionAwareActivity
9
+ import com.facebook.react.modules.core.PermissionListener
10
10
 
11
11
  class PaywalloNotificationsModule(private val reactContext: ReactApplicationContext) :
12
12
  ReactContextBaseJavaModule(reactContext) {
@@ -14,26 +14,21 @@ class PaywalloNotificationsModule(private val reactContext: ReactApplicationCont
14
14
  private var permissionPromise: Promise? = null
15
15
  private val PERMISSION_REQUEST_CODE = 1001
16
16
 
17
- private val activityEventListener = object : BaseActivityEventListener() {
18
- override fun onRequestPermissionsResult(
19
- requestCode: Int,
20
- permissions: Array<out String>,
21
- grantResults: IntArray
22
- ) {
23
- if (requestCode != PERMISSION_REQUEST_CODE) return
24
- val promise = permissionPromise ?: return
25
- permissionPromise = null
17
+ private val permissionListener = PermissionListener { requestCode, _, grantResults ->
18
+ if (requestCode != PERMISSION_REQUEST_CODE) {
19
+ return@PermissionListener false
20
+ }
21
+ val promise = permissionPromise
22
+ permissionPromise = null
26
23
 
24
+ if (promise != null) {
27
25
  if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
28
26
  promise.resolve("granted")
29
27
  } else {
30
28
  promise.resolve("denied")
31
29
  }
32
30
  }
33
- }
34
-
35
- init {
36
- reactContext.addActivityEventListener(activityEventListener)
31
+ true
37
32
  }
38
33
 
39
34
  override fun getName() = "PaywalloNotifications"
@@ -79,7 +74,10 @@ class PaywalloNotificationsModule(private val reactContext: ReactApplicationCont
79
74
  fun requestPermission(options: ReadableMap?, promise: Promise) {
80
75
  // Android 13+ (API 33) requires POST_NOTIFICATIONS runtime permission
81
76
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
82
- val activity = currentActivity
77
+ // RN 0.80+ removed the synthetic `currentActivity` property on
78
+ // ReactContextBaseJavaModule (base class was rewritten in Kotlin).
79
+ // Reach it via the ReactContext, which is still Java-backed.
80
+ val activity = reactContext.currentActivity
83
81
  if (activity == null) {
84
82
  promise.resolve("denied")
85
83
  return
@@ -90,23 +88,41 @@ class PaywalloNotificationsModule(private val reactContext: ReactApplicationCont
90
88
 
91
89
  if (granted) {
92
90
  promise.resolve("granted")
93
- } else {
94
- // If there's already a pending promise, reject it before storing the new one
95
- permissionPromise?.reject("PERMISSION_SUPERSEDED", "New permission request superseded this one")
96
- // Store promise and wait for onRequestPermissionsResult callback
97
- permissionPromise = promise
98
- ActivityCompat.requestPermissions(
99
- activity,
100
- arrayOf(Manifest.permission.POST_NOTIFICATIONS),
101
- PERMISSION_REQUEST_CODE
102
- )
91
+ return
92
+ }
93
+
94
+ val permissionAware = activity as? PermissionAwareActivity
95
+ if (permissionAware == null) {
96
+ promise.resolve("denied")
97
+ return
103
98
  }
99
+
100
+ // If there's already a pending promise, reject it before storing the new one
101
+ permissionPromise?.reject("PERMISSION_SUPERSEDED", "New permission request superseded this one")
102
+ // Store promise and wait for permission listener callback
103
+ permissionPromise = promise
104
+ permissionAware.requestPermissions(
105
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
106
+ PERMISSION_REQUEST_CODE,
107
+ permissionListener
108
+ )
104
109
  } else {
105
110
  // Pre-Android 13: notifications always allowed
106
111
  promise.resolve("granted")
107
112
  }
108
113
  }
109
114
 
115
+ // iOS calls UIApplication.registerForRemoteNotifications() here to obtain
116
+ // an APNS token. On Android there's no equivalent: FCM tokens are minted
117
+ // automatically by Firebase and surfaced via FirebaseMessagingService.onNewToken.
118
+ // Exposing this as a no-op keeps the cross-platform bridge contract and
119
+ // silences the "method not found" warning from NativeModules when the JS
120
+ // bridge calls it after permission is granted.
121
+ @ReactMethod
122
+ fun registerForPush(promise: Promise) {
123
+ promise.resolve(null)
124
+ }
125
+
110
126
  @ReactMethod
111
127
  fun hasPermission(promise: Promise) {
112
128
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -130,7 +146,7 @@ class PaywalloNotificationsModule(private val reactContext: ReactApplicationCont
130
146
  }
131
147
 
132
148
  // Check if the app was opened from a notification tap
133
- val activity = currentActivity
149
+ val activity = reactContext.currentActivity
134
150
  val intent = activity?.intent
135
151
  val extras = intent?.extras
136
152
 
@@ -161,9 +177,10 @@ class PaywalloNotificationsModule(private val reactContext: ReactApplicationCont
161
177
  }
162
178
  }
163
179
 
164
- override fun onCatalystInstanceDestroy() {
165
- reactContext.removeActivityEventListener(activityEventListener)
180
+ // invalidate() is the post-TurboModule replacement for onCatalystInstanceDestroy.
181
+ // The latter is @Deprecated(forRemoval = true) since RN 0.74 and will be removed.
182
+ override fun invalidate() {
166
183
  permissionPromise = null
167
- super.onCatalystInstanceDestroy()
184
+ super.invalidate()
168
185
  }
169
186
  }
@@ -195,7 +195,9 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
195
195
  }
196
196
 
197
197
  val client = billingClient!!
198
- val activity = currentActivity
198
+ // RN 0.80+ removed the synthetic `currentActivity` property on
199
+ // ReactContextBaseJavaModule. Reach it via ReactContext instead.
200
+ val activity = reactContext.currentActivity
199
201
 
200
202
  if (activity == null) {
201
203
  promise.reject("NO_ACTIVITY", "No current activity")
@@ -427,13 +429,15 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
427
429
  }
428
430
  }
429
431
 
430
- override fun onCatalystInstanceDestroy() {
432
+ // invalidate() is the post-TurboModule replacement for onCatalystInstanceDestroy.
433
+ // The latter is @Deprecated(forRemoval = true) since RN 0.74 and will be removed.
434
+ override fun invalidate() {
431
435
  // Reject any pending purchase promise so the JS side doesn't hang forever
432
436
  purchasePromise?.reject("MODULE_DESTROYED", "Module was destroyed before purchase completed")
433
437
  purchasePromise = null
434
438
  scope.cancel()
435
439
  billingClient?.endConnection()
436
440
  billingClient = null
437
- super.onCatalystInstanceDestroy()
441
+ super.invalidate()
438
442
  }
439
443
  }
@@ -10,11 +10,13 @@ import com.facebook.react.uimanager.events.Event
10
10
  class PaywalloWebViewEvent(
11
11
  surfaceId: Int,
12
12
  viewId: Int,
13
- private val eventName: String,
13
+ // RN 0.81 `Event.eventName` is now an open `val` on the base class.
14
+ // Using `eventName` as a constructor parameter would shadow it.
15
+ private val name: String,
14
16
  private val payload: WritableMap
15
17
  ) : Event<PaywalloWebViewEvent>(surfaceId, viewId) {
16
18
 
17
- override fun getEventName(): String = eventName
19
+ override fun getEventName(): String = name
18
20
 
19
21
  override fun getEventData(): WritableMap = payload
20
22
  }
package/dist/index.d.mts CHANGED
@@ -721,7 +721,11 @@ interface FcmRemoteMessage {
721
721
  notification?: {
722
722
  title?: string;
723
723
  body?: string;
724
+ imageUrl?: string;
724
725
  };
726
+ title?: string;
727
+ body?: string;
728
+ imageUrl?: string;
725
729
  sentTime?: number;
726
730
  }
727
731
  /**
package/dist/index.d.ts CHANGED
@@ -721,7 +721,11 @@ interface FcmRemoteMessage {
721
721
  notification?: {
722
722
  title?: string;
723
723
  body?: string;
724
+ imageUrl?: string;
724
725
  };
726
+ title?: string;
727
+ body?: string;
728
+ imageUrl?: string;
725
729
  sentTime?: number;
726
730
  }
727
731
  /**
package/dist/index.js CHANGED
@@ -2024,13 +2024,21 @@ var init_NativePushBridge = __esm({
2024
2024
  if (!raw || typeof raw !== "object") return null;
2025
2025
  const r = raw;
2026
2026
  const notification = r["notification"];
2027
+ const asString = (v) => typeof v === "string" ? v : void 0;
2027
2028
  return {
2028
- messageId: typeof r["messageId"] === "string" ? r["messageId"] : void 0,
2029
+ messageId: asString(r["messageId"]),
2029
2030
  data: typeof r["data"] === "object" && r["data"] !== null ? r["data"] : void 0,
2030
2031
  notification: notification && typeof notification === "object" ? {
2031
- title: typeof notification["title"] === "string" ? notification["title"] : void 0,
2032
- body: typeof notification["body"] === "string" ? notification["body"] : void 0
2032
+ title: asString(notification["title"]),
2033
+ body: asString(notification["body"]),
2034
+ imageUrl: asString(notification["imageUrl"])
2033
2035
  } : void 0,
2036
+ // Pass through top-level title/body/imageUrl emitted by the native modules
2037
+ // (both iOS and Android currently emit these at the top level rather than
2038
+ // nested under `notification`).
2039
+ title: asString(r["title"]),
2040
+ body: asString(r["body"]),
2041
+ imageUrl: asString(r["imageUrl"]),
2034
2042
  sentTime: typeof r["sentTime"] === "number" ? r["sentTime"] : void 0
2035
2043
  };
2036
2044
  } catch {
@@ -2169,8 +2177,9 @@ function setupForegroundHandler(deps, onReceive) {
2169
2177
  const data = message.data ?? {};
2170
2178
  const payload = extractPayload({
2171
2179
  ...data,
2172
- title: data["title"] ?? message.notification?.title ?? "",
2173
- body: data["body"] ?? message.notification?.body ?? ""
2180
+ title: data["title"] ?? message.notification?.title ?? message.title ?? "",
2181
+ body: data["body"] ?? message.notification?.body ?? message.body ?? "",
2182
+ imageUrl: data["imageUrl"] ?? message.notification?.imageUrl ?? message.imageUrl
2174
2183
  });
2175
2184
  if (debug) console.log("[Paywallo PUSH] foreground message received:", payload.notificationId);
2176
2185
  onReceive(payload);
@@ -2361,8 +2370,8 @@ async function getInitialNotificationPayload(bridge) {
2361
2370
  notificationId: String(data["notificationId"] ?? data["notification_id"] ?? ""),
2362
2371
  campaignId: String(data["campaignId"] ?? data["campaign_id"] ?? ""),
2363
2372
  variantKey: data["variantKey"] != null ? String(data["variantKey"]) : void 0,
2364
- title: String(data["title"] ?? message.notification?.title ?? ""),
2365
- body: String(data["body"] ?? message.notification?.body ?? ""),
2373
+ title: String(data["title"] ?? message.notification?.title ?? message.title ?? ""),
2374
+ body: String(data["body"] ?? message.notification?.body ?? message.body ?? ""),
2366
2375
  data
2367
2376
  };
2368
2377
  }
@@ -2377,8 +2386,8 @@ var init_NotificationHandlerSetup = __esm({
2377
2386
  // src/core/version.ts
2378
2387
  function resolveVersion() {
2379
2388
  try {
2380
- if ("2.2.4") {
2381
- return "2.2.4";
2389
+ if ("2.2.5") {
2390
+ return "2.2.5";
2382
2391
  }
2383
2392
  } catch {
2384
2393
  }
@@ -4723,12 +4732,24 @@ var init_QueueProcessor = __esm({
4723
4732
  const freshHeaders = this.config.getFreshHeaders?.() ?? {};
4724
4733
  const headers = { ...persistedHeaders, ...freshHeaders };
4725
4734
  this.log("Sending event batch", { size: batch.length, batchIndex: Math.floor(i / BATCH_SIZE) });
4726
- const response = await this.httpClient.request("/sdk/ingest/batch", {
4727
- method: "POST",
4728
- body: { events: bodies },
4729
- headers,
4730
- skipRetry: false
4731
- });
4735
+ let response;
4736
+ try {
4737
+ response = await this.httpClient.request("/sdk/ingest/batch", {
4738
+ method: "POST",
4739
+ body: { events: bodies },
4740
+ headers,
4741
+ skipRetry: false
4742
+ });
4743
+ } catch (error) {
4744
+ for (const item of batch) {
4745
+ retryIds.add(item.id);
4746
+ }
4747
+ this.log("Event batch threw network error - will retry", {
4748
+ size: batch.length,
4749
+ error: error instanceof Error ? error.message : String(error)
4750
+ });
4751
+ continue;
4752
+ }
4732
4753
  if (response.ok) {
4733
4754
  for (const item of batch) {
4734
4755
  await offlineQueue.removeItem(item.id);
@@ -4768,12 +4789,21 @@ var init_QueueProcessor = __esm({
4768
4789
  });
4769
4790
  const persistedHeaders = item.headers ?? {};
4770
4791
  const freshHeaders = this.config.getFreshHeaders?.() ?? {};
4771
- const response = await this.httpClient.request(item.url, {
4772
- method: item.method,
4773
- body: item.body,
4774
- headers: { ...persistedHeaders, ...freshHeaders },
4775
- skipRetry: false
4776
- });
4792
+ let response;
4793
+ try {
4794
+ response = await this.httpClient.request(item.url, {
4795
+ method: item.method,
4796
+ body: item.body,
4797
+ headers: { ...persistedHeaders, ...freshHeaders },
4798
+ skipRetry: false
4799
+ });
4800
+ } catch (error) {
4801
+ this.log("Request threw network error - will retry", {
4802
+ id: item.id,
4803
+ error: error instanceof Error ? error.message : String(error)
4804
+ });
4805
+ return { success: false, permanent: false };
4806
+ }
4777
4807
  if (response.ok) {
4778
4808
  this.log("Queue item processed successfully", { id: item.id });
4779
4809
  return { success: true, permanent: false };