@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 +20 -0
- package/README.md +211 -0
- package/adchain-sdk-react-native.podspec +25 -0
- package/android/build.gradle +38 -0
- package/android/src/main/java/com/adchain/exposdk/AdchainOfferwallViewManager.kt +288 -0
- package/android/src/main/java/com/adchain/exposdk/AdchainSdkModule.kt +742 -0
- package/android/src/main/java/com/adchain/exposdk/AdchainSdkPackage.kt +14 -0
- package/app.plugin.js +17 -0
- package/ios/AdchainOfferwallViewManager.m +17 -0
- package/ios/AdchainOfferwallViewManager.swift +242 -0
- package/ios/AdchainSdk.m +87 -0
- package/ios/AdchainSdk.swift +792 -0
- package/lib/module/index.js +162 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +88 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +153 -0
- package/plugin/src/withAdchainAndroid.js +56 -0
- package/plugin/src/withAdchainConfig.js +22 -0
- package/plugin/src/withAdchainIOS.js +60 -0
- package/react-native.config.js +8 -0
- package/src/index.tsx +274 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
package com.adchain.exposdk
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.*
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
6
|
+
import com.adchain.sdk.core.*
|
|
7
|
+
import com.adchain.sdk.quiz.*
|
|
8
|
+
import com.adchain.sdk.quiz.models.QuizEvent
|
|
9
|
+
import com.adchain.sdk.mission.*
|
|
10
|
+
import com.adchain.sdk.offerwall.*
|
|
11
|
+
import android.app.Activity
|
|
12
|
+
import android.os.Handler
|
|
13
|
+
import android.os.Looper
|
|
14
|
+
|
|
15
|
+
@ReactModule(name = AdchainSdkModule.NAME)
|
|
16
|
+
class AdchainSdkModule(private val reactContext: ReactApplicationContext)
|
|
17
|
+
: ReactContextBaseJavaModule(reactContext) {
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
const val NAME = "AdchainSdk"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun getName() = NAME
|
|
24
|
+
|
|
25
|
+
// 내부 인스턴스 관리 (자동 생성/캐싱)
|
|
26
|
+
private val quizInstances = mutableMapOf<String, AdchainQuiz>()
|
|
27
|
+
private val missionInstances = mutableMapOf<String, AdchainMission>()
|
|
28
|
+
|
|
29
|
+
// ===== 1. SDK 초기화 =====
|
|
30
|
+
|
|
31
|
+
@ReactMethod
|
|
32
|
+
fun initialize(appKey: String, appSecret: String, options: ReadableMap?, promise: Promise) {
|
|
33
|
+
try {
|
|
34
|
+
// options에서 environment와 timeout 추출
|
|
35
|
+
val environment = options?.getString("environment")
|
|
36
|
+
val timeout = if (options?.hasKey("timeout") == true && !options.isNull("timeout")) {
|
|
37
|
+
options.getDouble("timeout")
|
|
38
|
+
} else null
|
|
39
|
+
|
|
40
|
+
val env = when(environment?.uppercase()) {
|
|
41
|
+
"STAGING" -> AdchainSdkConfig.Environment.STAGING
|
|
42
|
+
"DEVELOPMENT" -> AdchainSdkConfig.Environment.DEVELOPMENT
|
|
43
|
+
else -> AdchainSdkConfig.Environment.PRODUCTION
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
val configBuilder = AdchainSdkConfig.Builder(appKey, appSecret)
|
|
47
|
+
.setEnvironment(env)
|
|
48
|
+
|
|
49
|
+
timeout?.let {
|
|
50
|
+
configBuilder.setTimeout(it.toLong())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
val config = configBuilder.build()
|
|
54
|
+
|
|
55
|
+
// Activity가 null인 경우 최대 1초간 대기
|
|
56
|
+
if (reactContext.currentActivity == null) {
|
|
57
|
+
val handler = Handler(Looper.getMainLooper())
|
|
58
|
+
var attempts = 0
|
|
59
|
+
val maxAttempts = 10 // 최대 10회 시도 (1초)
|
|
60
|
+
|
|
61
|
+
val runnable = object : Runnable {
|
|
62
|
+
override fun run() {
|
|
63
|
+
attempts++
|
|
64
|
+
val activity = reactContext.currentActivity
|
|
65
|
+
|
|
66
|
+
if (activity != null) {
|
|
67
|
+
// Activity가 준비됨, SDK 초기화 진행
|
|
68
|
+
AdchainSdk.initialize(activity.application, config)
|
|
69
|
+
promise.resolve(createResponse(true, "SDK initialized successfully"))
|
|
70
|
+
} else if (attempts < maxAttempts) {
|
|
71
|
+
// 100ms 후 재시도
|
|
72
|
+
handler.postDelayed(this, 100)
|
|
73
|
+
} else {
|
|
74
|
+
// 최대 시도 횟수 초과
|
|
75
|
+
promise.reject("INIT_ERROR", "Current activity is null after waiting 1 second")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 첫 시도는 100ms 후에
|
|
81
|
+
handler.postDelayed(runnable, 100)
|
|
82
|
+
} else {
|
|
83
|
+
// Activity가 이미 준비되어 있음
|
|
84
|
+
AdchainSdk.initialize(reactContext.currentActivity!!.application, config)
|
|
85
|
+
promise.resolve(createResponse(true, "SDK initialized successfully"))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
} catch (t: Throwable) {
|
|
89
|
+
promise.reject("INIT_ERROR", t.message, t)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ===== 2. 인증 관련 (4개) =====
|
|
94
|
+
|
|
95
|
+
@ReactMethod
|
|
96
|
+
fun login(userId: String, userInfo: ReadableMap?, promise: Promise) {
|
|
97
|
+
try {
|
|
98
|
+
val userBuilder = AdchainSdkUser.Builder(userId)
|
|
99
|
+
|
|
100
|
+
// userInfo에서 각 필드 추출
|
|
101
|
+
val gender = userInfo?.getString("gender")
|
|
102
|
+
val birthYear = if (userInfo?.hasKey("birthYear") == true && !userInfo.isNull("birthYear")) {
|
|
103
|
+
userInfo.getDouble("birthYear").toInt()
|
|
104
|
+
} else null
|
|
105
|
+
|
|
106
|
+
gender?.let {
|
|
107
|
+
val genderEnum = when(it.uppercase()) {
|
|
108
|
+
"MALE", "M" -> AdchainSdkUser.Gender.MALE
|
|
109
|
+
"FEMALE", "F" -> AdchainSdkUser.Gender.FEMALE
|
|
110
|
+
else -> null // Android SDK에 OTHER가 없음
|
|
111
|
+
}
|
|
112
|
+
genderEnum?.let { g ->
|
|
113
|
+
userBuilder.setGender(g)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
birthYear?.let {
|
|
118
|
+
userBuilder.setBirthYear(it)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// customProperties 처리
|
|
122
|
+
val customProperties = userInfo?.getMap("customProperties")
|
|
123
|
+
customProperties?.let { props ->
|
|
124
|
+
val iterator = props.keySetIterator()
|
|
125
|
+
while (iterator.hasNextKey()) {
|
|
126
|
+
val key = iterator.nextKey()
|
|
127
|
+
val value = props.getString(key)
|
|
128
|
+
if (value != null) {
|
|
129
|
+
userBuilder.setCustomProperty(key, value)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
val user = userBuilder.build()
|
|
135
|
+
|
|
136
|
+
AdchainSdk.login(user, object : AdchainSdkLoginListener {
|
|
137
|
+
override fun onSuccess() {
|
|
138
|
+
promise.resolve(createResponse(true, "Login successful"))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override fun onFailure(errorType: AdchainSdkLoginListener.ErrorType) {
|
|
142
|
+
val errorMessage = when (errorType) {
|
|
143
|
+
AdchainSdkLoginListener.ErrorType.NOT_INITIALIZED -> "SDK not initialized"
|
|
144
|
+
AdchainSdkLoginListener.ErrorType.INVALID_USER_ID -> "Invalid user ID"
|
|
145
|
+
AdchainSdkLoginListener.ErrorType.AUTHENTICATION_FAILED -> "Authentication failed"
|
|
146
|
+
AdchainSdkLoginListener.ErrorType.NETWORK_ERROR -> "Network error"
|
|
147
|
+
AdchainSdkLoginListener.ErrorType.UNKNOWN -> "Unknown error"
|
|
148
|
+
}
|
|
149
|
+
promise.reject("LOGIN_ERROR", errorMessage)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
} catch (t: Throwable) {
|
|
153
|
+
promise.reject("LOGIN_ERROR", t.message, t)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@ReactMethod
|
|
158
|
+
fun logout(promise: Promise) {
|
|
159
|
+
try {
|
|
160
|
+
AdchainSdk.logout()
|
|
161
|
+
promise.resolve(createResponse(true, "Logout successful"))
|
|
162
|
+
} catch (t: Throwable) {
|
|
163
|
+
promise.reject("LOGOUT_ERROR", t.message, t)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@ReactMethod
|
|
168
|
+
fun isLoggedIn(promise: Promise) {
|
|
169
|
+
try {
|
|
170
|
+
promise.resolve(AdchainSdk.isLoggedIn)
|
|
171
|
+
} catch (t: Throwable) {
|
|
172
|
+
promise.reject("ERROR", t.message, t)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@ReactMethod
|
|
177
|
+
fun getCurrentUser(promise: Promise) {
|
|
178
|
+
try {
|
|
179
|
+
val user = AdchainSdk.getCurrentUser()
|
|
180
|
+
if (user != null) {
|
|
181
|
+
val map = Arguments.createMap().apply {
|
|
182
|
+
putString("userId", user.userId)
|
|
183
|
+
user.gender?.let { putString("gender", it.name) }
|
|
184
|
+
user.birthYear?.let { putInt("birthYear", it) }
|
|
185
|
+
}
|
|
186
|
+
promise.resolve(map)
|
|
187
|
+
} else {
|
|
188
|
+
promise.resolve(null)
|
|
189
|
+
}
|
|
190
|
+
} catch (t: Throwable) {
|
|
191
|
+
promise.reject("ERROR", t.message, t)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ===== 3. Quiz 관련 (2개) =====
|
|
196
|
+
|
|
197
|
+
@ReactMethod
|
|
198
|
+
fun loadQuizList(unitId: String, promise: Promise) {
|
|
199
|
+
try {
|
|
200
|
+
// 인스턴스 자동 생성/재사용
|
|
201
|
+
val quiz = quizInstances.getOrPut(unitId) {
|
|
202
|
+
AdchainQuiz()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
quiz.getQuizList(
|
|
206
|
+
onSuccess = { quizResponse ->
|
|
207
|
+
val responseMap = Arguments.createMap().apply {
|
|
208
|
+
putBoolean("success", quizResponse.success ?: true)
|
|
209
|
+
putString("titleText", quizResponse.titleText)
|
|
210
|
+
putString("completedImageUrl", quizResponse.completedImageUrl)
|
|
211
|
+
quizResponse.completedImageWidth?.let { putInt("completedImageWidth", it) }
|
|
212
|
+
quizResponse.completedImageHeight?.let { putInt("completedImageHeight", it) }
|
|
213
|
+
putString("message", quizResponse.message)
|
|
214
|
+
|
|
215
|
+
// events 배열 처리
|
|
216
|
+
val eventsArray = Arguments.createArray()
|
|
217
|
+
quizResponse.events.forEach { quizEvent ->
|
|
218
|
+
val eventMap = Arguments.createMap().apply {
|
|
219
|
+
putString("id", quizEvent.id)
|
|
220
|
+
putString("title", quizEvent.title)
|
|
221
|
+
putString("description", quizEvent.description ?: "")
|
|
222
|
+
putString("imageUrl", quizEvent.imageUrl)
|
|
223
|
+
putString("point", quizEvent.point)
|
|
224
|
+
putBoolean("isCompleted", quizEvent.completed ?: false)
|
|
225
|
+
}
|
|
226
|
+
eventsArray.pushMap(eventMap)
|
|
227
|
+
}
|
|
228
|
+
putArray("events", eventsArray)
|
|
229
|
+
}
|
|
230
|
+
promise.resolve(responseMap)
|
|
231
|
+
},
|
|
232
|
+
onFailure = { error ->
|
|
233
|
+
promise.reject("QUIZ_LOAD_ERROR", error.toString())
|
|
234
|
+
},
|
|
235
|
+
shouldStoreCallbacks = true // 콜백 저장하여 자동 refresh 가능하게 함
|
|
236
|
+
)
|
|
237
|
+
} catch (t: Throwable) {
|
|
238
|
+
promise.reject("QUIZ_ERROR", t.message, t)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@ReactMethod
|
|
243
|
+
fun clickQuiz(unitId: String, quizId: String, promise: Promise) {
|
|
244
|
+
try {
|
|
245
|
+
val quiz = quizInstances.getOrPut(unitId) {
|
|
246
|
+
AdchainQuiz()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// iOS와 동일한 방식: 리스너 설정
|
|
250
|
+
quiz.setQuizEventsListener(object : AdchainQuizEventsListener {
|
|
251
|
+
override fun onImpressed(quizEvent: QuizEvent) {
|
|
252
|
+
// 필요시 처리
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
override fun onClicked(quizEvent: QuizEvent) {
|
|
256
|
+
// 필요시 처리
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
override fun onQuizCompleted(quizEvent: QuizEvent, score: Int) {
|
|
260
|
+
// React Native로 이벤트 전송
|
|
261
|
+
reactContext
|
|
262
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
263
|
+
.emit("onQuizCompleted", Arguments.createMap().apply {
|
|
264
|
+
putString("unitId", unitId)
|
|
265
|
+
putString("quizId", quizId)
|
|
266
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
quiz.clickQuiz(quizId)
|
|
272
|
+
promise.resolve(createResponse(true, "Quiz clicked"))
|
|
273
|
+
} catch (t: Throwable) {
|
|
274
|
+
promise.reject("QUIZ_ERROR", t.message, t)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ===== 4. Mission 관련 (3개) =====
|
|
279
|
+
|
|
280
|
+
@ReactMethod
|
|
281
|
+
fun loadMissionList(unitId: String, promise: Promise) {
|
|
282
|
+
try {
|
|
283
|
+
// 인스턴스 자동 생성/재사용
|
|
284
|
+
val mission = missionInstances.getOrPut(unitId) {
|
|
285
|
+
AdchainMission()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// iOS와 동일하게 loadMissionList에서도 리스너 설정
|
|
289
|
+
// missionRefreshed 이벤트를 받기 위해 필수
|
|
290
|
+
val localUnitId = unitId
|
|
291
|
+
mission.setEventsListener(object : AdchainMissionEventsListener {
|
|
292
|
+
override fun onImpressed(mission: Mission) {
|
|
293
|
+
// 필요시 처리
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
override fun onClicked(mission: Mission) {
|
|
297
|
+
// 필요시 처리
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
override fun onCompleted(mission: Mission) {
|
|
301
|
+
// React Native로 이벤트 전송
|
|
302
|
+
reactContext
|
|
303
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
304
|
+
.emit("onMissionCompleted", Arguments.createMap().apply {
|
|
305
|
+
putString("unitId", localUnitId)
|
|
306
|
+
putString("missionId", mission.id)
|
|
307
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
override fun onProgressed(mission: Mission) {
|
|
312
|
+
// React Native로 이벤트 전송
|
|
313
|
+
reactContext
|
|
314
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
315
|
+
.emit("onMissionProgressed", Arguments.createMap().apply {
|
|
316
|
+
putString("unitId", localUnitId)
|
|
317
|
+
putString("missionId", mission.id)
|
|
318
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
override fun onRefreshed(refreshedUnitId: String?) {
|
|
323
|
+
// React Native로 이벤트 전송
|
|
324
|
+
reactContext
|
|
325
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
326
|
+
.emit("onMissionRefreshed", Arguments.createMap().apply {
|
|
327
|
+
putString("unitId", refreshedUnitId ?: localUnitId)
|
|
328
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
mission.getMissionList(
|
|
334
|
+
onSuccess = { missionList ->
|
|
335
|
+
// getMissionStatus를 호출하여 정확한 진행 상태 가져오기
|
|
336
|
+
mission.getMissionStatus(
|
|
337
|
+
onSuccess = { status ->
|
|
338
|
+
val array = Arguments.createArray()
|
|
339
|
+
|
|
340
|
+
// 미션 목록
|
|
341
|
+
missionList.forEach { m ->
|
|
342
|
+
val map = Arguments.createMap().apply {
|
|
343
|
+
putString("id", m.id)
|
|
344
|
+
putString("title", m.title)
|
|
345
|
+
putString("description", m.description)
|
|
346
|
+
putString("imageUrl", m.imageUrl)
|
|
347
|
+
putString("point", m.point) // 원본 문자열 그대로 전달
|
|
348
|
+
putBoolean("isCompleted", m.status == "completed")
|
|
349
|
+
putString("type", m.type?.value ?: "normal")
|
|
350
|
+
putString("actionUrl", m.landingUrl)
|
|
351
|
+
}
|
|
352
|
+
array.pushMap(map)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
val result = Arguments.createMap().apply {
|
|
356
|
+
putArray("missions", array)
|
|
357
|
+
putInt("completedCount", status.current) // getMissionStatus에서 가져온 값
|
|
358
|
+
putInt("totalCount", status.total) // getMissionStatus에서 가져온 값
|
|
359
|
+
putBoolean("canClaimReward", status.isCompleted && status.total > 0)
|
|
360
|
+
|
|
361
|
+
// 신규 필드 추가 (missionResponse에서 가져오기)
|
|
362
|
+
val missionInstance = missionInstances[unitId]
|
|
363
|
+
missionInstance?.let { m ->
|
|
364
|
+
val response = m.getMissionResponse()
|
|
365
|
+
putString("titleText", response?.titleText ?: "무료 포인트 모으기!")
|
|
366
|
+
putString("descriptionText", response?.descriptionText ?: "간단 광고 참여하고 100 포인트 받기")
|
|
367
|
+
putString("bottomText", response?.bottomText ?: "800만 포인트 받으러 가기")
|
|
368
|
+
putString("rewardIconUrl", response?.rewardIconUrl ?: "https://adchain-assets.1self.world/img_reward_coin.png")
|
|
369
|
+
putString("bottomIconUrl", response?.bottomIconUrl ?: "https://adchain-assets.1self.world/img_offerwall_coin.png")
|
|
370
|
+
} ?: run {
|
|
371
|
+
// 기본값 설정
|
|
372
|
+
putString("titleText", "무료 포인트 모으기!")
|
|
373
|
+
putString("descriptionText", "간단 광고 참여하고 100 포인트 받기")
|
|
374
|
+
putString("bottomText", "800만 포인트 받으러 가기")
|
|
375
|
+
putString("rewardIconUrl", "https://adchain-assets.1self.world/img_reward_coin.png")
|
|
376
|
+
putString("bottomIconUrl", "https://adchain-assets.1self.world/img_offerwall_coin.png")
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
promise.resolve(result)
|
|
381
|
+
},
|
|
382
|
+
onFailure = { error ->
|
|
383
|
+
// getMissionStatus 실패 시 기본값 사용
|
|
384
|
+
val array = Arguments.createArray()
|
|
385
|
+
missionList.forEach { m ->
|
|
386
|
+
val map = Arguments.createMap().apply {
|
|
387
|
+
putString("id", m.id)
|
|
388
|
+
putString("title", m.title)
|
|
389
|
+
putString("description", m.description)
|
|
390
|
+
putString("imageUrl", m.imageUrl)
|
|
391
|
+
putString("point", m.point)
|
|
392
|
+
putBoolean("isCompleted", m.status == "completed")
|
|
393
|
+
putString("type", m.type?.value ?: "normal")
|
|
394
|
+
putString("actionUrl", m.landingUrl)
|
|
395
|
+
}
|
|
396
|
+
array.pushMap(map)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
val completedCount = missionList.count { it.status == "completed" }
|
|
400
|
+
val totalCount = missionList.size
|
|
401
|
+
|
|
402
|
+
val result = Arguments.createMap().apply {
|
|
403
|
+
putArray("missions", array)
|
|
404
|
+
putInt("completedCount", completedCount)
|
|
405
|
+
putInt("totalCount", totalCount)
|
|
406
|
+
putBoolean("canClaimReward", completedCount == totalCount && totalCount > 0)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
promise.resolve(result)
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
},
|
|
413
|
+
onFailure = { error ->
|
|
414
|
+
promise.reject("MISSION_LOAD_ERROR", error.toString())
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
} catch (t: Throwable) {
|
|
418
|
+
promise.reject("MISSION_ERROR", t.message, t)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
@ReactMethod
|
|
423
|
+
fun clickMission(unitId: String, missionId: String, promise: Promise) {
|
|
424
|
+
try {
|
|
425
|
+
val mission = missionInstances.getOrPut(unitId) {
|
|
426
|
+
AdchainMission()
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 외부 변수를 로컬 변수로 캡처
|
|
430
|
+
val localUnitId = unitId
|
|
431
|
+
val localMissionId = missionId
|
|
432
|
+
|
|
433
|
+
// iOS와 동일한 방식: 리스너 설정
|
|
434
|
+
mission.setEventsListener(object : AdchainMissionEventsListener {
|
|
435
|
+
override fun onImpressed(mission: Mission) {
|
|
436
|
+
// 필요시 처리
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
override fun onClicked(mission: Mission) {
|
|
440
|
+
// 필요시 처리
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
override fun onCompleted(mission: Mission) {
|
|
444
|
+
// React Native로 이벤트 전송
|
|
445
|
+
reactContext
|
|
446
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
447
|
+
.emit("onMissionCompleted", Arguments.createMap().apply {
|
|
448
|
+
putString("unitId", localUnitId)
|
|
449
|
+
putString("missionId", localMissionId)
|
|
450
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
override fun onProgressed(mission: Mission) {
|
|
455
|
+
// React Native로 이벤트 전송 (missionCompleted와 동일한 구조)
|
|
456
|
+
reactContext
|
|
457
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
458
|
+
.emit("onMissionProgressed", Arguments.createMap().apply {
|
|
459
|
+
putString("unitId", localUnitId)
|
|
460
|
+
putString("missionId", localMissionId)
|
|
461
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
override fun onRefreshed(refreshedUnitId: String?) {
|
|
466
|
+
// React Native로 이벤트 전송
|
|
467
|
+
reactContext
|
|
468
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
469
|
+
.emit("onMissionRefreshed", Arguments.createMap().apply {
|
|
470
|
+
putString("unitId", refreshedUnitId ?: localUnitId)
|
|
471
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
mission.clickMission(missionId)
|
|
477
|
+
promise.resolve(createResponse(true, "Mission clicked"))
|
|
478
|
+
} catch (t: Throwable) {
|
|
479
|
+
promise.reject("MISSION_ERROR", t.message, t)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
@ReactMethod
|
|
484
|
+
fun claimReward(unitId: String, promise: Promise) {
|
|
485
|
+
try {
|
|
486
|
+
val mission = missionInstances.getOrPut(unitId) {
|
|
487
|
+
AdchainMission()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
mission.clickGetReward()
|
|
491
|
+
promise.resolve(createResponse(true, "Reward claimed"))
|
|
492
|
+
} catch (t: Throwable) {
|
|
493
|
+
promise.reject("MISSION_ERROR", t.message, t)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ===== 5. Debug/Utility Methods (3개) =====
|
|
498
|
+
|
|
499
|
+
@ReactMethod
|
|
500
|
+
fun isInitialized(promise: Promise) {
|
|
501
|
+
try {
|
|
502
|
+
promise.resolve(AdchainSdk.isInitialized())
|
|
503
|
+
} catch (t: Throwable) {
|
|
504
|
+
promise.reject("ERROR", t.message, t)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
@ReactMethod
|
|
509
|
+
fun getUserId(promise: Promise) {
|
|
510
|
+
try {
|
|
511
|
+
val userId = AdchainSdk.getCurrentUser()?.userId ?: ""
|
|
512
|
+
promise.resolve(userId)
|
|
513
|
+
} catch (t: Throwable) {
|
|
514
|
+
promise.reject("ERROR", t.message, t)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
@ReactMethod
|
|
519
|
+
fun getIFA(promise: Promise) {
|
|
520
|
+
try {
|
|
521
|
+
val context = reactContext.applicationContext
|
|
522
|
+
// SDK의 public 메서드를 사용하여 IFA 가져오기
|
|
523
|
+
// SDK가 캐싱, 권한 처리, 에러 처리 등을 모두 관리함
|
|
524
|
+
val ifa = AdchainSdk.getAdvertisingId(context) ?: ""
|
|
525
|
+
promise.resolve(ifa)
|
|
526
|
+
} catch (t: Throwable) {
|
|
527
|
+
promise.reject("ERROR", t.message, t)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ===== 6. Banner (1개) =====
|
|
532
|
+
|
|
533
|
+
@ReactMethod
|
|
534
|
+
fun getBannerInfo(placementId: String, promise: Promise) {
|
|
535
|
+
try {
|
|
536
|
+
AdchainSdk.getBannerInfo(placementId) { result ->
|
|
537
|
+
if (result.isSuccess) {
|
|
538
|
+
val response = result.getOrNull()
|
|
539
|
+
val map = Arguments.createMap().apply {
|
|
540
|
+
putBoolean("success", response?.success ?: false)
|
|
541
|
+
putString("imageUrl", response?.imageUrl)
|
|
542
|
+
response?.imageWidth?.let { putInt("imageWidth", it) }
|
|
543
|
+
response?.imageHeight?.let { putInt("imageHeight", it) }
|
|
544
|
+
putString("titleText", response?.titleText)
|
|
545
|
+
putString("linkType", response?.linkType)
|
|
546
|
+
putString("internalLinkUrl", response?.internalLinkUrl)
|
|
547
|
+
putString("externalLinkUrl", response?.externalLinkUrl)
|
|
548
|
+
}
|
|
549
|
+
promise.resolve(map)
|
|
550
|
+
} else {
|
|
551
|
+
promise.reject("BANNER_ERROR", result.exceptionOrNull()?.message)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} catch (t: Throwable) {
|
|
555
|
+
promise.reject("BANNER_ERROR", t.message, t)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ===== 7. Offerwall (3개) =====
|
|
560
|
+
|
|
561
|
+
@ReactMethod
|
|
562
|
+
fun openOfferwall(placementId: String?, promise: Promise) {
|
|
563
|
+
try {
|
|
564
|
+
reactContext.currentActivity?.let { activity ->
|
|
565
|
+
val callback = object : OfferwallCallback {
|
|
566
|
+
override fun onOpened() {
|
|
567
|
+
promise.resolve(createResponse(true, "Offerwall opened"))
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
override fun onClosed() {
|
|
571
|
+
// 이미 resolve 되었으므로 무시
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
override fun onError(message: String) {
|
|
575
|
+
// 이미 resolve 되었으므로 무시
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
override fun onRewardEarned(amount: Int) {
|
|
579
|
+
// 이미 resolve 되었으므로 무시
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Event callback for custom events
|
|
584
|
+
val eventCallback = object : OfferwallEventCallback {
|
|
585
|
+
override fun onCustomEvent(eventType: String, payload: Map<String, Any?>) {
|
|
586
|
+
android.util.Log.d("AdchainSdk", "[WebView → App] Custom Event: type=$eventType, payload=$payload")
|
|
587
|
+
// Sample 앱에서 처리하도록 이벤트만 전달 (Toast/Alert 표시 안 함)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
override fun onDataRequest(requestId: String, requestType: String, params: Map<String, Any?>): Map<String, Any?>? {
|
|
591
|
+
android.util.Log.d("AdchainSdk", "[WebView → App] Data Request: type=$requestType, params=$params")
|
|
592
|
+
|
|
593
|
+
// Return mock data based on request type
|
|
594
|
+
val response = when (requestType) {
|
|
595
|
+
"user_points" -> mapOf("points" to 12345, "currency" to "KRW")
|
|
596
|
+
"user_profile" -> mapOf("userId" to "test_123", "nickname" to "TestPlayer", "level" to 42)
|
|
597
|
+
"app_version" -> mapOf("version" to "1.0.0", "buildNumber" to 100)
|
|
598
|
+
else -> null
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
android.util.Log.d("AdchainSdk", "[App → WebView] Data Response: $response")
|
|
602
|
+
return response
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
val finalPlacementId = placementId ?: ""
|
|
607
|
+
AdchainSdk.openOfferwall(activity, finalPlacementId, callback, eventCallback)
|
|
608
|
+
} ?: promise.reject("OFFERWALL_ERROR", "Current activity is null")
|
|
609
|
+
} catch (t: Throwable) {
|
|
610
|
+
promise.reject("OFFERWALL_ERROR", t.message, t)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
@ReactMethod
|
|
615
|
+
fun openOfferwallWithUrl(url: String, placementId: String?, promise: Promise) {
|
|
616
|
+
try {
|
|
617
|
+
reactContext.currentActivity?.let { activity ->
|
|
618
|
+
val callback = object : OfferwallCallback {
|
|
619
|
+
override fun onOpened() {
|
|
620
|
+
promise.resolve(createResponse(true, "Offerwall opened with URL"))
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
override fun onClosed() {
|
|
624
|
+
// 이미 resolve 되었으므로 무시
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
override fun onError(message: String) {
|
|
628
|
+
// 이미 resolve 되었으므로 무시
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
override fun onRewardEarned(amount: Int) {
|
|
632
|
+
// 이미 resolve 되었으므로 무시
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Event callback for custom events
|
|
637
|
+
val eventCallback = object : OfferwallEventCallback {
|
|
638
|
+
override fun onCustomEvent(eventType: String, payload: Map<String, Any?>) {
|
|
639
|
+
android.util.Log.d("AdchainSdk", "[WebView → App] Custom Event: type=$eventType, payload=$payload")
|
|
640
|
+
// Sample 앱에서 처리하도록 이벤트만 전달 (Toast/Alert 표시 안 함)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
override fun onDataRequest(requestId: String, requestType: String, params: Map<String, Any?>): Map<String, Any?>? {
|
|
644
|
+
android.util.Log.d("AdchainSdk", "[WebView → App] Data Request: type=$requestType, params=$params")
|
|
645
|
+
|
|
646
|
+
// Return mock data based on request type
|
|
647
|
+
val response = when (requestType) {
|
|
648
|
+
"user_points" -> mapOf("points" to 12345, "currency" to "KRW")
|
|
649
|
+
"user_profile" -> mapOf("userId" to "test_123", "nickname" to "TestPlayer", "level" to 42)
|
|
650
|
+
"app_version" -> mapOf("version" to "1.0.0", "buildNumber" to 100)
|
|
651
|
+
else -> null
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
android.util.Log.d("AdchainSdk", "[App → WebView] Data Response: $response")
|
|
655
|
+
return response
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
val finalPlacementId = placementId ?: ""
|
|
660
|
+
AdchainSdk.openOfferwallWithUrl(url, activity, finalPlacementId, callback, eventCallback)
|
|
661
|
+
} ?: promise.reject("OFFERWALL_ERROR", "Current activity is null")
|
|
662
|
+
} catch (t: Throwable) {
|
|
663
|
+
promise.reject("OFFERWALL_ERROR", t.message, t)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
@ReactMethod
|
|
668
|
+
fun openExternalBrowser(url: String, placementId: String?, promise: Promise) {
|
|
669
|
+
try {
|
|
670
|
+
reactContext.currentActivity?.let { activity ->
|
|
671
|
+
val finalPlacementId = placementId ?: ""
|
|
672
|
+
val success = AdchainSdk.openExternalBrowser(url, activity, finalPlacementId)
|
|
673
|
+
|
|
674
|
+
if (success) {
|
|
675
|
+
promise.resolve(createResponse(true, "External browser opened"))
|
|
676
|
+
} else {
|
|
677
|
+
promise.reject("BROWSER_ERROR", "Failed to open external browser")
|
|
678
|
+
}
|
|
679
|
+
} ?: promise.reject("BROWSER_ERROR", "Current activity is null")
|
|
680
|
+
} catch (t: Throwable) {
|
|
681
|
+
promise.reject("BROWSER_ERROR", t.message, t)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
@ReactMethod
|
|
686
|
+
fun openAdjoeOfferwall(placementId: String?, promise: Promise) {
|
|
687
|
+
try {
|
|
688
|
+
reactContext.currentActivity?.let { activity ->
|
|
689
|
+
val callback = object : OfferwallCallback {
|
|
690
|
+
override fun onOpened() {
|
|
691
|
+
promise.resolve(createResponse(true, "Adjoe Offerwall opened"))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
override fun onClosed() {
|
|
695
|
+
// 이미 resolve 되었으므로 무시
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
override fun onError(message: String) {
|
|
699
|
+
// 이미 resolve 되었으므로 무시
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
override fun onRewardEarned(amount: Int) {
|
|
703
|
+
// 이미 resolve 되었으므로 무시
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
val finalPlacementId = placementId ?: ""
|
|
708
|
+
AdchainSdk.openAdjoeOfferwall(activity, finalPlacementId, callback)
|
|
709
|
+
} ?: promise.reject("ADJOE_ERROR", "Current activity is null")
|
|
710
|
+
} catch (t: Throwable) {
|
|
711
|
+
promise.reject("ADJOE_ERROR", t.message, t)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ===== Event Emitter Methods =====
|
|
716
|
+
|
|
717
|
+
@ReactMethod
|
|
718
|
+
fun addListener(eventName: String) {
|
|
719
|
+
// iOS와의 호환성을 위해 필요하지만 Android에서는 구현 불필요
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
@ReactMethod
|
|
723
|
+
fun removeListeners(count: Int) {
|
|
724
|
+
// iOS와의 호환성을 위해 필요하지만 Android에서는 구현 불필요
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ===== Helper Methods =====
|
|
728
|
+
|
|
729
|
+
private fun createResponse(success: Boolean, message: String): WritableMap {
|
|
730
|
+
return Arguments.createMap().apply {
|
|
731
|
+
putBoolean("success", success)
|
|
732
|
+
putString("message", message)
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
@Deprecated("Deprecated in Java")
|
|
737
|
+
override fun onCatalystInstanceDestroy() {
|
|
738
|
+
super.onCatalystInstanceDestroy()
|
|
739
|
+
quizInstances.clear()
|
|
740
|
+
missionInstances.clear()
|
|
741
|
+
}
|
|
742
|
+
}
|