@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.
- package/LICENSE +21 -0
- package/MonetySdk.podspec +19 -0
- package/README.md +164 -0
- package/android/build.gradle +54 -0
- package/android/src/main/java/com/monety/sdk/MonetySdkPackage.kt +16 -0
- package/android/src/main/java/com/monety/sdk/PanelStoreKitModule.kt +315 -0
- package/android/src/main/java/com/monety/sdk/PanelWebView.kt +74 -0
- package/android/src/main/java/com/monety/sdk/PanelWebViewManager.kt +52 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloSdkPackage.kt +16 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloStoreKitModule.kt +315 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloWebView.kt +74 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloWebViewManager.kt +52 -0
- package/dist/index.d.mts +1141 -0
- package/dist/index.d.ts +1141 -0
- package/dist/index.js +4793 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4709 -0
- package/dist/index.mjs.map +1 -0
- package/ios/PanelStoreKit.m +21 -0
- package/ios/PanelStoreKit.swift +245 -0
- package/ios/PanelWebView.h +15 -0
- package/ios/PanelWebView.m +182 -0
- package/ios/PanelWebViewManager.m +41 -0
- package/ios/PanelWebViewModule.h +8 -0
- package/ios/PanelWebViewModule.m +42 -0
- package/ios/PaywalloStoreKit.m +21 -0
- package/ios/PaywalloStoreKit.swift +245 -0
- package/ios/PaywalloWebView.h +15 -0
- package/ios/PaywalloWebView.m +182 -0
- package/ios/PaywalloWebViewManager.m +41 -0
- package/ios/PaywalloWebViewModule.h +8 -0
- package/ios/PaywalloWebViewModule.m +42 -0
- package/package.json +93 -0
- 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
|
+
}
|