@virex-tech/paywallo-sdk 2.2.4 → 2.2.6

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
  }
@@ -0,0 +1,31 @@
1
+ package com.paywallo.sdk
2
+
3
+ import com.facebook.react.bridge.*
4
+ import android.os.Build
5
+
6
+ class PaywalloEnvModule(private val reactContext: ReactApplicationContext) :
7
+ ReactContextBaseJavaModule(reactContext) {
8
+
9
+ override fun getName() = "PaywalloEnv"
10
+
11
+ @ReactMethod
12
+ fun detect(promise: Promise) {
13
+ try {
14
+ val packageName = reactContext.packageName
15
+ val packageManager = reactContext.packageManager
16
+ val installer = if (Build.VERSION.SDK_INT >= 30) {
17
+ packageManager.getInstallSourceInfo(packageName).installingPackageName
18
+ } else {
19
+ @Suppress("DEPRECATION")
20
+ packageManager.getInstallerPackageName(packageName)
21
+ }
22
+ if (installer == "com.android.vending") {
23
+ promise.resolve("production")
24
+ } else {
25
+ promise.resolve("sandbox")
26
+ }
27
+ } catch (e: Exception) {
28
+ promise.resolve("production")
29
+ }
30
+ }
31
+ }
@@ -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
  }
@@ -12,7 +12,8 @@ class PaywalloSdkPackage : ReactPackage {
12
12
  PaywalloFBBridgeModule(reactContext),
13
13
  PaywalloStorageModule(reactContext),
14
14
  PaywalloDeviceModule(reactContext),
15
- PaywalloNotificationsModule(reactContext)
15
+ PaywalloNotificationsModule(reactContext),
16
+ PaywalloEnvModule(reactContext)
16
17
  )
17
18
  }
18
19
 
@@ -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
@@ -307,6 +307,13 @@ interface PaywalloConfig {
307
307
  debug?: boolean;
308
308
  environment?: Environment;
309
309
  errorStrings?: PaywallErrorStrings$1;
310
+ /**
311
+ * App version reported to the server (used in event context.app_version
312
+ * and identify payload). When omitted, defaults to "unknown".
313
+ * Consumers should pass `Application.nativeApplicationVersion` (Expo)
314
+ * or `DeviceInfo.getVersion()` (bare RN) here.
315
+ */
316
+ appVersion?: string;
310
317
  }
311
318
  interface UserProperties {
312
319
  [key: string]: string | number | boolean | null;
@@ -387,12 +394,18 @@ interface EventContext {
387
394
  session_id?: string;
388
395
  device_id?: string;
389
396
  app_version?: string;
397
+ app_build?: string;
398
+ bundle_id?: string;
390
399
  sdk_version?: string;
391
400
  platform?: string;
392
401
  os_version?: string;
393
402
  device_model?: string;
394
403
  timezone?: string;
395
404
  locale?: string;
405
+ carrier?: string;
406
+ screen_width?: number;
407
+ screen_height?: number;
408
+ screen_density?: number;
396
409
  attribution?: Record<string, unknown>;
397
410
  ids?: Record<string, unknown>;
398
411
  }
@@ -721,7 +734,11 @@ interface FcmRemoteMessage {
721
734
  notification?: {
722
735
  title?: string;
723
736
  body?: string;
737
+ imageUrl?: string;
724
738
  };
739
+ title?: string;
740
+ body?: string;
741
+ imageUrl?: string;
725
742
  sentTime?: number;
726
743
  }
727
744
  /**
@@ -892,6 +909,7 @@ declare class NotificationsManager {
892
909
  private getDistinctId;
893
910
  private appKey;
894
911
  private apiBaseUrl;
912
+ private environment;
895
913
  private tokenRefreshCleanup;
896
914
  private foregroundCleanup;
897
915
  private pendingHandlersConfig;
@@ -906,6 +924,7 @@ declare class NotificationsManager {
906
924
  secureStorage: SecureStorage;
907
925
  getDeviceId: () => string | null;
908
926
  getDistinctId?: () => string;
927
+ environment?: "sandbox" | "production";
909
928
  }): void;
910
929
  initialize(config: NotificationsManagerConfig): Promise<void>;
911
930
  private applyConfig;
package/dist/index.d.ts CHANGED
@@ -307,6 +307,13 @@ interface PaywalloConfig {
307
307
  debug?: boolean;
308
308
  environment?: Environment;
309
309
  errorStrings?: PaywallErrorStrings$1;
310
+ /**
311
+ * App version reported to the server (used in event context.app_version
312
+ * and identify payload). When omitted, defaults to "unknown".
313
+ * Consumers should pass `Application.nativeApplicationVersion` (Expo)
314
+ * or `DeviceInfo.getVersion()` (bare RN) here.
315
+ */
316
+ appVersion?: string;
310
317
  }
311
318
  interface UserProperties {
312
319
  [key: string]: string | number | boolean | null;
@@ -387,12 +394,18 @@ interface EventContext {
387
394
  session_id?: string;
388
395
  device_id?: string;
389
396
  app_version?: string;
397
+ app_build?: string;
398
+ bundle_id?: string;
390
399
  sdk_version?: string;
391
400
  platform?: string;
392
401
  os_version?: string;
393
402
  device_model?: string;
394
403
  timezone?: string;
395
404
  locale?: string;
405
+ carrier?: string;
406
+ screen_width?: number;
407
+ screen_height?: number;
408
+ screen_density?: number;
396
409
  attribution?: Record<string, unknown>;
397
410
  ids?: Record<string, unknown>;
398
411
  }
@@ -721,7 +734,11 @@ interface FcmRemoteMessage {
721
734
  notification?: {
722
735
  title?: string;
723
736
  body?: string;
737
+ imageUrl?: string;
724
738
  };
739
+ title?: string;
740
+ body?: string;
741
+ imageUrl?: string;
725
742
  sentTime?: number;
726
743
  }
727
744
  /**
@@ -892,6 +909,7 @@ declare class NotificationsManager {
892
909
  private getDistinctId;
893
910
  private appKey;
894
911
  private apiBaseUrl;
912
+ private environment;
895
913
  private tokenRefreshCleanup;
896
914
  private foregroundCleanup;
897
915
  private pendingHandlersConfig;
@@ -906,6 +924,7 @@ declare class NotificationsManager {
906
924
  secureStorage: SecureStorage;
907
925
  getDeviceId: () => string | null;
908
926
  getDistinctId?: () => string;
927
+ environment?: "sandbox" | "production";
909
928
  }): void;
910
929
  initialize(config: NotificationsManagerConfig): Promise<void>;
911
930
  private applyConfig;