@1selfworld/adchain-sdk-react-native 1.0.1

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 ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 1SelfWorld Labs
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # @adchain/expo-sdk
2
+
3
+ AdChain SDK for React Native with full Expo support.
4
+
5
+ ## 🚨 Important Notices
6
+
7
+ ### Expo Go Not Supported
8
+ This SDK includes native code and **does not work in Expo Go**.
9
+
10
+ You must use:
11
+ - βœ… Development Build: `npx expo run:android` or `npx expo run:ios`
12
+ - βœ… EAS Build: `eas build --profile development`
13
+
14
+ ### πŸ” Security Notice
15
+ app.json에 μ €μž₯된 API ν‚€λŠ” μ•± λ²ˆλ“€μ— ν¬ν•¨λ©λ‹ˆλ‹€.
16
+
17
+ **ꢌμž₯사항:**
18
+ - βœ… **개발/ν…ŒμŠ€νŠΈ:** app.json μ‚¬μš© OK
19
+ - ⚠️ **ν”„λ‘œλ•μ…˜:** ν™˜κ²½λ³„ ν‚€ 뢄리 ꢌμž₯
20
+ - Android: `buildConfigField` μ‚¬μš©
21
+ - iOS: `xcconfig` 파일 μ‚¬μš©
22
+ - ❌ **μ ˆλŒ€ κΈˆμ§€:** Private API ν‚€, 결제/인증 λΉ„λ°€ν‚€ λ…ΈμΆœ
23
+
24
+ **λŒ€μ•ˆ:**
25
+ ```typescript
26
+ // λŸ°νƒ€μž„μ— μ„œλ²„μ—μ„œ 토큰 λ°œκΈ‰
27
+ const token = await fetchTokenFromServer();
28
+ await AdchainSDK.initializeWithToken(token);
29
+ ```
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npx expo install @adchain/expo-sdk expo-constants
35
+ ```
36
+
37
+ **Note:** `expo-constants` is a peer dependency. Install it separately.
38
+
39
+ ## Configuration
40
+
41
+ Add to your `app.json`:
42
+
43
+ ```json
44
+ {
45
+ "expo": {
46
+ "plugins": [
47
+ [
48
+ "@adchain/expo-sdk",
49
+ {
50
+ "appKey": "YOUR_APP_KEY",
51
+ "appSecret": "YOUR_APP_SECRET",
52
+ "environment": "PRODUCTION"
53
+ }
54
+ ]
55
+ ]
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ```typescript
63
+ import { useEffect } from 'react';
64
+ import AdchainSDK from '@adchain/expo-sdk';
65
+
66
+ export default function App() {
67
+ useEffect(() => {
68
+ // Auto-initialize using app.json config
69
+ AdchainSDK.autoInitialize()
70
+ .then(() => console.log('SDK ready'))
71
+ .catch(console.error);
72
+ }, []);
73
+
74
+ const handleLogin = async () => {
75
+ await AdchainSDK.login({ userId: 'user123' });
76
+ };
77
+
78
+ const handleOfferwall = async () => {
79
+ await AdchainSDK.openOfferwall('placement-id');
80
+ };
81
+ }
82
+ ```
83
+
84
+ ## API Reference
85
+
86
+ ### Core Methods
87
+ - `autoInitialize()` - Auto-initialize using app.json config
88
+ - `initialize(config)` - Manual initialization
89
+ - `login(user)` - User login
90
+ - `logout()` - User logout
91
+ - `isLoggedIn()` - Check login status
92
+ - `getCurrentUser()` - Get current user info
93
+
94
+ ### Offerwall
95
+ - `openOfferwall(placementId?)` - Open offerwall
96
+ - `openAdjoeOfferwall(placementId?)` - Open ADJOE offerwall
97
+
98
+ ### Quiz & Mission
99
+ - `loadQuizList(unitId)` - Load quiz list
100
+ - `clickQuiz(unitId, quizId)` - Click quiz
101
+ - `loadMissionList(unitId)` - Load mission list
102
+ - `clickMission(unitId, missionId)` - Click mission
103
+ - `claimReward(unitId)` - Claim reward
104
+
105
+ ### Debug
106
+ - `getUserId()` - Get current user ID
107
+ - `getIFA()` - Get advertising ID
108
+ - `isInitialized()` - Check SDK initialization
109
+
110
+ ## πŸ”§ Technical Notes
111
+
112
+ ### Swift 5.5 (Not 5.9)
113
+ This SDK uses **Swift 5.5** due to known issues with optional parameter passing
114
+ in React Native bridges with Swift 5.9.
115
+
116
+ **Background:** In production, we experienced bugs where optional parameters in
117
+ Swift 5.9 were not properly passed to React Native, causing silent failures.
118
+
119
+ Tested and stable with:
120
+ - Xcode 14.x - 15.x
121
+ - Swift 5.5
122
+ - iOS 14.0+
123
+
124
+ ### Kotlin Standard Library
125
+ The SDK includes `kotlin-stdlib:1.9.21` as required by the native AdChain SDK.
126
+ If you experience version conflicts, Gradle will automatically resolve to the
127
+ host app's version.
128
+
129
+ ### React Native Dependency
130
+ The SDK uses `compileOnly` for `react-android` to avoid duplicate classes in
131
+ the AAR. The dependency is provided by the host application.
132
+
133
+ ## Build & Run
134
+
135
+ ```bash
136
+ # Prebuild (generate native code)
137
+ npx expo prebuild
138
+
139
+ # Verify plugin applied
140
+ cat android/settings.gradle | grep jitpack
141
+ cat ios/Podfile | grep AdChainSDK
142
+
143
+ # Run on Android
144
+ npx expo run:android
145
+
146
+ # Run on iOS
147
+ npx expo run:ios
148
+ ```
149
+
150
+ ## Requirements
151
+
152
+ - Expo SDK 50+
153
+ - React Native 0.74+
154
+ - iOS 14.0+
155
+ - Android API 24+
156
+ - **Development Build** (Expo Go not supported)
157
+
158
+ ## Troubleshooting
159
+
160
+ ### "SDK not initialized" error
161
+ - Make sure you added the plugin to app.json
162
+ - Check that appKey and appSecret are correct
163
+ - Verify you called `autoInitialize()` before other methods
164
+
165
+ ### "Expo config not found" error
166
+ - You're probably using Expo Go (not supported)
167
+ - Use: `npx expo run:android` or `npx expo run:ios`
168
+
169
+ ### "expo-constants not found"
170
+ - Install peer dependency: `npx expo install expo-constants`
171
+
172
+ ### Prebuild fails
173
+ - Check @expo/config-plugins is in devDependencies
174
+ - Try: `rm -rf node_modules && npm install`
175
+
176
+ ### JitPack not found (Android)
177
+ - Verify settings.gradle has maven { url "https://jitpack.io" }
178
+ - Check internet connection
179
+ - Try: cd android && ./gradlew clean
180
+
181
+ ### Pod install fails (iOS)
182
+ - Check Git URL is correct
183
+ - Verify tag v1.0.42 exists
184
+ - Try: cd ios && rm -rf Pods Podfile.lock && pod install
185
+
186
+ ## Version Compatibility
187
+
188
+ | Package Version | Android SDK | iOS SDK | Swift | Expo SDK |
189
+ |----------------|-------------|---------|-------|----------|
190
+ | 1.0.0 | v1.0.28 | v1.0.42 | 5.5 | 50-53 |
191
+
192
+ ## Known Limitations
193
+
194
+ ### v1.0.0
195
+ - OTA updates (expo-updates) not yet supported - use Development Build
196
+ - Swift 5.9 optional parameter bug - staying on 5.5
197
+ - CocoaPods Trunk: AdChainSDK uses Git Pod (not on Trunk yet)
198
+
199
+ ### Planned for v1.1.0
200
+ - expo-updates fallback support
201
+ - Conditional iOS plugin (Trunk vs Git)
202
+ - Server-side token initialization
203
+ - Enhanced error messages
204
+
205
+ ## License
206
+
207
+ Proprietary
208
+
209
+ ## Support
210
+
211
+ Email: contacts@1self.world
@@ -0,0 +1,25 @@
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 = "adchain-sdk-react-native"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => "14.0" }
14
+ s.source = { :git => "https://github.com/1selfworld-labs/adchain-sdk-react-native.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+
18
+ # Swift 5.5 maintained (avoiding 5.9 optional parameter bugs)
19
+ # In production, we experienced bugs where optional parameters in Swift 5.9
20
+ # were not properly passed to React Native bridges, causing silent failures.
21
+ s.swift_version = '5.5'
22
+
23
+ s.dependency "React-Core"
24
+ s.dependency "AdChainSDK"
25
+ end
@@ -0,0 +1,38 @@
1
+ def safeExtGet(prop, fallback) {
2
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
3
+ }
4
+
5
+ apply plugin: 'com.android.library'
6
+ apply plugin: 'org.jetbrains.kotlin.android'
7
+
8
+ android {
9
+ namespace "com.adchain.exposdk"
10
+
11
+ compileSdk safeExtGet('compileSdkVersion', 34)
12
+
13
+ defaultConfig {
14
+ minSdkVersion safeExtGet('minSdkVersion', 24)
15
+ targetSdkVersion safeExtGet('targetSdkVersion', 34)
16
+ }
17
+
18
+ compileOptions {
19
+ sourceCompatibility JavaVersion.VERSION_17
20
+ targetCompatibility JavaVersion.VERSION_17
21
+ }
22
+
23
+ kotlinOptions {
24
+ jvmTarget = '17'
25
+ }
26
+
27
+ lintOptions {
28
+ abortOnError false
29
+ }
30
+ }
31
+
32
+ // Repositories are managed by the host app's settings.gradle
33
+
34
+ dependencies {
35
+ implementation 'com.facebook.react:react-native:+'
36
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.21"
37
+ implementation 'com.github.1selfworld-labs:adchain-sdk-android:v1.0.28'
38
+ }
@@ -0,0 +1,288 @@
1
+ package com.adchain.exposdk
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.ReactContext
6
+ import com.facebook.react.bridge.ReadableArray
7
+ import com.facebook.react.bridge.WritableMap
8
+ import com.facebook.react.modules.core.DeviceEventManagerModule
9
+ import com.facebook.react.uimanager.SimpleViewManager
10
+ import com.facebook.react.uimanager.ThemedReactContext
11
+ import com.adchain.sdk.core.AdchainSdk
12
+ import com.adchain.sdk.offerwall.AdchainOfferwallView
13
+ import com.adchain.sdk.offerwall.OfferwallCallback
14
+ import com.adchain.sdk.offerwall.OfferwallEventCallback
15
+
16
+ class AdchainOfferwallViewManager : SimpleViewManager<AdchainOfferwallView>() {
17
+
18
+ companion object {
19
+ private const val TAG = "OfferwallViewManager"
20
+ private const val REACT_CLASS = "AdchainOfferwallView"
21
+ private const val COMMAND_LOAD_OFFERWALL = 1
22
+ private const val COMMAND_HANDLE_BACK_PRESS = 2
23
+ private const val COMMAND_SEND_DATA_RESPONSE = 3 // NEW
24
+ }
25
+
26
+ override fun getName() = REACT_CLASS
27
+
28
+ override fun createViewInstance(reactContext: ThemedReactContext): AdchainOfferwallView {
29
+ Log.d(TAG, "createViewInstance called")
30
+ return AdchainOfferwallView(reactContext)
31
+ }
32
+
33
+ override fun getCommandsMap(): Map<String, Int> {
34
+ Log.d(TAG, "getCommandsMap called")
35
+ return mapOf(
36
+ "loadOfferwall" to COMMAND_LOAD_OFFERWALL,
37
+ "handleBackPress" to COMMAND_HANDLE_BACK_PRESS,
38
+ "sendDataResponse" to COMMAND_SEND_DATA_RESPONSE // NEW
39
+ )
40
+ }
41
+
42
+ // Store pending data request responses
43
+ private val pendingDataResponses = mutableMapOf<String, Map<String, Any?>>()
44
+
45
+ override fun receiveCommand(
46
+ root: AdchainOfferwallView,
47
+ commandId: String?,
48
+ args: ReadableArray?
49
+ ) {
50
+ Log.d(TAG, "receiveCommand called - commandId: $commandId, args: $args")
51
+ when (commandId) {
52
+ "loadOfferwall" -> {
53
+ val placementId = args?.getString(0) ?: ""
54
+ Log.d(TAG, "loadOfferwall command - placementId: $placementId")
55
+ loadOfferwall(root, placementId)
56
+ }
57
+ "handleBackPress" -> {
58
+ Log.d(TAG, "handleBackPress command")
59
+ val handled = root.handleBackPress()
60
+ Log.d(TAG, "handleBackPress result: $handled")
61
+ }
62
+ "sendDataResponse" -> { // NEW
63
+ val requestId = args?.getString(0) ?: ""
64
+ val responseData = args?.getMap(1)?.toHashMap()
65
+ Log.d(TAG, "sendDataResponse command - requestId: $requestId, data: $responseData")
66
+ if (responseData != null) {
67
+ pendingDataResponses[requestId] = responseData
68
+ }
69
+ }
70
+ else -> {
71
+ Log.w(TAG, "Unknown command: $commandId")
72
+ }
73
+ }
74
+ }
75
+
76
+ private fun loadOfferwall(view: AdchainOfferwallView, placementId: String) {
77
+ Log.d(TAG, "loadOfferwall method called - placementId: $placementId")
78
+
79
+ if (placementId.isEmpty()) {
80
+ Log.w(TAG, "placementId is empty, returning")
81
+ return
82
+ }
83
+
84
+ try {
85
+ // Get SDK config, user, and offerwall URL
86
+ val config = AdchainSdk.getConfig()
87
+ val user = AdchainSdk.getCurrentUser()
88
+ val offerwallUrl = AdchainSdk.getOfferwallUrl()
89
+
90
+ Log.d(TAG, "SDK state - config: ${config != null}, user: ${user != null}, url: $offerwallUrl")
91
+
92
+ if (config == null) {
93
+ Log.e(TAG, "SDK not initialized")
94
+ sendErrorEvent(view, "SDK not initialized")
95
+ return
96
+ }
97
+
98
+ if (user == null) {
99
+ Log.e(TAG, "User not logged in")
100
+ sendErrorEvent(view, "User not logged in")
101
+ return
102
+ }
103
+
104
+ if (offerwallUrl.isNullOrEmpty()) {
105
+ Log.e(TAG, "Offerwall URL not available")
106
+ sendErrorEvent(view, "Offerwall URL not available")
107
+ return
108
+ }
109
+
110
+ Log.d(TAG, "About to call view.loadOfferwall()")
111
+ Log.d(TAG, "Parameters - baseUrl: $offerwallUrl, userId: ${user.userId}, appKey: ${config.appKey}, placementId: $placementId")
112
+
113
+ // Set callback for offerwall events
114
+ Log.d(TAG, "Setting callback...")
115
+ view.setCallback(object : OfferwallCallback {
116
+ override fun onOpened() {
117
+ sendEvent(view, "onOfferwallOpened", Arguments.createMap())
118
+ }
119
+
120
+ override fun onClosed() {
121
+ sendEvent(view, "onOfferwallClosed", Arguments.createMap())
122
+ }
123
+
124
+ override fun onError(message: String) {
125
+ val map = Arguments.createMap().apply {
126
+ putString("error", message)
127
+ }
128
+ sendEvent(view, "onOfferwallError", map)
129
+ }
130
+
131
+ override fun onRewardEarned(amount: Int) {
132
+ val map = Arguments.createMap().apply {
133
+ putInt("amount", amount)
134
+ }
135
+ sendEvent(view, "onRewardEarned", map)
136
+ }
137
+
138
+ override fun onHeightChanged(height: Int) {
139
+ Log.d(TAG, "onHeightChanged callback received - height: $height")
140
+ val map = Arguments.createMap().apply {
141
+ putInt("height", height)
142
+ }
143
+ sendEvent(view, "onHeightChange", map)
144
+ }
145
+ })
146
+
147
+ // NEW: Set event callback for custom events
148
+ Log.d(TAG, "Setting event callback...")
149
+ view.setEventCallback(object : OfferwallEventCallback {
150
+ override fun onCustomEvent(eventType: String, payload: Map<String, Any?>) {
151
+ Log.d(TAG, "onCustomEvent received - type: $eventType, payload: $payload")
152
+ // Sample μ•±μ—μ„œ μ²˜λ¦¬ν•˜λ„λ‘ 이벀트만 전달 (Toast ν‘œμ‹œ μ•ˆ 함)
153
+
154
+ val map = Arguments.createMap().apply {
155
+ putString("eventType", eventType)
156
+ putMap("payload", convertMapToWritableMap(payload))
157
+ }
158
+ sendEvent(view, "onCustomEvent", map)
159
+ }
160
+
161
+ override fun onDataRequest(
162
+ requestId: String,
163
+ requestType: String,
164
+ params: Map<String, Any?>
165
+ ): Map<String, Any?>? {
166
+ Log.d(TAG, "onDataRequest received - id: $requestId, type: $requestType, params: $params")
167
+
168
+ // Send event to React Native
169
+ val map = Arguments.createMap().apply {
170
+ putString("requestId", requestId)
171
+ putString("requestType", requestType)
172
+ putMap("params", convertMapToWritableMap(params))
173
+ }
174
+ sendEvent(view, "onDataRequest", map)
175
+
176
+ // Wait for response from React Native (with timeout)
177
+ val startTime = System.currentTimeMillis()
178
+ val timeout = 5000L // 5 seconds
179
+
180
+ while (System.currentTimeMillis() - startTime < timeout) {
181
+ val response = pendingDataResponses.remove(requestId)
182
+ if (response != null) {
183
+ Log.d(TAG, "Data response received for request: $requestId")
184
+ return response
185
+ }
186
+ Thread.sleep(10)
187
+ }
188
+
189
+ Log.w(TAG, "Data request timeout: $requestId")
190
+ return null
191
+ }
192
+ })
193
+
194
+ // Load offerwall
195
+ Log.d(TAG, "Calling view.loadOfferwall()...")
196
+ view.loadOfferwall(
197
+ baseUrl = offerwallUrl,
198
+ userId = user.userId,
199
+ appKey = config.appKey,
200
+ placementId = placementId
201
+ )
202
+ Log.d(TAG, "view.loadOfferwall() call completed")
203
+
204
+ } catch (e: Exception) {
205
+ Log.e(TAG, "Exception in loadOfferwall", e)
206
+ e.printStackTrace()
207
+ sendErrorEvent(view, "Failed to load offerwall: ${e.message}")
208
+ }
209
+ }
210
+
211
+ private fun sendEvent(view: AdchainOfferwallView, eventName: String, params: WritableMap) {
212
+ Log.d(TAG, "sendEvent called - eventName: $eventName, params: $params")
213
+ val reactContext = view.context as ReactContext
214
+ try {
215
+ reactContext
216
+ .getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
217
+ .receiveEvent(view.id, eventName, params)
218
+ Log.d(TAG, "sendEvent completed successfully")
219
+ } catch (e: Exception) {
220
+ Log.e(TAG, "sendEvent failed", e)
221
+ e.printStackTrace()
222
+ }
223
+ }
224
+
225
+ private fun sendErrorEvent(view: AdchainOfferwallView, error: String) {
226
+ val map = Arguments.createMap().apply {
227
+ putString("error", error)
228
+ }
229
+ sendEvent(view, "onOfferwallError", map)
230
+ }
231
+
232
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
233
+ return mapOf(
234
+ "onOfferwallOpened" to mapOf("registrationName" to "onOfferwallOpened"),
235
+ "onOfferwallClosed" to mapOf("registrationName" to "onOfferwallClosed"),
236
+ "onOfferwallError" to mapOf("registrationName" to "onOfferwallError"),
237
+ "onRewardEarned" to mapOf("registrationName" to "onRewardEarned"),
238
+ "onHeightChange" to mapOf("registrationName" to "onHeightChange"),
239
+ "onCustomEvent" to mapOf("registrationName" to "onCustomEvent"), // NEW
240
+ "onDataRequest" to mapOf("registrationName" to "onDataRequest") // NEW
241
+ )
242
+ }
243
+
244
+ // Helper function to convert Map to WritableMap
245
+ private fun convertMapToWritableMap(map: Map<String, Any?>): WritableMap {
246
+ val writableMap = Arguments.createMap()
247
+ map.forEach { (key, value) ->
248
+ when (value) {
249
+ null -> writableMap.putNull(key)
250
+ is String -> writableMap.putString(key, value)
251
+ is Int -> writableMap.putInt(key, value)
252
+ is Double -> writableMap.putDouble(key, value)
253
+ is Boolean -> writableMap.putBoolean(key, value)
254
+ is Map<*, *> -> {
255
+ @Suppress("UNCHECKED_CAST")
256
+ writableMap.putMap(key, convertMapToWritableMap(value as Map<String, Any?>))
257
+ }
258
+ is List<*> -> {
259
+ writableMap.putArray(key, convertListToWritableArray(value))
260
+ }
261
+ else -> Log.w(TAG, "Unsupported value type for key: $key")
262
+ }
263
+ }
264
+ return writableMap
265
+ }
266
+
267
+ private fun convertListToWritableArray(list: List<*>): com.facebook.react.bridge.WritableArray {
268
+ val writableArray = Arguments.createArray()
269
+ list.forEach { item ->
270
+ when (item) {
271
+ null -> writableArray.pushNull()
272
+ is String -> writableArray.pushString(item)
273
+ is Int -> writableArray.pushInt(item)
274
+ is Double -> writableArray.pushDouble(item)
275
+ is Boolean -> writableArray.pushBoolean(item)
276
+ is Map<*, *> -> {
277
+ @Suppress("UNCHECKED_CAST")
278
+ writableArray.pushMap(convertMapToWritableMap(item as Map<String, Any?>))
279
+ }
280
+ is List<*> -> {
281
+ writableArray.pushArray(convertListToWritableArray(item))
282
+ }
283
+ else -> Log.w(TAG, "Unsupported item type in list")
284
+ }
285
+ }
286
+ return writableArray
287
+ }
288
+ }