@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.
@@ -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
+ }