@virex-tech/paywallo-sdk 1.0.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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/MonetySdk.podspec +19 -0
  3. package/README.md +164 -0
  4. package/android/build.gradle +54 -0
  5. package/android/src/main/java/com/monety/sdk/MonetySdkPackage.kt +16 -0
  6. package/android/src/main/java/com/monety/sdk/PanelStoreKitModule.kt +315 -0
  7. package/android/src/main/java/com/monety/sdk/PanelWebView.kt +74 -0
  8. package/android/src/main/java/com/monety/sdk/PanelWebViewManager.kt +52 -0
  9. package/android/src/main/java/com/paywallo/sdk/PaywalloSdkPackage.kt +16 -0
  10. package/android/src/main/java/com/paywallo/sdk/PaywalloStoreKitModule.kt +315 -0
  11. package/android/src/main/java/com/paywallo/sdk/PaywalloWebView.kt +74 -0
  12. package/android/src/main/java/com/paywallo/sdk/PaywalloWebViewManager.kt +52 -0
  13. package/dist/index.d.mts +1141 -0
  14. package/dist/index.d.ts +1141 -0
  15. package/dist/index.js +4793 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/index.mjs +4709 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/ios/PanelStoreKit.m +21 -0
  20. package/ios/PanelStoreKit.swift +245 -0
  21. package/ios/PanelWebView.h +15 -0
  22. package/ios/PanelWebView.m +182 -0
  23. package/ios/PanelWebViewManager.m +41 -0
  24. package/ios/PanelWebViewModule.h +8 -0
  25. package/ios/PanelWebViewModule.m +42 -0
  26. package/ios/PaywalloStoreKit.m +21 -0
  27. package/ios/PaywalloStoreKit.swift +245 -0
  28. package/ios/PaywalloWebView.h +15 -0
  29. package/ios/PaywalloWebView.m +182 -0
  30. package/ios/PaywalloWebViewManager.m +41 -0
  31. package/ios/PaywalloWebViewModule.h +8 -0
  32. package/ios/PaywalloWebViewModule.m +42 -0
  33. package/package.json +93 -0
  34. package/react-native.config.js +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Monety
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "MonetySdk"
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.homepage = "https://github.com/monety/sdk-react-native"
10
+ s.license = package['license']
11
+ s.authors = { "Monety" => "support@monety.io" }
12
+ s.platforms = { :ios => "15.0" }
13
+ s.source = { :git => "https://github.com/monety/sdk-react-native.git", :tag => "v#{s.version}" }
14
+ s.source_files = "ios/**/*.{h,m,swift}"
15
+ s.frameworks = "WebKit", "StoreKit"
16
+ s.swift_version = "5.5"
17
+
18
+ s.dependency "React-Core"
19
+ end
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # @monety/sdk-react-native
2
+
3
+ SDK oficial para integração com Monety em aplicações React Native. Oferece Analytics, Feature Flags, Paywalls dinâmicas e gerenciamento de In-App Purchases.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ npm install @monety/sdk-react-native
9
+ ```
10
+
11
+ ### Peer Dependencies
12
+
13
+ O SDK requer as seguintes dependências instaladas no seu projeto:
14
+
15
+ ```bash
16
+ npm install @react-native-async-storage/async-storage @react-native-community/netinfo react-native-iap react-native-device-info
17
+ ```
18
+
19
+ #### Persistência de Device ID (Recomendado)
20
+
21
+ Para melhor persistência do device ID entre reinstalações do app (especialmente no iOS), instale também:
22
+
23
+ ```bash
24
+ npm install react-native-keychain
25
+ ```
26
+
27
+ Esta dependência é opcional, mas **altamente recomendada**. Com ela:
28
+ - **iOS**: Device ID persiste no Keychain mesmo após desinstalar o app
29
+ - **Android**: Device ID é armazenado de forma segura no Keystore
30
+
31
+ Sem ela, o SDK usa AsyncStorage como fallback (funciona, mas não persiste após desinstalação).
32
+
33
+ Para iOS:
34
+ ```bash
35
+ cd ios && pod install
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ### 1. Configure o Provider
41
+
42
+ ```tsx
43
+ import { PanelProvider } from '@monety/sdk-react-native';
44
+
45
+ export default function App() {
46
+ return (
47
+ <PanelProvider
48
+ config={{
49
+ appKey: 'sua_app_key',
50
+ debug: __DEV__,
51
+ }}
52
+ >
53
+ <YourApp />
54
+ </PanelProvider>
55
+ );
56
+ }
57
+ ```
58
+
59
+ ### 2. Use os Hooks
60
+
61
+ ```tsx
62
+ import { usePanel, useSubscription } from '@monety/sdk-react-native';
63
+
64
+ function MyComponent() {
65
+ const { presentPaywall, hasActiveSubscription } = usePanel();
66
+ const { subscription } = useSubscription();
67
+
68
+ const handlePurchase = async () => {
69
+ const hasAccess = await hasActiveSubscription();
70
+ if (!hasAccess) {
71
+ await presentPaywall('default');
72
+ }
73
+ };
74
+
75
+ return (
76
+ <Button onPress={handlePurchase}>
77
+ {subscription ? 'Premium' : 'Upgrade'}
78
+ </Button>
79
+ );
80
+ }
81
+ ```
82
+
83
+ ## API Principal
84
+
85
+ ### PanelProvider
86
+
87
+ Provider que inicializa o SDK e disponibiliza o contexto para toda a aplicação.
88
+
89
+ ```tsx
90
+ <PanelProvider config={PanelConfig}>
91
+ {children}
92
+ </PanelProvider>
93
+ ```
94
+
95
+ ### usePanel()
96
+
97
+ Hook principal com acesso a todas as funcionalidades:
98
+
99
+ - `isInitialized` - Se o SDK foi inicializado
100
+ - `distinctId` - ID único do usuário
101
+ - `presentPaywall(placement)` - Apresenta uma paywall
102
+ - `hasActiveSubscription()` - Verifica se há assinatura ativa
103
+ - `getSubscription()` - Obtém dados da assinatura
104
+ - `restorePurchases()` - Restaura compras anteriores
105
+
106
+ ### useSubscription()
107
+
108
+ Hook para gerenciamento de assinaturas:
109
+
110
+ - `subscription` - Dados da assinatura atual
111
+ - `isLoading` - Estado de carregamento
112
+ - `refresh()` - Atualiza dados da assinatura
113
+
114
+ ### Panel (API Imperativa)
115
+
116
+ Para uso fora de componentes React:
117
+
118
+ ```tsx
119
+ import { Panel } from '@monety/sdk-react-native';
120
+
121
+ // Identificar usuário
122
+ await Panel.identify('user_123', { email: 'user@example.com' });
123
+
124
+ // Rastrear evento
125
+ await Panel.track('purchase_completed', { product: 'premium' });
126
+
127
+ // Feature Flags
128
+ const variant = await Panel.getVariant('new_feature');
129
+ ```
130
+
131
+ ## Tipos
132
+
133
+ ```tsx
134
+ interface PanelConfig {
135
+ appKey: string;
136
+ baseUrl?: string;
137
+ debug?: boolean;
138
+ environment?: 'Production' | 'Sandbox';
139
+ }
140
+
141
+ interface Subscription {
142
+ productId: string;
143
+ status: 'active' | 'expired' | 'cancelled' | 'in_grace_period';
144
+ expiresAt: Date | null;
145
+ platform: 'ios' | 'android';
146
+ autoRenewEnabled: boolean;
147
+ }
148
+
149
+ interface PaywallResult {
150
+ presented: boolean;
151
+ purchased: boolean;
152
+ restored: boolean;
153
+ cancelled: boolean;
154
+ error?: Error;
155
+ }
156
+ ```
157
+
158
+ ## Suporte
159
+
160
+ Para dúvidas ou problemas, entre em contato com o suporte Monety.
161
+
162
+ ## Licença
163
+
164
+ MIT
@@ -0,0 +1,54 @@
1
+ buildscript {
2
+ ext.safeExtGet = {prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ repositories {
6
+ google()
7
+ mavenCentral()
8
+ }
9
+ dependencies {
10
+ classpath("com.android.tools.build:gradle:7.4.2")
11
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.9.22')}")
12
+ }
13
+ }
14
+
15
+ apply plugin: 'com.android.library'
16
+ apply plugin: 'kotlin-android'
17
+
18
+ android {
19
+ namespace "com.monety.sdk"
20
+ compileSdkVersion safeExtGet('compileSdkVersion', 34)
21
+
22
+ defaultConfig {
23
+ minSdkVersion safeExtGet('minSdkVersion', 24)
24
+ targetSdkVersion safeExtGet('targetSdkVersion', 34)
25
+ }
26
+
27
+ compileOptions {
28
+ sourceCompatibility JavaVersion.VERSION_17
29
+ targetCompatibility JavaVersion.VERSION_17
30
+ }
31
+
32
+ kotlinOptions {
33
+ jvmTarget = '17'
34
+ }
35
+
36
+ sourceSets {
37
+ main {
38
+ java.srcDirs = ['src/main/java']
39
+ }
40
+ }
41
+ }
42
+
43
+ repositories {
44
+ google()
45
+ mavenCentral()
46
+ }
47
+
48
+ dependencies {
49
+ implementation "com.facebook.react:react-native:+"
50
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '1.9.22')}"
51
+ implementation "com.android.billingclient:billing-ktx:7.0.0"
52
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
53
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
54
+ }
@@ -0,0 +1,16 @@
1
+ package com.monety.sdk
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class MonetySdkPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(PanelStoreKitModule(reactContext))
11
+ }
12
+
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
+ return listOf(PanelWebViewManager(reactContext))
15
+ }
16
+ }
@@ -0,0 +1,315 @@
1
+ package com.monety.sdk
2
+
3
+ import android.app.Activity
4
+ import com.android.billingclient.api.*
5
+ import com.facebook.react.bridge.*
6
+ import kotlinx.coroutines.*
7
+
8
+ class PanelStoreKitModule(private val reactContext: ReactApplicationContext) :
9
+ ReactContextBaseJavaModule(reactContext), PurchasesUpdatedListener {
10
+
11
+ private var billingClient: BillingClient? = null
12
+ private var purchasePromise: Promise? = null
13
+ private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
14
+
15
+ override fun getName() = "PanelStoreKit"
16
+
17
+ private fun ensureBillingClient(): BillingClient {
18
+ if (billingClient == null || billingClient?.isReady != true) {
19
+ billingClient = BillingClient.newBuilder(reactContext)
20
+ .setListener(this)
21
+ .enablePendingPurchases()
22
+ .build()
23
+ }
24
+ return billingClient!!
25
+ }
26
+
27
+ private suspend fun connectBilling(): Boolean {
28
+ val client = ensureBillingClient()
29
+ if (client.isReady) return true
30
+
31
+ return suspendCancellableCoroutine { cont ->
32
+ client.startConnection(object : BillingClientStateListener {
33
+ override fun onBillingSetupFinished(result: BillingResult) {
34
+ if (cont.isActive) {
35
+ cont.resume(result.responseCode == BillingClient.BillingResponseCode.OK) {}
36
+ }
37
+ }
38
+ override fun onBillingServiceDisconnected() {
39
+ if (cont.isActive) {
40
+ cont.resume(false) {}
41
+ }
42
+ }
43
+ })
44
+ }
45
+ }
46
+
47
+ @ReactMethod
48
+ fun getProducts(productIds: ReadableArray, promise: Promise) {
49
+ scope.launch {
50
+ try {
51
+ if (!connectBilling()) {
52
+ promise.reject("BILLING_ERROR", "Failed to connect to billing service")
53
+ return@launch
54
+ }
55
+
56
+ val ids = mutableListOf<String>()
57
+ for (i in 0 until productIds.size()) {
58
+ productIds.getString(i)?.let { ids.add(it) }
59
+ }
60
+
61
+ // Try subscriptions first, then one-time
62
+ val subsProducts = queryProducts(ids, BillingClient.ProductType.SUBS)
63
+ val inappProducts = queryProducts(ids, BillingClient.ProductType.INAPP)
64
+
65
+ val result = WritableNativeArray()
66
+
67
+ for (details in subsProducts + inappProducts) {
68
+ val map = WritableNativeMap()
69
+ map.putString("productId", details.productId)
70
+ map.putString("title", details.title)
71
+ map.putString("description", details.description)
72
+
73
+ val subDetails = details.subscriptionOfferDetails
74
+ if (subDetails != null && subDetails.isNotEmpty()) {
75
+ val offer = subDetails[0]
76
+ val pricingPhases = offer.pricingPhases.pricingPhaseList
77
+
78
+ // Find the main pricing phase (not trial)
79
+ val mainPhase = if (pricingPhases.size > 1) {
80
+ pricingPhases.last()
81
+ } else {
82
+ pricingPhases.first()
83
+ }
84
+
85
+ map.putString("price", (mainPhase.priceAmountMicros / 1_000_000.0).toString())
86
+ map.putDouble("priceValue", mainPhase.priceAmountMicros / 1_000_000.0)
87
+ map.putString("currency", mainPhase.priceCurrencyCode)
88
+ map.putString("localizedPrice", mainPhase.formattedPrice)
89
+ map.putString("type", "subscription")
90
+ map.putString("subscriptionPeriod", mainPhase.billingPeriod)
91
+
92
+ // Check for free trial
93
+ if (pricingPhases.size > 1) {
94
+ val trialPhase = pricingPhases.first()
95
+ if (trialPhase.priceAmountMicros == 0L) {
96
+ map.putString("freeTrialPeriod", trialPhase.billingPeriod)
97
+ } else {
98
+ map.putString("introductoryPrice", trialPhase.formattedPrice)
99
+ map.putDouble("introductoryPriceValue", trialPhase.priceAmountMicros / 1_000_000.0)
100
+ }
101
+ }
102
+ } else {
103
+ val oneTime = details.oneTimePurchaseOfferDetails
104
+ if (oneTime != null) {
105
+ map.putString("price", (oneTime.priceAmountMicros / 1_000_000.0).toString())
106
+ map.putDouble("priceValue", oneTime.priceAmountMicros / 1_000_000.0)
107
+ map.putString("currency", oneTime.priceCurrencyCode)
108
+ map.putString("localizedPrice", oneTime.formattedPrice)
109
+ map.putString("type", "non_consumable")
110
+ }
111
+ }
112
+
113
+ result.pushMap(map)
114
+ }
115
+
116
+ promise.resolve(result)
117
+ } catch (e: Exception) {
118
+ promise.reject("PRODUCTS_ERROR", "Failed to load products: ${e.message}", e)
119
+ }
120
+ }
121
+ }
122
+
123
+ private suspend fun queryProducts(ids: List<String>, type: String): List<ProductDetails> {
124
+ val client = billingClient ?: return emptyList()
125
+
126
+ val productList = ids.map { id ->
127
+ QueryProductDetailsParams.Product.newBuilder()
128
+ .setProductId(id)
129
+ .setProductType(type)
130
+ .build()
131
+ }
132
+
133
+ val params = QueryProductDetailsParams.newBuilder()
134
+ .setProductList(productList)
135
+ .build()
136
+
137
+ return suspendCancellableCoroutine { cont ->
138
+ client.queryProductDetailsAsync(params) { result, detailsList ->
139
+ if (cont.isActive) {
140
+ if (result.responseCode == BillingClient.BillingResponseCode.OK) {
141
+ cont.resume(detailsList ?: emptyList()) {}
142
+ } else {
143
+ cont.resume(emptyList()) {}
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ @ReactMethod
151
+ fun purchase(productId: String, promise: Promise) {
152
+ scope.launch {
153
+ try {
154
+ if (!connectBilling()) {
155
+ promise.reject("BILLING_ERROR", "Failed to connect to billing service")
156
+ return@launch
157
+ }
158
+
159
+ val client = billingClient!!
160
+ val activity = currentActivity
161
+
162
+ if (activity == null) {
163
+ promise.reject("NO_ACTIVITY", "No current activity")
164
+ return@launch
165
+ }
166
+
167
+ // Query product details
168
+ val subsProducts = queryProducts(listOf(productId), BillingClient.ProductType.SUBS)
169
+ val inappProducts = queryProducts(listOf(productId), BillingClient.ProductType.INAPP)
170
+ val details = (subsProducts + inappProducts).firstOrNull()
171
+
172
+ if (details == null) {
173
+ promise.reject("PRODUCT_NOT_FOUND", "Product $productId not found")
174
+ return@launch
175
+ }
176
+
177
+ purchasePromise = promise
178
+
179
+ val flowParamsBuilder = BillingFlowParams.newBuilder()
180
+
181
+ val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
182
+ .setProductDetails(details)
183
+
184
+ // For subscriptions, set the offer token
185
+ details.subscriptionOfferDetails?.firstOrNull()?.let { offer ->
186
+ productDetailsParams.setOfferToken(offer.offerToken)
187
+ }
188
+
189
+ flowParamsBuilder.setProductDetailsParamsList(
190
+ listOf(productDetailsParams.build())
191
+ )
192
+
193
+ client.launchBillingFlow(activity, flowParamsBuilder.build())
194
+ } catch (e: Exception) {
195
+ purchasePromise = null
196
+ promise.reject("PURCHASE_ERROR", "Purchase failed: ${e.message}", e)
197
+ }
198
+ }
199
+ }
200
+
201
+ override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
202
+ val promise = purchasePromise ?: return
203
+ purchasePromise = null
204
+
205
+ when (result.responseCode) {
206
+ BillingClient.BillingResponseCode.OK -> {
207
+ val purchase = purchases?.firstOrNull()
208
+ if (purchase != null) {
209
+ val map = WritableNativeMap()
210
+ map.putString("transactionId", purchase.orderId ?: purchase.purchaseToken)
211
+ map.putString("productId", purchase.products.firstOrNull() ?: "")
212
+ map.putString("receipt", purchase.purchaseToken)
213
+ map.putDouble("transactionDate", purchase.purchaseTime.toDouble())
214
+ promise.resolve(map)
215
+ } else {
216
+ promise.resolve(null)
217
+ }
218
+ }
219
+ BillingClient.BillingResponseCode.USER_CANCELED -> {
220
+ promise.resolve(null)
221
+ }
222
+ else -> {
223
+ promise.reject("PURCHASE_ERROR", "Purchase failed: ${result.debugMessage}")
224
+ }
225
+ }
226
+ }
227
+
228
+ @ReactMethod
229
+ fun finishTransaction(transactionId: String, promise: Promise) {
230
+ scope.launch {
231
+ try {
232
+ if (!connectBilling()) {
233
+ promise.resolve(null)
234
+ return@launch
235
+ }
236
+
237
+ val client = billingClient!!
238
+
239
+ // Query purchases to find the one to acknowledge
240
+ val subsResult = client.queryPurchasesAsync(
241
+ QueryPurchasesParams.newBuilder()
242
+ .setProductType(BillingClient.ProductType.SUBS)
243
+ .build()
244
+ )
245
+
246
+ val allPurchases = subsResult.purchasesList
247
+
248
+ val purchase = allPurchases.firstOrNull {
249
+ it.orderId == transactionId || it.purchaseToken == transactionId
250
+ }
251
+
252
+ if (purchase != null && !purchase.isAcknowledged) {
253
+ val ackParams = AcknowledgePurchaseParams.newBuilder()
254
+ .setPurchaseToken(purchase.purchaseToken)
255
+ .build()
256
+
257
+ client.acknowledgePurchase(ackParams) { ackResult ->
258
+ if (ackResult.responseCode == BillingClient.BillingResponseCode.OK) {
259
+ promise.resolve(null)
260
+ } else {
261
+ promise.reject("ACK_ERROR", "Failed to acknowledge: ${ackResult.debugMessage}")
262
+ }
263
+ }
264
+ } else {
265
+ promise.resolve(null)
266
+ }
267
+ } catch (e: Exception) {
268
+ promise.resolve(null)
269
+ }
270
+ }
271
+ }
272
+
273
+ @ReactMethod
274
+ fun getActiveTransactions(promise: Promise) {
275
+ scope.launch {
276
+ try {
277
+ if (!connectBilling()) {
278
+ promise.resolve(WritableNativeArray())
279
+ return@launch
280
+ }
281
+
282
+ val client = billingClient!!
283
+ val result = WritableNativeArray()
284
+
285
+ val subsResult = client.queryPurchasesAsync(
286
+ QueryPurchasesParams.newBuilder()
287
+ .setProductType(BillingClient.ProductType.SUBS)
288
+ .build()
289
+ )
290
+
291
+ for (purchase in subsResult.purchasesList) {
292
+ if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
293
+ val map = WritableNativeMap()
294
+ map.putString("transactionId", purchase.orderId ?: purchase.purchaseToken)
295
+ map.putString("productId", purchase.products.firstOrNull() ?: "")
296
+ map.putString("receipt", purchase.purchaseToken)
297
+ map.putDouble("transactionDate", purchase.purchaseTime.toDouble())
298
+ result.pushMap(map)
299
+ }
300
+ }
301
+
302
+ promise.resolve(result)
303
+ } catch (e: Exception) {
304
+ promise.resolve(WritableNativeArray())
305
+ }
306
+ }
307
+ }
308
+
309
+ override fun onCatalystInstanceDestroy() {
310
+ scope.cancel()
311
+ billingClient?.endConnection()
312
+ billingClient = null
313
+ super.onCatalystInstanceDestroy()
314
+ }
315
+ }
@@ -0,0 +1,74 @@
1
+ package com.monety.sdk
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.webkit.JavascriptInterface
5
+ import android.webkit.WebChromeClient
6
+ import android.webkit.WebSettings
7
+ import android.webkit.WebView
8
+ import android.webkit.WebViewClient
9
+ import com.facebook.react.bridge.Arguments
10
+ import com.facebook.react.bridge.ReactContext
11
+ import com.facebook.react.uimanager.events.RCTEventEmitter
12
+
13
+ @SuppressLint("SetJavaScriptEnabled", "ViewConstructor")
14
+ class PanelWebView(context: ReactContext) : WebView(context) {
15
+
16
+ private val reactContext = context
17
+
18
+ init {
19
+ setupWebView()
20
+ }
21
+
22
+ private fun setupWebView() {
23
+ settings.apply {
24
+ javaScriptEnabled = true
25
+ domStorageEnabled = true
26
+ mediaPlaybackRequiresUserGesture = false
27
+ mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
28
+ cacheMode = WebSettings.LOAD_DEFAULT
29
+ }
30
+
31
+ webViewClient = object : WebViewClient() {
32
+ override fun onPageFinished(view: WebView?, url: String?) {
33
+ super.onPageFinished(view, url)
34
+ dispatchEvent("onLoadEnd", null)
35
+ }
36
+ }
37
+
38
+ webChromeClient = WebChromeClient()
39
+
40
+ addJavascriptInterface(WebAppInterface(), "ReactNativeWebView")
41
+ }
42
+
43
+ private var pendingHtml: String? = null
44
+
45
+ fun setSourceUrl(url: String?) {
46
+ if (pendingHtml.isNullOrEmpty()) {
47
+ url?.let { loadUrl(it) }
48
+ }
49
+ }
50
+
51
+ fun setSourceHtml(html: String?) {
52
+ pendingHtml = html
53
+ html?.let { loadDataWithBaseURL("https://app.monety.io", it, "text/html", "UTF-8", null) }
54
+ }
55
+
56
+ fun injectJavaScript(script: String) {
57
+ evaluateJavascript(script, null)
58
+ }
59
+
60
+ private fun dispatchEvent(eventName: String, data: String?) {
61
+ val event = Arguments.createMap().apply {
62
+ data?.let { putString("data", it) }
63
+ }
64
+ reactContext.getJSModule(RCTEventEmitter::class.java)
65
+ .receiveEvent(id, eventName, event)
66
+ }
67
+
68
+ inner class WebAppInterface {
69
+ @JavascriptInterface
70
+ fun postMessage(message: String) {
71
+ post { dispatchEvent("onMessage", message) }
72
+ }
73
+ }
74
+ }