@adstage/react-native-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +327 -0
  3. package/adstage-react-native.podspec +24 -0
  4. package/android/build.gradle +66 -0
  5. package/android/src/main/AndroidManifest.xml +4 -0
  6. package/android/src/main/java/io/nbase/adstage/reactnative/AdStageModule.kt +701 -0
  7. package/android/src/main/java/io/nbase/adstage/reactnative/AdStagePackage.kt +24 -0
  8. package/ios/AdStageModule.m +70 -0
  9. package/ios/AdStageModule.swift +457 -0
  10. package/lib/commonjs/AdStage.js +213 -0
  11. package/lib/commonjs/AdStage.js.map +1 -0
  12. package/lib/commonjs/deeplink/AdStageDeepLink.js +235 -0
  13. package/lib/commonjs/deeplink/AdStageDeepLink.js.map +1 -0
  14. package/lib/commonjs/event/AdStageEvent.js +689 -0
  15. package/lib/commonjs/event/AdStageEvent.js.map +1 -0
  16. package/lib/commonjs/index.js +34 -0
  17. package/lib/commonjs/index.js.map +1 -0
  18. package/lib/commonjs/promotion/AdStagePromotion.js +158 -0
  19. package/lib/commonjs/promotion/AdStagePromotion.js.map +1 -0
  20. package/lib/commonjs/types.js +2 -0
  21. package/lib/commonjs/types.js.map +1 -0
  22. package/lib/module/AdStage.js +206 -0
  23. package/lib/module/AdStage.js.map +1 -0
  24. package/lib/module/deeplink/AdStageDeepLink.js +228 -0
  25. package/lib/module/deeplink/AdStageDeepLink.js.map +1 -0
  26. package/lib/module/event/AdStageEvent.js +682 -0
  27. package/lib/module/event/AdStageEvent.js.map +1 -0
  28. package/lib/module/index.js +15 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/promotion/AdStagePromotion.js +151 -0
  31. package/lib/module/promotion/AdStagePromotion.js.map +1 -0
  32. package/lib/module/types.js +2 -0
  33. package/lib/module/types.js.map +1 -0
  34. package/lib/typescript/src/AdStage.d.ts +124 -0
  35. package/lib/typescript/src/AdStage.d.ts.map +1 -0
  36. package/lib/typescript/src/deeplink/AdStageDeepLink.d.ts +154 -0
  37. package/lib/typescript/src/deeplink/AdStageDeepLink.d.ts.map +1 -0
  38. package/lib/typescript/src/event/AdStageEvent.d.ts +426 -0
  39. package/lib/typescript/src/event/AdStageEvent.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +13 -0
  41. package/lib/typescript/src/index.d.ts.map +1 -0
  42. package/lib/typescript/src/promotion/AdStagePromotion.d.ts +98 -0
  43. package/lib/typescript/src/promotion/AdStagePromotion.d.ts.map +1 -0
  44. package/lib/typescript/src/types.d.ts +305 -0
  45. package/lib/typescript/src/types.d.ts.map +1 -0
  46. package/package.json +105 -0
  47. package/src/AdStage.ts +212 -0
  48. package/src/deeplink/AdStageDeepLink.ts +246 -0
  49. package/src/event/AdStageEvent.ts +844 -0
  50. package/src/index.ts +48 -0
  51. package/src/promotion/AdStagePromotion.ts +162 -0
  52. package/src/types.ts +392 -0
