@virex-tech/paywallo-sdk 1.4.1 → 1.5.2
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.
- package/PaywalloSdk.podspec +2 -3
- package/README.md +58 -37
- package/android/build.gradle +4 -1
- package/android/consumer-rules.pro +24 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloDeviceModule.kt +148 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloFBBridgeModule.kt +10 -7
- package/android/src/main/java/com/paywallo/sdk/PaywalloSdkPackage.kt +3 -1
- package/android/src/main/java/com/paywallo/sdk/PaywalloStorageModule.kt +106 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloStoreKitModule.kt +30 -9
- package/android/src/main/java/com/paywallo/sdk/PaywalloWebView.kt +39 -3
- package/android/src/main/java/com/paywallo/sdk/PaywalloWebViewEvent.kt +20 -0
- package/dist/index.d.mts +26 -117
- package/dist/index.d.ts +26 -117
- package/dist/index.js +859 -627
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +816 -579
- package/dist/index.mjs.map +1 -1
- package/ios/PaywalloDevice.m +8 -0
- package/ios/PaywalloDevice.swift +91 -0
- package/ios/PaywalloFBBridge.swift +51 -0
- package/ios/PaywalloStorage.m +35 -0
- package/ios/PaywalloStorage.swift +171 -0
- package/ios/PaywalloStoreKit.swift +16 -2
- package/ios/PaywalloWebView.m +20 -0
- package/package.json +12 -11
package/PaywalloSdk.podspec
CHANGED
|
@@ -11,10 +11,9 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.authors = { "Paywallo" => "support@paywallo.io" }
|
|
12
12
|
s.platforms = { :ios => "15.0" }
|
|
13
13
|
s.source = { :git => "https://github.com/paywallo/sdk-react-native.git", :tag => "v#{s.version}" }
|
|
14
|
-
s.source_files = "ios/**/*.{h,m,swift}"
|
|
15
|
-
s.frameworks = "
|
|
14
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
15
|
+
s.frameworks = "Security", "StoreKit", "WebKit", "CoreTelephony", "AVFoundation"
|
|
16
16
|
s.swift_version = "5.5"
|
|
17
17
|
|
|
18
18
|
s.dependency "React-Core"
|
|
19
|
-
s.dependency "FBSDKCoreKit", "~> 17.0"
|
|
20
19
|
end
|
package/README.md
CHANGED
|
@@ -8,25 +8,23 @@ Paywalls, subscriptions, analytics and feature flags for React Native.
|
|
|
8
8
|
npm install @virex-tech/paywallo-sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
iOS:
|
|
13
12
|
```bash
|
|
14
|
-
|
|
13
|
+
cd ios && pod install
|
|
15
14
|
```
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
That's it. The SDK includes its own native modules for storage, device info and StoreKit — no extra dependencies needed.
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
npm install react-native-keychain
|
|
21
|
-
```
|
|
18
|
+
### Optional: Ad tracking (IDFA)
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
If your app uses Facebook Ads, AppsFlyer or any attribution that needs the Apple IDFA, install the tracking transparency module:
|
|
24
21
|
|
|
25
|
-
iOS only:
|
|
26
22
|
```bash
|
|
27
|
-
|
|
23
|
+
npm install expo-tracking-transparency
|
|
28
24
|
```
|
|
29
25
|
|
|
26
|
+
This enables the ATT prompt ("Allow this app to track your activity?"). Without it, the SDK skips IDFA collection — everything else works normally.
|
|
27
|
+
|
|
30
28
|
## Quick start
|
|
31
29
|
|
|
32
30
|
### 1. Wrap your app
|
|
@@ -48,28 +46,21 @@ export default function App() {
|
|
|
48
46
|
}
|
|
49
47
|
```
|
|
50
48
|
|
|
51
|
-
### 2.
|
|
49
|
+
### 2. Gate content with a paywall
|
|
52
50
|
|
|
53
51
|
```tsx
|
|
54
|
-
import {
|
|
52
|
+
import { PaywalloClient } from '@virex-tech/paywallo-sdk';
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
const hasAccess = await PaywalloClient.requireSubscriptionWithCampaign('my-campaign');
|
|
55
|
+
if (hasAccess) {
|
|
56
|
+
// User has an active subscription or just purchased
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
const hasAccess = await hasActiveSubscription();
|
|
62
|
-
if (!hasAccess) {
|
|
63
|
-
await presentPaywall('default');
|
|
64
|
-
}
|
|
65
|
-
};
|
|
60
|
+
### 3. Check subscription status
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{subscription ? 'Premium' : 'Upgrade'}
|
|
70
|
-
</Button>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
62
|
+
```tsx
|
|
63
|
+
const isActive = await PaywalloClient.hasActiveSubscription();
|
|
73
64
|
```
|
|
74
65
|
|
|
75
66
|
## Hooks
|
|
@@ -79,20 +70,32 @@ function MyComponent() {
|
|
|
79
70
|
| Property | Type | Description |
|
|
80
71
|
|---|---|---|
|
|
81
72
|
| `isInitialized` | `boolean` | SDK ready |
|
|
73
|
+
| `isReady` | `boolean` | SDK ready and products loaded |
|
|
82
74
|
| `distinctId` | `string` | Current user ID |
|
|
83
|
-
| `
|
|
75
|
+
| `presentCampaign(placement)` | `Promise<CampaignResult>` | Show a campaign paywall |
|
|
84
76
|
| `hasActiveSubscription()` | `Promise<boolean>` | Check subscription status |
|
|
85
77
|
| `getSubscription()` | `Promise<Subscription>` | Get subscription data |
|
|
86
|
-
| `restorePurchases()` | `Promise<
|
|
78
|
+
| `restorePurchases()` | `Promise<RestoreResult>` | Restore previous purchases |
|
|
87
79
|
|
|
88
80
|
### useSubscription()
|
|
89
81
|
|
|
90
82
|
| Property | Type | Description |
|
|
91
83
|
|---|---|---|
|
|
92
84
|
| `subscription` | `Subscription \| null` | Current subscription |
|
|
85
|
+
| `hasActiveSubscription` | `boolean` | Whether subscription is active |
|
|
93
86
|
| `isLoading` | `boolean` | Loading state |
|
|
94
87
|
| `refresh()` | `() => void` | Refetch subscription data |
|
|
95
88
|
|
|
89
|
+
### usePurchase()
|
|
90
|
+
|
|
91
|
+
| Property | Type | Description |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `purchase(productId)` | `Promise<PurchaseOutcome>` | Purchase a product |
|
|
94
|
+
| `restore()` | `Promise<IAPPurchase[]>` | Restore purchases |
|
|
95
|
+
| `isPurchasing` | `boolean` | Purchase in progress |
|
|
96
|
+
| `isRestoring` | `boolean` | Restore in progress |
|
|
97
|
+
| `error` | `PurchaseError \| null` | Last error |
|
|
98
|
+
|
|
96
99
|
## Imperative API
|
|
97
100
|
|
|
98
101
|
For use outside React components:
|
|
@@ -100,23 +103,34 @@ For use outside React components:
|
|
|
100
103
|
```tsx
|
|
101
104
|
import { PaywalloClient } from '@virex-tech/paywallo-sdk';
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
// Identify user
|
|
107
|
+
await PaywalloClient.identify({ email: 'user@example.com' });
|
|
104
108
|
|
|
105
|
-
|
|
109
|
+
// Track events
|
|
110
|
+
await PaywalloClient.track('purchase_completed', { properties: { product: 'premium' } });
|
|
106
111
|
|
|
112
|
+
// Feature flags
|
|
107
113
|
const variant = await PaywalloClient.getVariant('new_feature');
|
|
114
|
+
|
|
115
|
+
// Subscription gate
|
|
116
|
+
const hasAccess = await PaywalloClient.requireSubscriptionWithCampaign('premium');
|
|
108
117
|
```
|
|
109
118
|
|
|
110
|
-
##
|
|
119
|
+
## Config options
|
|
111
120
|
|
|
112
121
|
```tsx
|
|
113
122
|
interface PaywalloConfig {
|
|
114
|
-
appKey: string;
|
|
115
|
-
|
|
116
|
-
debug?: boolean;
|
|
117
|
-
|
|
123
|
+
appKey: string; // Required — your app key from the dashboard
|
|
124
|
+
apiUrl?: string; // API URL (default: https://paywallo.com.br)
|
|
125
|
+
debug?: boolean; // Enable debug logs (default: false)
|
|
126
|
+
autoStartSession?: boolean; // Auto-start session on init (default: true)
|
|
127
|
+
sessionFlags?: string[]; // Flags to resolve during session start
|
|
118
128
|
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Types
|
|
119
132
|
|
|
133
|
+
```tsx
|
|
120
134
|
interface Subscription {
|
|
121
135
|
productId: string;
|
|
122
136
|
status: 'active' | 'expired' | 'cancelled' | 'in_grace_period';
|
|
@@ -125,12 +139,19 @@ interface Subscription {
|
|
|
125
139
|
autoRenewEnabled: boolean;
|
|
126
140
|
}
|
|
127
141
|
|
|
128
|
-
interface
|
|
142
|
+
interface CampaignResult {
|
|
129
143
|
presented: boolean;
|
|
130
144
|
purchased: boolean;
|
|
131
145
|
restored: boolean;
|
|
132
146
|
cancelled: boolean;
|
|
133
147
|
error?: Error;
|
|
148
|
+
skippedReason?: 'subscriber';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface PurchaseOutcome {
|
|
152
|
+
status: 'success' | 'cancelled' | 'failed';
|
|
153
|
+
purchase?: IAPPurchase;
|
|
154
|
+
error?: PurchaseError;
|
|
134
155
|
}
|
|
135
156
|
```
|
|
136
157
|
|
package/android/build.gradle
CHANGED
|
@@ -22,6 +22,7 @@ android {
|
|
|
22
22
|
defaultConfig {
|
|
23
23
|
minSdkVersion safeExtGet('minSdkVersion', 24)
|
|
24
24
|
targetSdkVersion safeExtGet('targetSdkVersion', 34)
|
|
25
|
+
consumerProguardFiles "consumer-rules.pro"
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
compileOptions {
|
|
@@ -51,5 +52,7 @@ dependencies {
|
|
|
51
52
|
implementation "com.android.billingclient:billing-ktx:7.0.0"
|
|
52
53
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
|
|
53
54
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
|
54
|
-
|
|
55
|
+
compileOnly "com.facebook.android:facebook-android-sdk:17.+"
|
|
56
|
+
implementation "androidx.security:security-crypto:1.0.0"
|
|
57
|
+
implementation "com.android.installreferrer:installreferrer:2.2"
|
|
55
58
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# InstallReferrer — keep callback interface used via reflection
|
|
2
|
+
-keep interface com.android.installreferrer.api.InstallReferrerStateListener { *; }
|
|
3
|
+
-keep class com.android.installreferrer.** { *; }
|
|
4
|
+
|
|
5
|
+
# EncryptedSharedPreferences — uses reflection internally
|
|
6
|
+
-keep class androidx.security.crypto.** { *; }
|
|
7
|
+
|
|
8
|
+
# Paywallo SDK — keep ReactMethod-annotated methods and module classes
|
|
9
|
+
-keepclassmembers class com.paywallo.sdk.** {
|
|
10
|
+
@com.facebook.react.bridge.ReactMethod <methods>;
|
|
11
|
+
}
|
|
12
|
+
-keep class com.paywallo.sdk.**Module { *; }
|
|
13
|
+
-keep class com.paywallo.sdk.**Package { *; }
|
|
14
|
+
|
|
15
|
+
# WebView classes — keep for React Native view manager and JavascriptInterface
|
|
16
|
+
-keep class com.paywallo.sdk.PaywalloWebView { *; }
|
|
17
|
+
-keep class com.paywallo.sdk.PaywalloWebView$WebAppInterface { *; }
|
|
18
|
+
-keep class com.paywallo.sdk.PaywalloWebViewManager { *; }
|
|
19
|
+
-keep class com.paywallo.sdk.PaywalloWebViewEvent { *; }
|
|
20
|
+
|
|
21
|
+
# JavascriptInterface — keep annotated methods to prevent stripping
|
|
22
|
+
-keepclassmembers class com.paywallo.sdk.** {
|
|
23
|
+
@android.webkit.JavascriptInterface <methods>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
package com.paywallo.sdk
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.*
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import android.os.Environment
|
|
6
|
+
import android.os.StatFs
|
|
7
|
+
import android.app.ActivityManager
|
|
8
|
+
import android.content.Context
|
|
9
|
+
import android.provider.Settings
|
|
10
|
+
import android.telephony.TelephonyManager
|
|
11
|
+
import androidx.core.content.pm.PackageInfoCompat
|
|
12
|
+
import com.android.installreferrer.api.InstallReferrerClient
|
|
13
|
+
import com.android.installreferrer.api.InstallReferrerStateListener
|
|
14
|
+
|
|
15
|
+
class PaywalloDeviceModule(private val reactContext: ReactApplicationContext) :
|
|
16
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
17
|
+
|
|
18
|
+
override fun getName() = "PaywalloDevice"
|
|
19
|
+
|
|
20
|
+
@ReactMethod
|
|
21
|
+
fun getDeviceInfo(promise: Promise) {
|
|
22
|
+
try {
|
|
23
|
+
val map = Arguments.createMap()
|
|
24
|
+
|
|
25
|
+
// deviceId
|
|
26
|
+
try {
|
|
27
|
+
val deviceId = Settings.Secure.getString(
|
|
28
|
+
reactContext.contentResolver,
|
|
29
|
+
Settings.Secure.ANDROID_ID
|
|
30
|
+
)
|
|
31
|
+
map.putString("deviceId", if (deviceId.isNullOrEmpty()) "unknown" else deviceId)
|
|
32
|
+
} catch (e: Exception) {
|
|
33
|
+
map.putString("deviceId", "unknown")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
map.putString("model", Build.MODEL ?: "unknown")
|
|
37
|
+
map.putString("modelId", Build.DEVICE ?: "unknown")
|
|
38
|
+
map.putString("systemVersion", Build.VERSION.RELEASE ?: "unknown")
|
|
39
|
+
map.putString("systemName", "Android")
|
|
40
|
+
map.putString("brand", Build.BRAND ?: "unknown")
|
|
41
|
+
|
|
42
|
+
// appVersion, buildNumber, bundleId
|
|
43
|
+
try {
|
|
44
|
+
val packageName = reactContext.packageName
|
|
45
|
+
val pm = reactContext.packageManager
|
|
46
|
+
val pkgInfo = pm.getPackageInfo(packageName, 0)
|
|
47
|
+
map.putString("appVersion", pkgInfo.versionName ?: "unknown")
|
|
48
|
+
val buildNumber = PackageInfoCompat.getLongVersionCode(pkgInfo).toString()
|
|
49
|
+
map.putString("buildNumber", buildNumber)
|
|
50
|
+
map.putString("bundleId", packageName)
|
|
51
|
+
} catch (e: Exception) {
|
|
52
|
+
map.putString("appVersion", "unknown")
|
|
53
|
+
map.putString("buildNumber", "unknown")
|
|
54
|
+
map.putString("bundleId", reactContext.packageName ?: "unknown")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// totalDisk, freeDisk
|
|
58
|
+
try {
|
|
59
|
+
val stat = StatFs(Environment.getDataDirectory().path)
|
|
60
|
+
map.putDouble("totalDisk", stat.totalBytes.toDouble())
|
|
61
|
+
map.putDouble("freeDisk", stat.availableBytes.toDouble())
|
|
62
|
+
} catch (e: Exception) {
|
|
63
|
+
map.putDouble("totalDisk", 0.0)
|
|
64
|
+
map.putDouble("freeDisk", 0.0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// totalRam
|
|
68
|
+
try {
|
|
69
|
+
val activityManager = reactContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
70
|
+
val memInfo = ActivityManager.MemoryInfo()
|
|
71
|
+
activityManager.getMemoryInfo(memInfo)
|
|
72
|
+
map.putDouble("totalRam", memInfo.totalMem.toDouble())
|
|
73
|
+
} catch (e: Exception) {
|
|
74
|
+
map.putDouble("totalRam", 0.0)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// carrier
|
|
78
|
+
try {
|
|
79
|
+
val tm = reactContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
|
80
|
+
val carrier = tm.networkOperatorName
|
|
81
|
+
map.putString("carrier", if (carrier.isNullOrEmpty()) "unknown" else carrier)
|
|
82
|
+
} catch (e: Exception) {
|
|
83
|
+
map.putString("carrier", "unknown")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// installerPackage
|
|
87
|
+
try {
|
|
88
|
+
val packageName = reactContext.packageName
|
|
89
|
+
val pm = reactContext.packageManager
|
|
90
|
+
val installer = pm.getInstallerPackageName(packageName)
|
|
91
|
+
if (installer != null) {
|
|
92
|
+
map.putString("installerPackage", installer)
|
|
93
|
+
} else {
|
|
94
|
+
map.putNull("installerPackage")
|
|
95
|
+
}
|
|
96
|
+
} catch (e: Exception) {
|
|
97
|
+
map.putNull("installerPackage")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// idfv — iOS-only concept
|
|
101
|
+
map.putNull("idfv")
|
|
102
|
+
|
|
103
|
+
promise.resolve(map)
|
|
104
|
+
} catch (e: Exception) {
|
|
105
|
+
promise.resolve(Arguments.createMap())
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@ReactMethod
|
|
110
|
+
fun getInstallReferrer(promise: Promise) {
|
|
111
|
+
try {
|
|
112
|
+
val client = InstallReferrerClient.newBuilder(reactContext).build()
|
|
113
|
+
// resolved tracks whether the promise was already settled to prevent
|
|
114
|
+
// double-resolve when endConnection() triggers onInstallReferrerServiceDisconnected
|
|
115
|
+
var resolved = false
|
|
116
|
+
|
|
117
|
+
client.startConnection(object : InstallReferrerStateListener {
|
|
118
|
+
override fun onInstallReferrerSetupFinished(responseCode: Int) {
|
|
119
|
+
if (resolved) return
|
|
120
|
+
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
|
|
121
|
+
try {
|
|
122
|
+
val referrer = client.installReferrer.installReferrer
|
|
123
|
+
resolved = true
|
|
124
|
+
promise.resolve(referrer)
|
|
125
|
+
} catch (e: Exception) {
|
|
126
|
+
resolved = true
|
|
127
|
+
promise.resolve(null)
|
|
128
|
+
} finally {
|
|
129
|
+
client.endConnection()
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
resolved = true
|
|
133
|
+
client.endConnection()
|
|
134
|
+
promise.resolve(null)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override fun onInstallReferrerServiceDisconnected() {
|
|
139
|
+
if (resolved) return
|
|
140
|
+
resolved = true
|
|
141
|
+
promise.resolve(null)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
} catch (e: Exception) {
|
|
145
|
+
promise.resolve(null)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -20,7 +20,8 @@ class PaywalloFBBridgeModule(private val reactContext: ReactApplicationContext)
|
|
|
20
20
|
} else {
|
|
21
21
|
promise.resolve(anonId)
|
|
22
22
|
}
|
|
23
|
-
} catch (
|
|
23
|
+
} catch (t: Throwable) {
|
|
24
|
+
// catches NoClassDefFoundError when Facebook SDK is not present at runtime
|
|
24
25
|
promise.resolve(null)
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -32,7 +33,8 @@ class PaywalloFBBridgeModule(private val reactContext: ReactApplicationContext)
|
|
|
32
33
|
val logger = AppEventsLogger.newLogger(reactApplicationContext)
|
|
33
34
|
logger.logEvent(name, bundle)
|
|
34
35
|
promise.resolve(null)
|
|
35
|
-
} catch (
|
|
36
|
+
} catch (t: Throwable) {
|
|
37
|
+
// catches NoClassDefFoundError when Facebook SDK is not present at runtime
|
|
36
38
|
promise.resolve(null)
|
|
37
39
|
}
|
|
38
40
|
}
|
|
@@ -44,7 +46,8 @@ class PaywalloFBBridgeModule(private val reactContext: ReactApplicationContext)
|
|
|
44
46
|
val logger = AppEventsLogger.newLogger(reactApplicationContext)
|
|
45
47
|
logger.logPurchase(BigDecimal.valueOf(amount), Currency.getInstance(currency), bundle)
|
|
46
48
|
promise.resolve(null)
|
|
47
|
-
} catch (
|
|
49
|
+
} catch (t: Throwable) {
|
|
50
|
+
// catches NoClassDefFoundError when Facebook SDK is not present at runtime
|
|
48
51
|
promise.resolve(null)
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -53,8 +56,8 @@ class PaywalloFBBridgeModule(private val reactContext: ReactApplicationContext)
|
|
|
53
56
|
fun setUserID(userId: String) {
|
|
54
57
|
try {
|
|
55
58
|
AppEventsLogger.setUserID(userId)
|
|
56
|
-
} catch (
|
|
57
|
-
//
|
|
59
|
+
} catch (t: Throwable) {
|
|
60
|
+
// catches NoClassDefFoundError when Facebook SDK is not present at runtime
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -62,8 +65,8 @@ class PaywalloFBBridgeModule(private val reactContext: ReactApplicationContext)
|
|
|
62
65
|
fun flush() {
|
|
63
66
|
try {
|
|
64
67
|
AppEventsLogger.newLogger(reactApplicationContext).flush()
|
|
65
|
-
} catch (
|
|
66
|
-
//
|
|
68
|
+
} catch (t: Throwable) {
|
|
69
|
+
// catches NoClassDefFoundError when Facebook SDK is not present at runtime
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
@@ -9,7 +9,9 @@ class PaywalloSdkPackage : ReactPackage {
|
|
|
9
9
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
10
|
return listOf(
|
|
11
11
|
PaywalloStoreKitModule(reactContext),
|
|
12
|
-
PaywalloFBBridgeModule(reactContext)
|
|
12
|
+
PaywalloFBBridgeModule(reactContext),
|
|
13
|
+
PaywalloStorageModule(reactContext),
|
|
14
|
+
PaywalloDeviceModule(reactContext)
|
|
13
15
|
)
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
package com.paywallo.sdk
|
|
2
|
+
|
|
3
|
+
import android.content.SharedPreferences
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import androidx.security.crypto.EncryptedSharedPreferences
|
|
6
|
+
import androidx.security.crypto.MasterKey
|
|
7
|
+
import com.facebook.react.bridge.*
|
|
8
|
+
|
|
9
|
+
class PaywalloStorageModule(private val reactContext: ReactApplicationContext) :
|
|
10
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
11
|
+
|
|
12
|
+
override fun getName() = "PaywalloStorage"
|
|
13
|
+
|
|
14
|
+
private val TAG = "PaywalloStorage"
|
|
15
|
+
private val SECURE_FILE = "paywallo_secure"
|
|
16
|
+
private val REGULAR_FILE = "paywallo_storage"
|
|
17
|
+
// No prefix — TypeScript layer handles prefixing
|
|
18
|
+
|
|
19
|
+
private val encryptedPrefs: SharedPreferences by lazy {
|
|
20
|
+
try {
|
|
21
|
+
val masterKey = MasterKey.Builder(reactContext)
|
|
22
|
+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
23
|
+
.build()
|
|
24
|
+
EncryptedSharedPreferences.create(
|
|
25
|
+
reactContext,
|
|
26
|
+
SECURE_FILE,
|
|
27
|
+
masterKey,
|
|
28
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
29
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
30
|
+
)
|
|
31
|
+
} catch (e: Exception) {
|
|
32
|
+
Log.w(TAG, "EncryptedSharedPreferences unavailable, falling back to regular prefs: ${e.message}")
|
|
33
|
+
reactContext.getSharedPreferences(SECURE_FILE, android.content.Context.MODE_PRIVATE)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private val regularPrefs: SharedPreferences by lazy {
|
|
38
|
+
reactContext.getSharedPreferences(REGULAR_FILE, android.content.Context.MODE_PRIVATE)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@ReactMethod
|
|
42
|
+
fun secureGet(key: String, promise: Promise) {
|
|
43
|
+
try {
|
|
44
|
+
val value = encryptedPrefs.getString(key, null)
|
|
45
|
+
promise.resolve(value)
|
|
46
|
+
} catch (e: Exception) {
|
|
47
|
+
Log.w(TAG, "secureGet failed: ${e.message}")
|
|
48
|
+
promise.resolve(null)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@ReactMethod
|
|
53
|
+
fun secureSet(key: String, value: String, promise: Promise) {
|
|
54
|
+
try {
|
|
55
|
+
encryptedPrefs.edit().putString(key, value).apply()
|
|
56
|
+
promise.resolve(true)
|
|
57
|
+
} catch (e: Exception) {
|
|
58
|
+
Log.w(TAG, "secureSet failed: ${e.message}")
|
|
59
|
+
promise.resolve(false)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@ReactMethod
|
|
64
|
+
fun secureRemove(key: String, promise: Promise) {
|
|
65
|
+
try {
|
|
66
|
+
encryptedPrefs.edit().remove(key).apply()
|
|
67
|
+
promise.resolve(true)
|
|
68
|
+
} catch (e: Exception) {
|
|
69
|
+
Log.w(TAG, "secureRemove failed: ${e.message}")
|
|
70
|
+
promise.resolve(false)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@ReactMethod
|
|
75
|
+
fun get(key: String, promise: Promise) {
|
|
76
|
+
try {
|
|
77
|
+
val value = regularPrefs.getString(key, null)
|
|
78
|
+
promise.resolve(value)
|
|
79
|
+
} catch (e: Exception) {
|
|
80
|
+
Log.w(TAG, "get failed: ${e.message}")
|
|
81
|
+
promise.resolve(null)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@ReactMethod
|
|
86
|
+
fun set(key: String, value: String, promise: Promise) {
|
|
87
|
+
try {
|
|
88
|
+
regularPrefs.edit().putString(key, value).apply()
|
|
89
|
+
promise.resolve(true)
|
|
90
|
+
} catch (e: Exception) {
|
|
91
|
+
Log.w(TAG, "set failed: ${e.message}")
|
|
92
|
+
promise.resolve(false)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@ReactMethod
|
|
97
|
+
fun remove(key: String, promise: Promise) {
|
|
98
|
+
try {
|
|
99
|
+
regularPrefs.edit().remove(key).apply()
|
|
100
|
+
promise.resolve(true)
|
|
101
|
+
} catch (e: Exception) {
|
|
102
|
+
Log.w(TAG, "remove failed: ${e.message}")
|
|
103
|
+
promise.resolve(false)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -148,7 +148,7 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
@ReactMethod
|
|
151
|
-
fun purchase(productId: String, promise: Promise) {
|
|
151
|
+
fun purchase(productId: String, appAccountToken: String?, promise: Promise) {
|
|
152
152
|
scope.launch {
|
|
153
153
|
try {
|
|
154
154
|
if (!connectBilling()) {
|
|
@@ -174,6 +174,9 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
|
|
|
174
174
|
return@launch
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// If there's already a pending purchase, reject the previous promise
|
|
178
|
+
// before overwriting it to avoid orphaned promises
|
|
179
|
+
purchasePromise?.reject("PURCHASE_CANCELLED", "Superseded by a new purchase request")
|
|
177
180
|
purchasePromise = promise
|
|
178
181
|
|
|
179
182
|
val flowParamsBuilder = BillingFlowParams.newBuilder()
|
|
@@ -190,6 +193,10 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
|
|
|
190
193
|
listOf(productDetailsParams.build())
|
|
191
194
|
)
|
|
192
195
|
|
|
196
|
+
if (!appAccountToken.isNullOrBlank()) {
|
|
197
|
+
flowParamsBuilder.setObfuscatedAccountId(appAccountToken)
|
|
198
|
+
}
|
|
199
|
+
|
|
193
200
|
client.launchBillingFlow(activity, flowParamsBuilder.build())
|
|
194
201
|
} catch (e: Exception) {
|
|
195
202
|
purchasePromise = null
|
|
@@ -206,12 +213,18 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
|
|
|
206
213
|
BillingClient.BillingResponseCode.OK -> {
|
|
207
214
|
val purchase = purchases?.firstOrNull()
|
|
208
215
|
if (purchase != null) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
|
|
217
|
+
val map = WritableNativeMap()
|
|
218
|
+
map.putBoolean("pending", true)
|
|
219
|
+
promise.resolve(map)
|
|
220
|
+
} else {
|
|
221
|
+
val map = WritableNativeMap()
|
|
222
|
+
map.putString("transactionId", purchase.orderId ?: purchase.purchaseToken)
|
|
223
|
+
map.putString("productId", purchase.products.firstOrNull() ?: "")
|
|
224
|
+
map.putString("receipt", purchase.purchaseToken)
|
|
225
|
+
map.putDouble("transactionDate", purchase.purchaseTime.toDouble())
|
|
226
|
+
promise.resolve(map)
|
|
227
|
+
}
|
|
215
228
|
} else {
|
|
216
229
|
promise.resolve(null)
|
|
217
230
|
}
|
|
@@ -236,14 +249,19 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
|
|
|
236
249
|
|
|
237
250
|
val client = billingClient!!
|
|
238
251
|
|
|
239
|
-
// Query
|
|
252
|
+
// Query both SUBS and INAPP purchases — one-time purchases also need acknowledgement
|
|
240
253
|
val subsResult = client.queryPurchasesAsync(
|
|
241
254
|
QueryPurchasesParams.newBuilder()
|
|
242
255
|
.setProductType(BillingClient.ProductType.SUBS)
|
|
243
256
|
.build()
|
|
244
257
|
)
|
|
258
|
+
val inappResult = client.queryPurchasesAsync(
|
|
259
|
+
QueryPurchasesParams.newBuilder()
|
|
260
|
+
.setProductType(BillingClient.ProductType.INAPP)
|
|
261
|
+
.build()
|
|
262
|
+
)
|
|
245
263
|
|
|
246
|
-
val allPurchases = subsResult.purchasesList
|
|
264
|
+
val allPurchases = subsResult.purchasesList + inappResult.purchasesList
|
|
247
265
|
|
|
248
266
|
val purchase = allPurchases.firstOrNull {
|
|
249
267
|
it.orderId == transactionId || it.purchaseToken == transactionId
|
|
@@ -307,6 +325,9 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
|
|
|
307
325
|
}
|
|
308
326
|
|
|
309
327
|
override fun onCatalystInstanceDestroy() {
|
|
328
|
+
// Reject any pending purchase promise so the JS side doesn't hang forever
|
|
329
|
+
purchasePromise?.reject("MODULE_DESTROYED", "Module was destroyed before purchase completed")
|
|
330
|
+
purchasePromise = null
|
|
310
331
|
scope.cancel()
|
|
311
332
|
billingClient?.endConnection()
|
|
312
333
|
billingClient = null
|