@@ -0,0 +1,701 @@
1
+ package io.nbase.adstage.reactnative
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.*
5
+ import com.facebook.react.modules.core.DeviceEventManagerModule
6
+ import io.nbase.adapter.adstage.AdStage
7
+ import io.nbase.adapter.adstage.models.*
8
+ import io.nbase.adapter.adstage.deeplink.DeeplinkHandler
9
+ import io.nbase.adapter.adstage.promotion.PromotionHandler
10
+ import kotlinx.coroutines.CoroutineScope
11
+ import kotlinx.coroutines.Dispatchers
12
+ import kotlinx.coroutines.SupervisorJob
13
+ import kotlinx.coroutines.launch
14
+
15
+ /**
16
+ * AdStage React Native 네이티브 모듈
17
+ *
18
+ * React Native와 AdStage 네이티브 SDK를 연결하는 브릿지입니다.
19
+ *
20
+ * @since 3.0.0
21
+ */
22
+ class AdStageModule(reactContext: ReactApplicationContext) :
23
+ ReactContextBaseJavaModule(reactContext) {
24
+
25
+ companion object {
26
+ private const val TAG = "AdStageModule"
27
+ private const val MODULE_NAME = "AdStageModule"
28
+ }
29
+
30
+ private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
31
+
32
+ override fun getName(): String = MODULE_NAME
33
+
34
+ // ============================================
35
+ // Initialization
36
+ // ============================================
37
+
38
+ @ReactMethod
39
+ fun initialize(apiKey: String, serverUrl: String, promise: Promise) {
40
+ try {
41
+ Log.d(TAG, "Initializing AdStage SDK...")
42
+
43
+ val context = currentActivity ?: reactApplicationContext
44
+
45
+ // serverUrl이 빈 문자열이면 기본값 사용 (iOS와 동일한 처리)
46
+ if (serverUrl.isEmpty()) {
47
+ AdStage.initialize(
48
+ context = context,
49
+ apiKey = apiKey
50
+ )
51
+ } else {
52
+ AdStage.initialize(
53
+ context = context,
54
+ apiKey = apiKey,
55
+ serverUrl = serverUrl
56
+ )
57
+ }
58
+
59
+ Log.d(TAG, "AdStage SDK initialized successfully")
60
+ promise.resolve(true)
61
+ } catch (e: Exception) {
62
+ Log.e(TAG, "Failed to initialize AdStage SDK", e)
63
+ promise.reject("INIT_ERROR", e.message, e)
64
+ }
65
+ }
66
+
67
+ @ReactMethod
68
+ fun getVersion(promise: Promise) {
69
+ promise.resolve("3.0.0")
70
+ }
71
+
72
+ // ============================================
73
+ // User Attributes
74
+ // ============================================
75
+
76
+ @ReactMethod
77
+ fun setUserAttributes(attributes: ReadableMap, promise: Promise) {
78
+ try {
79
+ val userAttributes = UserAttributes(
80
+ gender = attributes.getString("gender"),
81
+ country = attributes.getString("country"),
82
+ city = attributes.getString("city"),
83
+ age = attributes.getString("age"),
84
+ language = attributes.getString("language")
85
+ )
86
+
87
+ AdStage.setUserAttributes(userAttributes)
88
+ promise.resolve(true)
89
+ } catch (e: Exception) {
90
+ promise.reject("SET_USER_ATTR_ERROR", e.message, e)
91
+ }
92
+ }
93
+
94
+ @ReactMethod
95
+ fun getUserAttributes(promise: Promise) {
96
+ try {
97
+ val attrs = AdStage.getUserAttributes()
98
+ if (attrs != null) {
99
+ val map = Arguments.createMap().apply {
100
+ attrs.gender?.let { putString("gender", it) }
101
+ attrs.country?.let { putString("country", it) }
102
+ attrs.city?.let { putString("city", it) }
103
+ attrs.age?.let { putString("age", it) }
104
+ attrs.language?.let { putString("language", it) }
105
+ }
106
+ promise.resolve(map)
107
+ } else {
108
+ promise.resolve(null)
109
+ }
110
+ } catch (e: Exception) {
111
+ promise.reject("GET_USER_ATTR_ERROR", e.message, e)
112
+ }
113
+ }
114
+
115
+ @ReactMethod
116
+ fun clearUserAttributes(promise: Promise) {
117
+ try {
118
+ AdStage.clearUserAttributes()
119
+ promise.resolve(true)
120
+ } catch (e: Exception) {
121
+ promise.reject("CLEAR_USER_ATTR_ERROR", e.message, e)
122
+ }
123
+ }
124
+
125
+ // ============================================
126
+ // DeepLink
127
+ // ============================================
128
+
129
+ @ReactMethod
130
+ fun setDeepLinkListener() {
131
+ Log.d(TAG, "Setting deep link listener")
132
+
133
+ AdStage.setDeeplinkListener(object : DeeplinkListener {
134
+ override fun onDeeplinkReceived(data: DeeplinkData) {
135
+ Log.d(TAG, "Deep link received: ${data.shortPath}")
136
+
137
+ val params = Arguments.createMap().apply {
138
+ putString("linkId", data.deeplinkId)
139
+ putString("shortPath", data.shortPath)
140
+ putString("source", data.source.value)
141
+ putString("eventType", data.eventType.value)
142
+
143
+ val parametersMap = Arguments.createMap()
144
+ data.parameters.forEach { (key, value) ->
145
+ parametersMap.putString(key, value)
146
+ }
147
+ putMap("parameters", parametersMap)
148
+ }
149
+
150
+ sendEvent("onDeepLinkReceived", params)
151
+ }
152
+
153
+ override fun onDeeplinkFailed(error: String, shortPath: String?) {
154
+ Log.e(TAG, "Deep link failed: $error (shortPath: $shortPath)")
155
+ }
156
+ })
157
+ }
158
+
159
+ @ReactMethod
160
+ fun removeDeepLinkListener() {
161
+ AdStage.clearDeeplinkListener()
162
+ }
163
+
164
+ @ReactMethod
165
+ fun createDeepLink(request: ReadableMap, promise: Promise) {
166
+ scope.launch {
167
+ try {
168
+ val name = request.getString("name") ?: run {
169
+ promise.reject("INVALID_PARAMS", "name is required")
170
+ return@launch
171
+ }
172
+
173
+ val response = AdStage.createDeeplink(name) {
174
+ request.getString("description")?.let { description(it) }
175
+ request.getString("channel")?.let { channel(it) }
176
+ request.getString("subChannel")?.let { subChannel(it) }
177
+ request.getString("campaign")?.let { campaign(it) }
178
+ request.getString("adGroup")?.let { adGroup(it) }
179
+ request.getString("creative")?.let { creative(it) }
180
+ request.getString("keyword")?.let { keyword(it) }
181
+
182
+ // Redirect Config
183
+ request.getMap("redirectConfig")?.let { config ->
184
+ redirectConfig {
185
+ config.getMap("android")?.let { androidConfig ->
186
+ android {
187
+ androidConfig.getString("storeUrl")?.let { storeUrl(it) }
188
+ androidConfig.getString("appScheme")?.let { appScheme(it) }
189
+ androidConfig.getString("webUrl")?.let { webUrl(it) }
190
+ }
191
+ }
192
+ config.getMap("ios")?.let { iosConfig ->
193
+ ios {
194
+ iosConfig.getString("storeUrl")?.let { storeUrl(it) }
195
+ iosConfig.getString("appScheme")?.let { appScheme(it) }
196
+ iosConfig.getString("webUrl")?.let { webUrl(it) }
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ // Parameters
203
+ request.getMap("parameters")?.let { params ->
204
+ val iterator = params.keySetIterator()
205
+ while (iterator.hasNextKey()) {
206
+ val key = iterator.nextKey()
207
+ params.getString(key)?.let { value ->
208
+ parameter(key, value)
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ val result = Arguments.createMap().apply {
215
+ putString("shortUrl", response.shortUrl)
216
+ putString("shortPath", response.shortPath)
217
+ putString("linkId", response.id)
218
+ }
219
+
220
+ promise.resolve(result)
221
+ } catch (e: Exception) {
222
+ Log.e(TAG, "Failed to create deep link", e)
223
+ promise.reject("CREATE_DEEPLINK_ERROR", e.message, e)
224
+ }
225
+ }
226
+ }
227
+
228
+ @ReactMethod
229
+ fun checkPendingDeepLink() {
230
+ // Pending 딥링크는 리스너 설정 시 자동으로 전달됨
231
+ Log.d(TAG, "checkPendingDeepLink called")
232
+ }
233
+
234
+ @ReactMethod
235
+ fun handleIntent() {
236
+ currentActivity?.let { activity ->
237
+ AdStage.handleIntent(activity, activity.intent)
238
+ }
239
+ }
240
+
241
+ // ============================================
242
+ // Promotion
243
+ // ============================================
244
+
245
+ @ReactMethod
246
+ fun getPromotionList(params: ReadableMap, promise: Promise) {
247
+ scope.launch {
248
+ try {
249
+ val listParams = PromotionListParams(
250
+ bannerType = params.getString("bannerType"),
251
+ targetAudience = params.getString("targetAudience"),
252
+ deviceType = params.getString("deviceType"),
253
+ region = params.getString("region"),
254
+ limit = if (params.hasKey("limit")) params.getInt("limit") else null,
255
+ status = params.getString("status"),
256
+ partner = params.getString("partner"),
257
+ primaryInterest = params.getString("primaryInterest"),
258
+ primaryAgeGroup = params.getString("primaryAgeGroup"),
259
+ gameGenrePreference = params.getString("gameGenrePreference"),
260
+ playerSpendingTier = params.getString("playerSpendingTier"),
261
+ playTimePattern = params.getString("playTimePattern")
262
+ )
263
+
264
+ val response = AdStage.getPromotionList(listParams)
265
+
266
+ val promotionsArray = Arguments.createArray()
267
+ response.promotions.forEach { promo ->
268
+ promotionsArray.pushMap(promotionToMap(promo))
269
+ }
270
+
271
+ val result = Arguments.createMap().apply {
272
+ putInt("totalItems", response.totalItems)
273
+ putArray("promotions", promotionsArray)
274
+ }
275
+
276
+ promise.resolve(result)
277
+ } catch (e: Exception) {
278
+ Log.e(TAG, "Failed to get promotion list", e)
279
+ promise.reject("GET_PROMOTION_ERROR", e.message, e)
280
+ }
281
+ }
282
+ }
283
+
284
+ @ReactMethod
285
+ fun handlePromotionClick(promotionMap: ReadableMap, promise: Promise) {
286
+ scope.launch {
287
+ try {
288
+ val context = currentActivity ?: reactApplicationContext
289
+
290
+ // ReadableMap에서 Promotion 객체 복원
291
+ val promotion = mapToPromotion(promotionMap)
292
+
293
+ val result = AdStage.handlePromotionClick(context, promotion)
294
+
295
+ when (result) {
296
+ is PromotionClickResult.Success -> {
297
+ val map = Arguments.createMap().apply {
298
+ putBoolean("success", true)
299
+ putString("storeUrl", result.storeUrl)
300
+ }
301
+ promise.resolve(map)
302
+ }
303
+ is PromotionClickResult.Failure -> {
304
+ val map = Arguments.createMap().apply {
305
+ putBoolean("success", false)
306
+ putString("error", result.error)
307
+ }
308
+ promise.resolve(map)
309
+ }
310
+ }
311
+ } catch (e: Exception) {
312
+ Log.e(TAG, "Failed to handle promotion click", e)
313
+ promise.reject("PROMOTION_CLICK_ERROR", e.message, e)
314
+ }
315
+ }
316
+ }
317
+
318
+ // ============================================
319
+ // Event Tracking
320
+ // ============================================
321
+
322
+ @ReactMethod
323
+ fun trackEvent(eventName: String, params: ReadableMap?, promise: Promise) {
324
+ scope.launch {
325
+ try {
326
+ val eventParams = params?.toHashMap() ?: emptyMap<String, Any>()
327
+
328
+ // Unity 래퍼와 동일하게 모든 이벤트 타입을 처리
329
+ val event = parseAdStageEvent(eventName, eventParams)
330
+
331
+ val response = AdStage.trackEvent(event)
332
+
333
+ val result = Arguments.createMap().apply {
334
+ putBoolean("success", true)
335
+ putString("eventId", response.id)
336
+ }
337
+
338
+ promise.resolve(result)
339
+ } catch (e: Exception) {
340
+ Log.e(TAG, "Failed to track event: $eventName", e)
341
+ promise.reject("TRACK_EVENT_ERROR", e.message, e)
342
+ }
343
+ }
344
+ }
345
+
346
+ /**
347
+ * React Native params를 AdStageEvent로 변환
348
+ * Unity 래퍼와 동일한 로직
349
+ */
350
+ private fun parseAdStageEvent(eventName: String, params: Map<String, Any?>): AdStageEvent {
351
+ val safeParams = params.filterValues { it != null }.mapValues { it.value!! }
352
+
353
+ return when (eventName) {
354
+ // 광고 추적 이벤트
355
+ "click" -> AdStageEvent.Click(params = safeParams)
356
+ "view" -> AdStageEvent.View(params = safeParams)
357
+ "install" -> AdStageEvent.Install(params = safeParams)
358
+ "custom" -> AdStageEvent.Custom(params = safeParams)
359
+
360
+ // 사용자 라이프사이클
361
+ "sign_up" -> AdStageEvent.SignUp(
362
+ method = params["method"]?.toString()
363
+ )
364
+ "sign_up_start" -> AdStageEvent.SignUpStart(params = safeParams)
365
+ "login" -> AdStageEvent.Login(
366
+ method = params["method"]?.toString()
367
+ )
368
+ "logout" -> AdStageEvent.Logout(params = safeParams)
369
+ "first_open" -> AdStageEvent.FirstOpen(params = safeParams)
370
+
371
+ // 콘텐츠 조회
372
+ "home_view" -> AdStageEvent.HomeView(params = safeParams)
373
+ "product_list_view" -> AdStageEvent.ProductListView(
374
+ itemCategory = params["item_category"]?.toString()
375
+ )
376
+ "search_result_view" -> AdStageEvent.SearchResultView(
377
+ searchTerm = params["search_term"]?.toString()
378
+ )
379
+ "product_details_view" -> AdStageEvent.ProductDetailsView(
380
+ itemId = params["item_id"]?.toString(),
381
+ itemName = params["item_name"]?.toString()
382
+ )
383
+ "page_view" -> AdStageEvent.PageView(
384
+ pageUrl = params["page_url"]?.toString(),
385
+ pageTitle = params["page_title"]?.toString(),
386
+ pagePath = params["page_path"]?.toString(),
387
+ referrer = params["referrer"]?.toString()
388
+ )
389
+ "screen_view" -> AdStageEvent.ScreenView(
390
+ screenName = params["screen_name"]?.toString(),
391
+ screenClass = params["screen_class"]?.toString()
392
+ )
393
+
394
+ // 전자상거래
395
+ "add_to_cart" -> AdStageEvent.AddToCart(
396
+ value = params["value"]?.toDoubleOrNull(),
397
+ currency = params["currency"]?.toString(),
398
+ items = parseEcommerceItems(params["items"])
399
+ )
400
+ "remove_from_cart" -> AdStageEvent.RemoveFromCart(
401
+ value = params["value"]?.toDoubleOrNull(),
402
+ currency = params["currency"]?.toString(),
403
+ items = parseEcommerceItems(params["items"])
404
+ )
405
+ "add_to_wishlist" -> AdStageEvent.AddToWishlist(
406
+ itemId = params["item_id"]?.toString(),
407
+ itemName = params["item_name"]?.toString()
408
+ )
409
+ "add_payment_info" -> AdStageEvent.AddPaymentInfo(
410
+ paymentType = params["payment_type"]?.toString()
411
+ )
412
+ "begin_checkout" -> AdStageEvent.BeginCheckout(
413
+ value = params["value"]?.toDoubleOrNull(),
414
+ currency = params["currency"]?.toString(),
415
+ items = parseEcommerceItems(params["items"])
416
+ )
417
+ "purchase" -> AdStageEvent.Purchase(
418
+ value = params["value"]?.toDoubleOrNull(),
419
+ currency = params["currency"]?.toString(),
420
+ transactionId = params["transaction_id"]?.toString(),
421
+ tax = params["tax"]?.toDoubleOrNull(),
422
+ shipping = params["shipping"]?.toDoubleOrNull(),
423
+ coupon = params["coupon"]?.toString(),
424
+ items = parseEcommerceItems(params["items"])
425
+ )
426
+ "refund" -> AdStageEvent.Refund(
427
+ transactionId = params["transaction_id"]?.toString(),
428
+ value = params["value"]?.toDoubleOrNull(),
429
+ currency = params["currency"]?.toString()
430
+ )
431
+
432
+ // 구독/체험
433
+ "start_trial" -> AdStageEvent.StartTrial(
434
+ value = params["value"]?.toDoubleOrNull(),
435
+ currency = params["currency"]?.toString(),
436
+ trialDays = params["trial_days"]?.toIntOrNull()
437
+ )
438
+ "subscribe" -> AdStageEvent.Subscribe(
439
+ value = params["value"]?.toDoubleOrNull(),
440
+ currency = params["currency"]?.toString(),
441
+ subscriptionId = params["subscription_id"]?.toString()
442
+ )
443
+ "unsubscribe" -> AdStageEvent.Unsubscribe(
444
+ subscriptionId = params["subscription_id"]?.toString()
445
+ )
446
+
447
+ // 인앱 광고
448
+ "ad_impression" -> AdStageEvent.AdImpression(
449
+ adPlatform = params["ad_platform"]?.toString(),
450
+ adSource = params["ad_source"]?.toString(),
451
+ adFormat = params["ad_format"]?.toString(),
452
+ adUnitName = params["ad_unit_name"]?.toString()
453
+ )
454
+ "ad_click" -> AdStageEvent.AdClick(
455
+ adPlatform = params["ad_platform"]?.toString(),
456
+ adSource = params["ad_source"]?.toString(),
457
+ adFormat = params["ad_format"]?.toString(),
458
+ adUnitName = params["ad_unit_name"]?.toString()
459
+ )
460
+
461
+ // 게임 특화
462
+ "tutorial_begin" -> AdStageEvent.TutorialBegin(params = safeParams)
463
+ "tutorial_complete" -> AdStageEvent.TutorialComplete(params = safeParams)
464
+ "level_up" -> AdStageEvent.LevelUp(
465
+ level = params["level"]?.toIntOrNull(),
466
+ character = params["character"]?.toString()
467
+ )
468
+ "achievement" -> AdStageEvent.Achievement(
469
+ achievementId = params["achievement_id"]?.toString()
470
+ )
471
+ "game_play" -> AdStageEvent.GamePlay(
472
+ level = params["level"]?.toIntOrNull(),
473
+ levelName = params["level_name"]?.toString(),
474
+ character = params["character"]?.toString(),
475
+ contentType = params["content_type"]?.toString()
476
+ )
477
+ "acquire_bonus" -> AdStageEvent.AcquireBonus(
478
+ contentType = params["content_type"]?.toString(),
479
+ itemId = params["item_id"]?.toString(),
480
+ itemName = params["item_name"]?.toString(),
481
+ quantity = params["quantity"]?.toIntOrNull()
482
+ )
483
+ "select_game_server" -> AdStageEvent.SelectGameServer(
484
+ contentId = params["content_id"]?.toString(),
485
+ contentType = params["content_type"]?.toString(),
486
+ itemName = params["item_name"]?.toString()
487
+ )
488
+ "complete_patch" -> AdStageEvent.CompletePatch(
489
+ contentId = params["content_id"]?.toString(),
490
+ contentType = params["content_type"]?.toString()
491
+ )
492
+
493
+ // 상호작용
494
+ "search" -> AdStageEvent.Search(
495
+ searchTerm = params["search_term"]?.toString()
496
+ )
497
+ "select_content" -> AdStageEvent.SelectContent(
498
+ contentType = params["content_type"]?.toString(),
499
+ contentId = params["content_id"]?.toString()
500
+ )
501
+ "share" -> AdStageEvent.Share(
502
+ contentType = params["content_type"]?.toString(),
503
+ method = params["method"]?.toString()
504
+ )
505
+ "like" -> AdStageEvent.Like(
506
+ contentType = params["content_type"]?.toString(),
507
+ contentId = params["content_id"]?.toString()
508
+ )
509
+ "rate" -> AdStageEvent.Rate(
510
+ contentType = params["content_type"]?.toString(),
511
+ contentId = params["content_id"]?.toString(),
512
+ score = params["score"]?.toDoubleOrNull()
513
+ )
514
+ "schedule" -> AdStageEvent.Schedule(params = safeParams)
515
+ "spend_credits" -> AdStageEvent.SpendCredits(
516
+ value = params["value"]?.toDoubleOrNull(),
517
+ itemName = params["item_name"]?.toString()
518
+ )
519
+
520
+ // 금융 특화
521
+ "sell_stock" -> AdStageEvent.SellStock(
522
+ itemId = params["item_id"]?.toString(),
523
+ itemName = params["item_name"]?.toString(),
524
+ quantity = params["quantity"]?.toIntOrNull(),
525
+ price = params["price"]?.toDoubleOrNull(),
526
+ value = params["value"]?.toDoubleOrNull(),
527
+ currency = params["currency"]?.toString()
528
+ )
529
+ "buy_stock" -> AdStageEvent.BuyStock(
530
+ itemId = params["item_id"]?.toString(),
531
+ itemName = params["item_name"]?.toString(),
532
+ quantity = params["quantity"]?.toIntOrNull(),
533
+ price = params["price"]?.toDoubleOrNull(),
534
+ value = params["value"]?.toDoubleOrNull(),
535
+ currency = params["currency"]?.toString()
536
+ )
537
+ "complete_open_account" -> AdStageEvent.CompleteOpenAccount(
538
+ contentType = params["content_type"]?.toString(),
539
+ contentId = params["content_id"]?.toString(),
540
+ method = params["method"]?.toString()
541
+ )
542
+ "apply_card" -> AdStageEvent.ApplyCard(
543
+ contentType = params["content_type"]?.toString(),
544
+ itemName = params["item_name"]?.toString(),
545
+ itemId = params["item_id"]?.toString()
546
+ )
547
+
548
+ // 알 수 없는 이벤트는 Custom으로 처리
549
+ else -> {
550
+ Log.w(TAG, "Unknown event name: $eventName, using Custom event")
551
+ AdStageEvent.Custom(params = safeParams)
552
+ }
553
+ }
554
+ }
555
+
556
+ /**
557
+ * EcommerceItem 리스트 파싱
558
+ */
559
+ @Suppress("UNCHECKED_CAST")
560
+ private fun parseEcommerceItems(itemsData: Any?): List<EcommerceItem>? {
561
+ if (itemsData == null) return null
562
+
563
+ return try {
564
+ when (itemsData) {
565
+ is List<*> -> itemsData.mapNotNull { item ->
566
+ when (item) {
567
+ is Map<*, *> -> {
568
+ val itemId = (item["item_id"] ?: item["itemId"])?.toString() ?: return@mapNotNull null
569
+ val itemName = (item["item_name"] ?: item["itemName"])?.toString() ?: return@mapNotNull null
570
+ val price = (item["price"])?.toDoubleOrNull() ?: return@mapNotNull null
571
+ val quantity = (item["quantity"])?.toIntOrNull() ?: return@mapNotNull null
572
+
573
+ EcommerceItem(
574
+ itemId = itemId,
575
+ itemName = itemName,
576
+ price = price,
577
+ quantity = quantity
578
+ )
579
+ }
580
+ else -> null
581
+ }
582
+ }.takeIf { it.isNotEmpty() }
583
+
584
+ is ArrayList<*> -> parseEcommerceItems(itemsData.toList())
585
+
586
+ else -> {
587
+ Log.w(TAG, "Unknown items type: ${itemsData::class.java}")
588
+ null
589
+ }
590
+ }
591
+ } catch (e: Exception) {
592
+ Log.e(TAG, "Failed to parse EcommerceItems", e)
593
+ null
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Any 타입을 Double로 변환
599
+ */
600
+ private fun Any?.toDoubleOrNull(): Double? {
601
+ return when (this) {
602
+ is Number -> this.toDouble()
603
+ is String -> this.toDoubleOrNull()
604
+ else -> null
605
+ }
606
+ }
607
+
608
+ /**
609
+ * Any 타입을 Int로 변환
610
+ */
611
+ private fun Any?.toIntOrNull(): Int? {
612
+ return when (this) {
613
+ is Number -> this.toInt()
614
+ is String -> this.toIntOrNull()
615
+ else -> null
616
+ }
617
+ }
618
+
619
+ // ============================================
620
+ // Helpers
621
+ // ============================================
622
+
623
+ private fun sendEvent(eventName: String, params: WritableMap) {
624
+ reactApplicationContext
625
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
626
+ .emit(eventName, params)
627
+ }
628
+
629
+ private fun promotionToMap(promo: Promotion): WritableMap {
630
+ return Arguments.createMap().apply {
631
+ putString("id", promo.id)
632
+ putString("partner", promo.partner)
633
+ putString("appName", promo.appName)
634
+ putString("bannerUrl", promo.bannerUrl)
635
+ putString("bannerType", promo.bannerType)
636
+ putString("appDescription", promo.appDescription)
637
+
638
+ // DeepLink
639
+ putMap("deeplink", Arguments.createMap().apply {
640
+ putString("id", promo.deeplink.id)
641
+ putString("name", promo.deeplink.name)
642
+ putString("description", promo.deeplink.description)
643
+ putString("shortPath", promo.deeplink.shortPath)
644
+ putString("linkType", promo.deeplink.linkType)
645
+ putString("channel", promo.deeplink.channel)
646
+ putString("subChannel", promo.deeplink.subChannel)
647
+ putString("campaign", promo.deeplink.campaign)
648
+ putString("status", promo.deeplink.status)
649
+ })
650
+
651
+ // Store URLs
652
+ promo.storeUrls?.let { urls ->
653
+ putMap("storeUrls", Arguments.createMap().apply {
654
+ putString("android", urls.android)
655
+ putString("ios", urls.ios)
656
+ })
657
+ }
658
+ }
659
+ }
660
+
661
+ private fun mapToPromotion(map: ReadableMap): Promotion {
662
+ val deeplinkMap = map.getMap("deeplink")
663
+
664
+ return Promotion(
665
+ id = map.getString("id") ?: "",
666
+ partner = map.getString("partner") ?: "",
667
+ appName = map.getString("appName") ?: "",
668
+ bannerUrl = map.getString("bannerUrl") ?: "",
669
+ bannerType = map.getString("bannerType") ?: "",
670
+ appDescription = map.getString("appDescription") ?: "",
671
+ deeplink = PromotionDeeplink(
672
+ id = deeplinkMap?.getString("id") ?: "",
673
+ name = deeplinkMap?.getString("name") ?: "",
674
+ description = deeplinkMap?.getString("description") ?: "",
675
+ shortPath = deeplinkMap?.getString("shortPath") ?: "",
676
+ linkType = deeplinkMap?.getString("linkType") ?: "",
677
+ channel = deeplinkMap?.getString("channel") ?: "",
678
+ subChannel = deeplinkMap?.getString("subChannel") ?: "",
679
+ campaign = deeplinkMap?.getString("campaign") ?: "",
680
+ status = deeplinkMap?.getString("status") ?: ""
681
+ ),
682
+ storeUrls = map.getMap("storeUrls")?.let { urls ->
683
+ StoreUrls(
684
+ android = urls.getString("android") ?: "",
685
+ ios = urls.getString("ios") ?: ""
686
+ )
687
+ }
688
+ )
689
+ }
690
+
691
+ // Required for RN event emitter
692
+ @ReactMethod
693
+ fun addListener(eventName: String) {
694
+ // Keep: Required for RN built-in Event Emitter
695
+ }
696
+
697
+ @ReactMethod
698
+ fun removeListeners(count: Int) {
699
+ // Keep: Required for RN built-in Event Emitter
700
+ }
701
+ }