react-native-daro 1.0.16 → 2.0.0-rc.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/README.md +2 -5
- package/android/build.gradle +6 -13
- package/android/src/main/java/com/daro/reactnative/DaroRNAdBannerViewManager.kt +105 -0
- package/android/src/main/java/com/daro/reactnative/{DaroMAdNativeViewManager.kt → DaroRNAdNativeViewManager.kt} +34 -38
- package/android/src/main/java/com/daro/reactnative/DaroRNModule.kt +376 -0
- package/android/src/main/java/com/daro/reactnative/{DaroMPackage.kt → DaroRNPackage.kt} +7 -7
- package/android/src/main/java/com/daro/reactnative/DaroSdkBridge.kt +52 -0
- package/android/src/main/java/com/daro/reactnative/constants/{InternalDaroMBannerSize.kt → InternalDaroRNBannerSize.kt} +2 -2
- package/android/src/main/java/com/daro/reactnative/event/{DaroMEvent.kt → DaroRNEvent.kt} +6 -6
- package/android/src/main/java/com/daro/reactnative/impl/{DaroMBannerViewModuleImpl.kt → DaroRNBannerViewModuleImpl.kt} +3 -3
- package/android/src/main/java/com/daro/reactnative/impl/{DaroMInterstitialModuleImpl.kt → DaroRNInterstitialModuleImpl.kt} +4 -4
- package/android/src/main/java/com/daro/reactnative/impl/{DaroMLightPopupModuleImpl.kt → DaroRNLightPopupModuleImpl.kt} +4 -4
- package/android/src/main/java/com/daro/reactnative/impl/{DaroMRewardedAdModuleImpl.kt → DaroRNRewardedAdModuleImpl.kt} +4 -4
- package/android/src/main/java/com/daro/reactnative/util/AdInfoUtil.kt +12 -0
- package/android/src/main/java/com/daro/reactnative/util/DaroQaProbe.kt +147 -0
- package/android/src/main/java/com/daro/reactnative/util/EventEmitterUtil.kt +2 -2
- package/android/src/main/java/com/daro/reactnative/view/{DaroMAdBannerViewContainer.kt → DaroRNAdBannerViewContainer.kt} +2 -3
- package/android/src/main/java/com/daro/reactnative/view/{DaroMAdNativeViewContainer.kt → DaroRNAdNativeViewContainer.kt} +3 -4
- package/android/src/newarch/java/com/daro/reactnative/{DaroMModuleSpec.kt → DaroRNModuleSpec.kt} +1 -1
- package/android/src/oldarch/java/com/daro/reactnative/{DaroMModuleSpec.kt → DaroRNModuleSpec.kt} +32 -3
- package/ios/{DaroMModule+AppOpen.swift → DaroRNModule+AppOpen.swift} +10 -11
- package/ios/DaroRNModule+Consent.swift +88 -0
- package/ios/{DaroMModule+Interstitial.swift → DaroRNModule+Interstitial.swift} +9 -9
- package/ios/{DaroMModule+LightPopup.swift → DaroRNModule+LightPopup.swift} +2 -2
- package/ios/DaroRNModule+Qa.swift +158 -0
- package/ios/{DaroMModule+Rewarded.swift → DaroRNModule+Rewarded.swift} +10 -10
- package/ios/{DaroMModule.mm → DaroRNModule.mm} +19 -4
- package/ios/{DaroMModule.swift → DaroRNModule.swift} +20 -6
- package/ios/Fabric/DaroRNAdBannerComponentView.mm +219 -0
- package/ios/Fabric/{DaroMFabricEventSink.mm → DaroRNFabricEventSink.mm} +4 -4
- package/ios/Fabric/{DaroMFabricPropApplier.mm → DaroRNFabricPropApplier.mm} +14 -14
- package/ios/Fabric/DaroRNNativeAdComponentView.mm +306 -0
- package/ios/Paper/{DaroMAdBannerViewManager.m → DaroRNAdBannerViewManager.m} +1 -1
- package/ios/Paper/{DaroMAdBannerViewManager.swift → DaroRNAdBannerViewManager.swift} +5 -5
- package/ios/Paper/{DaroMNativeAdViewManager.m → DaroRNNativeAdViewManager.m} +1 -1
- package/ios/Paper/DaroRNNativeAdViewManager.swift +22 -0
- package/ios/Shared/DaroMediationDebugger.swift +14 -0
- package/ios/Shared/{DaroMAdEventSink.swift → DaroRNAdEventSink.swift} +1 -1
- package/ios/Shared/{DaroMBannerAdView.swift → DaroRNBannerAdView.swift} +11 -11
- package/ios/Shared/{DaroMNativeAdLoadCoordinator.swift → DaroRNNativeAdLoadCoordinator.swift} +4 -4
- package/ios/Shared/{DaroMNativeAdView.swift → DaroRNNativeAdView.swift} +21 -21
- package/ios/Shared/{DaroMNativeAssetResolver.swift → DaroRNNativeAssetResolver.swift} +6 -6
- package/lib/commonjs/AdBannerView.js +3 -5
- package/lib/commonjs/AdBannerView.js.map +1 -1
- package/lib/commonjs/AppOpenAd.js +4 -4
- package/lib/commonjs/AppOpenAd.js.map +1 -1
- package/lib/commonjs/{DaroMAdBannerViewNativeComponent.ts → DaroRNAdBannerViewNativeComponent.ts} +5 -2
- package/{src/DaroMNativeAdViewNativeComponent.ts → lib/commonjs/DaroRNNativeAdViewNativeComponent.ts} +5 -2
- package/lib/commonjs/NativeDaroModule.js +1 -1
- package/lib/commonjs/NativeDaroModule.js.map +1 -1
- package/lib/commonjs/index.js +121 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/nativeAd/NativeAdView.js +3 -3
- package/lib/commonjs/nativeAd/NativeAdView.js.map +1 -1
- package/lib/module/AdBannerView.js +1 -3
- package/lib/module/AdBannerView.js.map +1 -1
- package/lib/module/AppOpenAd.js +4 -4
- package/lib/module/AppOpenAd.js.map +1 -1
- package/{src/DaroMAdBannerViewNativeComponent.ts → lib/module/DaroRNAdBannerViewNativeComponent.ts} +5 -2
- package/lib/{commonjs/DaroMNativeAdViewNativeComponent.ts → module/DaroRNNativeAdViewNativeComponent.ts} +5 -2
- package/lib/module/NativeDaroModule.js +1 -1
- package/lib/module/NativeDaroModule.js.map +1 -1
- package/lib/module/index.js +101 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/nativeAd/NativeAdView.js +1 -1
- package/lib/module/nativeAd/NativeAdView.js.map +1 -1
- package/lib/typescript/commonjs/plugin/src/index.d.ts +2 -6
- package/lib/typescript/commonjs/plugin/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/plugin/src/withAndroid.d.ts +2 -6
- package/lib/typescript/commonjs/plugin/src/withAndroid.d.ts.map +1 -1
- package/lib/typescript/commonjs/plugin/src/withIos.d.ts +2 -6
- package/lib/typescript/commonjs/plugin/src/withIos.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/AdBannerView.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/AppOpenAd.d.ts +2 -2
- package/lib/typescript/commonjs/src/AppOpenAd.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/{DaroMAdBannerViewNativeComponent.d.ts → DaroRNAdBannerViewNativeComponent.d.ts} +3 -2
- package/lib/typescript/commonjs/src/DaroRNAdBannerViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/{DaroMNativeAdViewNativeComponent.d.ts → DaroRNNativeAdViewNativeComponent.d.ts} +3 -2
- package/lib/typescript/commonjs/src/DaroRNNativeAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/NativeDaroModule.d.ts +10 -1
- package/lib/typescript/commonjs/src/NativeDaroModule.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +79 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/types/AdInfo.d.ts +15 -0
- package/lib/typescript/commonjs/src/types/AdInfo.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/types/AdProps.d.ts +0 -1
- package/lib/typescript/commonjs/src/types/AdProps.d.ts.map +1 -1
- package/lib/typescript/module/plugin/src/index.d.ts +2 -6
- package/lib/typescript/module/plugin/src/index.d.ts.map +1 -1
- package/lib/typescript/module/plugin/src/withAndroid.d.ts +2 -6
- package/lib/typescript/module/plugin/src/withAndroid.d.ts.map +1 -1
- package/lib/typescript/module/plugin/src/withIos.d.ts +2 -6
- package/lib/typescript/module/plugin/src/withIos.d.ts.map +1 -1
- package/lib/typescript/module/src/AdBannerView.d.ts.map +1 -1
- package/lib/typescript/module/src/AppOpenAd.d.ts +2 -2
- package/lib/typescript/module/src/AppOpenAd.d.ts.map +1 -1
- package/lib/typescript/module/src/{DaroMAdBannerViewNativeComponent.d.ts → DaroRNAdBannerViewNativeComponent.d.ts} +3 -2
- package/lib/typescript/module/src/DaroRNAdBannerViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/{DaroMNativeAdViewNativeComponent.d.ts → DaroRNNativeAdViewNativeComponent.d.ts} +3 -2
- package/lib/typescript/module/src/DaroRNNativeAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/NativeDaroModule.d.ts +10 -1
- package/lib/typescript/module/src/NativeDaroModule.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +79 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/types/AdInfo.d.ts +15 -0
- package/lib/typescript/module/src/types/AdInfo.d.ts.map +1 -1
- package/lib/typescript/module/src/types/AdProps.d.ts +0 -1
- package/lib/typescript/module/src/types/AdProps.d.ts.map +1 -1
- package/package.json +3 -3
- package/plugin/build/index.d.ts +2 -6
- package/plugin/build/index.js +21 -56
- package/plugin/build/withAndroid.d.ts +2 -6
- package/plugin/build/withAndroid.js +48 -90
- package/plugin/build/withIos.d.ts +2 -6
- package/plugin/build/withIos.js +37 -33
- package/react-native-daro.podspec +2 -3
- package/src/AdBannerView.tsx +1 -3
- package/src/AppOpenAd.ts +2 -8
- package/{lib/module/DaroMAdBannerViewNativeComponent.ts → src/DaroRNAdBannerViewNativeComponent.ts} +5 -2
- package/{lib/module/DaroMNativeAdViewNativeComponent.ts → src/DaroRNNativeAdViewNativeComponent.ts} +5 -2
- package/src/NativeDaroModule.ts +17 -2
- package/src/index.tsx +136 -0
- package/src/nativeAd/NativeAdView.tsx +1 -1
- package/src/types/AdInfo.ts +17 -0
- package/src/types/AdProps.ts +0 -2
- package/android/src/admob/java/com/daro/reactnative/DaroSdkBridge.kt +0 -23
- package/android/src/main/java/com/daro/reactnative/DaroMAdBannerViewManager.kt +0 -155
- package/android/src/main/java/com/daro/reactnative/DaroMModule.kt +0 -257
- package/ios/Fabric/DaroMAdBannerComponentView.mm +0 -207
- package/ios/Fabric/DaroMNativeAdComponentView.mm +0 -294
- package/ios/Paper/DaroMNativeAdViewManager.swift +0 -22
- package/ios/mediation/admob/DaroMediationDebugger.swift +0 -11
- package/lib/commonjs/types/DaroMMobileAds.js +0 -2
- package/lib/commonjs/types/DaroMMobileAds.js.map +0 -1
- package/lib/module/types/DaroMMobileAds.js +0 -2
- package/lib/module/types/DaroMMobileAds.js.map +0 -1
- package/lib/typescript/commonjs/src/DaroMAdBannerViewNativeComponent.d.ts.map +0 -1
- package/lib/typescript/commonjs/src/DaroMNativeAdViewNativeComponent.d.ts.map +0 -1
- package/lib/typescript/commonjs/src/types/DaroMMobileAds.d.ts +0 -6
- package/lib/typescript/commonjs/src/types/DaroMMobileAds.d.ts.map +0 -1
- package/lib/typescript/module/src/DaroMAdBannerViewNativeComponent.d.ts.map +0 -1
- package/lib/typescript/module/src/DaroMNativeAdViewNativeComponent.d.ts.map +0 -1
- package/lib/typescript/module/src/types/DaroMMobileAds.d.ts +0 -6
- package/lib/typescript/module/src/types/DaroMMobileAds.d.ts.map +0 -1
- package/src/types/DaroMMobileAds.ts +0 -5
- /package/ios/{mediation/admob → Shared}/DaroMediationTypes.swift +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
package com.daro.reactnative.util
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.applovin.sdk.AppLovinSdk
|
|
5
|
+
import com.facebook.react.bridge.Arguments
|
|
6
|
+
import com.facebook.react.bridge.WritableArray
|
|
7
|
+
import com.facebook.react.bridge.WritableMap
|
|
8
|
+
import com.google.android.gms.ads.MobileAds
|
|
9
|
+
import com.google.android.gms.ads.VersionInfo
|
|
10
|
+
import com.google.android.gms.ads.initialization.AdapterStatus.State
|
|
11
|
+
import com.google.android.gms.ads.mediation.Adapter
|
|
12
|
+
import droom.daro.core.model.DaroMediationType
|
|
13
|
+
import droom.daro.core.routing.MediationReadiness
|
|
14
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
15
|
+
|
|
16
|
+
// daro-android-qa 의 MediationStatusProbe 와 같은 축 — 초기화 판정은 라우팅이 쓰는
|
|
17
|
+
// SDK 값(@Keep MediationReadiness)을 그대로 읽고, 서브네트워크 어댑터 목록은 SDK 가
|
|
18
|
+
// 갖지 않으므로 벤더에 직접 묻는다. 여기서 다시 추론하면 화면과 실제 라우팅이 갈린다.
|
|
19
|
+
object DaroQaProbe {
|
|
20
|
+
|
|
21
|
+
// GMA 의 adapterStatusMap 은 초기화된 것만 담는다 — 탑재 여부는 클래스로,
|
|
22
|
+
// 버전은 어댑터에게, 초기화 상태는 맵에서 따로 읽는다.
|
|
23
|
+
private val ADMOB_ADAPTERS = listOf(
|
|
24
|
+
"AppLovin" to "com.google.ads.mediation.applovin.AppLovinMediationAdapter",
|
|
25
|
+
"DT Exchange" to "com.google.ads.mediation.fyber.FyberMediationAdapter",
|
|
26
|
+
"InMobi" to "com.google.ads.mediation.inmobi.InMobiMediationAdapter",
|
|
27
|
+
"LINE" to "com.google.ads.mediation.line.LineMediationAdapter",
|
|
28
|
+
"Meta" to "com.google.ads.mediation.facebook.FacebookMediationAdapter",
|
|
29
|
+
"Mintegral" to "com.google.ads.mediation.mintegral.MintegralMediationAdapter",
|
|
30
|
+
"Moloco" to "com.google.ads.mediation.moloco.MolocoMediationAdapter",
|
|
31
|
+
"Pangle" to "com.google.ads.mediation.pangle.PangleMediationAdapter",
|
|
32
|
+
"PubMatic" to "com.google.ads.mediation.pubmatic.PubMaticMediationAdapter",
|
|
33
|
+
"Unity Ads" to "com.google.ads.mediation.unity.UnityMediationAdapter",
|
|
34
|
+
"Vungle" to "com.google.ads.mediation.vungle.VungleMediationAdapter",
|
|
35
|
+
"ironSource" to "com.google.ads.mediation.ironsource.IronSourceMediationAdapter",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
fun mediationStatuses(): WritableArray {
|
|
39
|
+
val statuses = Arguments.createArray()
|
|
40
|
+
listOf(DaroMediationType.Daro, DaroMediationType.DaroM).forEach { mediation ->
|
|
41
|
+
val ready = MediationReadiness.isReady(mediation)
|
|
42
|
+
statuses.pushMap(
|
|
43
|
+
Arguments.createMap().apply {
|
|
44
|
+
// **공개 어휘(publicName)를 쓴다** — `daroa`/`darom` (DARO-1587).
|
|
45
|
+
// 이 값은 getMediationStatuses() 로 소비자에게 나가므로 DaroAdInfo.mediationPlatform
|
|
46
|
+
// 과 같은 어휘여야 한다. 갈리면 한 앱 안에서 두 이름이 돌아다닌다.
|
|
47
|
+
// iOS 는 DaroMediationState 가 provider.publicName 을 쓰는 것과 같은 자리다.
|
|
48
|
+
putString("mediationPlatform", mediation.publicName)
|
|
49
|
+
putBoolean("isReady", ready == true)
|
|
50
|
+
putBoolean("hasFailed", ready == false)
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
return statuses
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fun adapterInventory(context: Context): WritableMap =
|
|
58
|
+
Arguments.createMap().apply {
|
|
59
|
+
putArray("admob", adMobAdapters())
|
|
60
|
+
putArray("applovin", appLovinAdapters(context))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun adapterRow(name: String, detail: String, isReady: Boolean): WritableMap =
|
|
64
|
+
Arguments.createMap().apply {
|
|
65
|
+
putString("name", name)
|
|
66
|
+
putString("detail", detail)
|
|
67
|
+
putBoolean("isReady", isReady)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private fun adMobAdapters(): WritableArray {
|
|
71
|
+
val initStates = runCatching {
|
|
72
|
+
MobileAds.getInitializationStatus()?.adapterStatusMap.orEmpty()
|
|
73
|
+
}.getOrDefault(emptyMap())
|
|
74
|
+
|
|
75
|
+
val rows = Arguments.createArray()
|
|
76
|
+
ADMOB_ADAPTERS.forEach { (name, className) ->
|
|
77
|
+
val version = adapterVersion(className)
|
|
78
|
+
val state = initStates[className]?.initializationState
|
|
79
|
+
rows.pushMap(
|
|
80
|
+
adapterRow(
|
|
81
|
+
name = name,
|
|
82
|
+
detail = when {
|
|
83
|
+
version == null -> "미탑재"
|
|
84
|
+
state == State.READY -> "$version · READY"
|
|
85
|
+
state == State.NOT_READY -> "$version · NOT_READY"
|
|
86
|
+
else -> "$version · 미초기화"
|
|
87
|
+
},
|
|
88
|
+
isReady = version != null,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
return rows
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 탑재된 어댑터의 버전은 바이너리가 정하므로 안 바뀐다 — 호출마다 12종을
|
|
96
|
+
// Class.forName + newInstance 하면 직렬화된 NativeModules 스레드를 잡아먹는다.
|
|
97
|
+
//
|
|
98
|
+
// **성공과 '진짜 미탑재'만 캐시한다.** 실패를 전부 캐시하면 다른 이유로 한 번 실패한
|
|
99
|
+
// 프로브가(벤더 SDK 로드 전 생성자 예외·클래스로더 문제) 프로세스 수명 동안 가짜
|
|
100
|
+
// '미탑재'로 굳는다 — 이 프로브가 배제하려던 오신호다. 반대로 아무것도 캐시하지
|
|
101
|
+
// 않으면 없는 어댑터마다 매 호출 ClassNotFoundException 을 던져(직렬 브리지 스레드다)
|
|
102
|
+
// 펼칠 때마다 12번 반복한다. 클래스 부재는 바이너리가 정하므로 그것만 캐시한다.
|
|
103
|
+
private val adapterVersionCache = ConcurrentHashMap<String, String>()
|
|
104
|
+
private val absentAdapters = ConcurrentHashMap.newKeySet<String>()
|
|
105
|
+
|
|
106
|
+
private fun adapterVersion(className: String): String? {
|
|
107
|
+
adapterVersionCache[className]?.let { return it }
|
|
108
|
+
if (className in absentAdapters) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return try {
|
|
113
|
+
val adapter = Class.forName(className).getDeclaredConstructor().newInstance() as Adapter
|
|
114
|
+
adapter.versionInfo.format().also { adapterVersionCache[className] = it }
|
|
115
|
+
} catch (ignored: ClassNotFoundException) {
|
|
116
|
+
// 바이너리에 없다 — 이 프로세스에서 생길 리 없으니 기억한다.
|
|
117
|
+
absentAdapters.add(className)
|
|
118
|
+
null
|
|
119
|
+
} catch (ignored: Throwable) {
|
|
120
|
+
// 그 밖의 실패는 일시적일 수 있다 — 다음 조회에서 다시 시도한다.
|
|
121
|
+
null
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// GMA 는 4자리 어댑터 버전을 3필드에 담는다 — micro 가 "patch*100 + build" 다.
|
|
126
|
+
private fun VersionInfo.format() =
|
|
127
|
+
"$majorVersion.$minorVersion.${microVersion / 100}.${microVersion % 100}"
|
|
128
|
+
|
|
129
|
+
// MAX 는 어댑터별 초기화 상태를 내주지 않는다 — 실린 어댑터와 버전만 안다.
|
|
130
|
+
private fun appLovinAdapters(context: Context): WritableArray {
|
|
131
|
+
val rows = Arguments.createArray()
|
|
132
|
+
runCatching {
|
|
133
|
+
AppLovinSdk.getInstance(context).availableMediatedNetworks
|
|
134
|
+
.sortedBy { it.name }
|
|
135
|
+
.forEach {
|
|
136
|
+
rows.pushMap(
|
|
137
|
+
adapterRow(
|
|
138
|
+
name = it.name,
|
|
139
|
+
detail = it.adapterVersion.ifBlank { "미탑재" },
|
|
140
|
+
isReady = it.adapterVersion.isNotBlank(),
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return rows
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -2,7 +2,7 @@ package com.daro.reactnative.util
|
|
|
2
2
|
|
|
3
3
|
import android.view.View
|
|
4
4
|
import com.daro.reactnative.event.AdViewEvent
|
|
5
|
-
import com.daro.reactnative.event.
|
|
5
|
+
import com.daro.reactnative.event.DaroRNEvent
|
|
6
6
|
import com.facebook.react.bridge.ReactContext
|
|
7
7
|
import com.facebook.react.bridge.WritableMap
|
|
8
8
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
@@ -11,7 +11,7 @@ import com.facebook.react.uimanager.events.Event
|
|
|
11
11
|
|
|
12
12
|
internal object EventEmitterUtil {
|
|
13
13
|
// React Native Bridge
|
|
14
|
-
fun sendReactNativeEvent(reactContext: ReactContext, event:
|
|
14
|
+
fun sendReactNativeEvent(reactContext: ReactContext, event: DaroRNEvent, params: WritableMap?) {
|
|
15
15
|
reactContext
|
|
16
16
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
17
17
|
.emit(event.value, params)
|
|
@@ -22,7 +22,7 @@ import droom.daro.view.DaroAdViewListener
|
|
|
22
22
|
import droom.daro.view.DaroBannerAdView
|
|
23
23
|
|
|
24
24
|
@SuppressLint("ViewConstructor")
|
|
25
|
-
class
|
|
25
|
+
class DaroRNAdBannerViewContainer(val context: ReactContext) : ReactViewGroup(context),
|
|
26
26
|
DaroAdViewListener {
|
|
27
27
|
private companion object {
|
|
28
28
|
private const val MAX_PENDING_LOAD_RETRIES = 10
|
|
@@ -59,7 +59,7 @@ class DaroMAdBannerViewContainer(val context: ReactContext) : ReactViewGroup(con
|
|
|
59
59
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
60
60
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
61
61
|
)
|
|
62
|
-
this@
|
|
62
|
+
this@DaroRNAdBannerViewContainer.addView(this)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
layoutAdView()
|
|
@@ -92,7 +92,6 @@ class DaroMAdBannerViewContainer(val context: ReactContext) : ReactViewGroup(con
|
|
|
92
92
|
context,
|
|
93
93
|
DaroBannerAdUnit(
|
|
94
94
|
key = adUnitId,
|
|
95
|
-
placement = adUnitId,
|
|
96
95
|
bannerSize = adSize!!,
|
|
97
96
|
)
|
|
98
97
|
).also { view ->
|
|
@@ -31,7 +31,7 @@ import kotlin.math.roundToInt
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@SuppressLint("ViewConstructor")
|
|
34
|
-
class
|
|
34
|
+
class DaroRNAdNativeViewContainer(val context: ReactContext) : ReactViewGroup(context),
|
|
35
35
|
DaroAdViewListener {
|
|
36
36
|
|
|
37
37
|
private companion object {
|
|
@@ -116,7 +116,7 @@ class DaroMAdNativeViewContainer(val context: ReactContext) : ReactViewGroup(con
|
|
|
116
116
|
adView = view
|
|
117
117
|
|
|
118
118
|
if (view.parent == null) {
|
|
119
|
-
this@
|
|
119
|
+
this@DaroRNAdNativeViewContainer.addView(view)
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
view.loadAd()
|
|
@@ -134,7 +134,6 @@ class DaroMAdNativeViewContainer(val context: ReactContext) : ReactViewGroup(con
|
|
|
134
134
|
context,
|
|
135
135
|
DaroNativeAdUnit(
|
|
136
136
|
key = adUnitId,
|
|
137
|
-
placement = adUnitId,
|
|
138
137
|
)
|
|
139
138
|
).also { view ->
|
|
140
139
|
view.setAdBinder(
|
|
@@ -253,7 +252,7 @@ class DaroMAdNativeViewContainer(val context: ReactContext) : ReactViewGroup(con
|
|
|
253
252
|
}
|
|
254
253
|
}
|
|
255
254
|
|
|
256
|
-
private inner class LoadAdTask(private val nativeAdView:
|
|
255
|
+
private inner class LoadAdTask(private val nativeAdView: DaroRNAdNativeViewContainer) :
|
|
257
256
|
Runnable {
|
|
258
257
|
|
|
259
258
|
override fun run() {
|
package/android/src/newarch/java/com/daro/reactnative/{DaroMModuleSpec.kt → DaroRNModuleSpec.kt}
RENAMED
|
@@ -2,5 +2,5 @@ package com.daro.reactnative
|
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
4
|
|
|
5
|
-
abstract class
|
|
5
|
+
abstract class DaroRNModuleSpec(reactContext: ReactApplicationContext) :
|
|
6
6
|
NativeDaroModuleSpec(reactContext)
|
package/android/src/oldarch/java/com/daro/reactnative/{DaroMModuleSpec.kt → DaroRNModuleSpec.kt}
RENAMED
|
@@ -6,10 +6,10 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
|
6
6
|
import com.facebook.react.bridge.ReactMethod
|
|
7
7
|
import com.facebook.react.bridge.ReadableMap
|
|
8
8
|
|
|
9
|
-
abstract class
|
|
9
|
+
abstract class DaroRNModuleSpec(reactContext: ReactApplicationContext) :
|
|
10
10
|
ReactContextBaseJavaModule(reactContext) {
|
|
11
11
|
|
|
12
|
-
override fun getName(): String =
|
|
12
|
+
override fun getName(): String = DaroRNModule.NAME
|
|
13
13
|
|
|
14
14
|
protected abstract fun getTypedExportedConstants(): MutableMap<String, Any>
|
|
15
15
|
|
|
@@ -30,9 +30,39 @@ abstract class DaroMModuleSpec(reactContext: ReactApplicationContext) :
|
|
|
30
30
|
@ReactMethod
|
|
31
31
|
abstract fun setUserId(userId: String, promise: Promise)
|
|
32
32
|
|
|
33
|
+
@ReactMethod
|
|
34
|
+
abstract fun setGdpr(status: String, consentString: String, promise: Promise)
|
|
35
|
+
|
|
36
|
+
@ReactMethod
|
|
37
|
+
abstract fun setCcpa(status: String, consentString: String, promise: Promise)
|
|
38
|
+
|
|
39
|
+
@ReactMethod
|
|
40
|
+
abstract fun setCoppa(status: String, promise: Promise)
|
|
41
|
+
|
|
33
42
|
@ReactMethod
|
|
34
43
|
abstract fun showMediationDebugger(promise: Promise)
|
|
35
44
|
|
|
45
|
+
// QA 표면 — newarch 는 NativeDaroModule.ts 에서 코드젠으로 생기지만 oldarch 는 이
|
|
46
|
+
// 손 스펙이 정본이다. 스펙에 메서드를 더할 때 여기도 같이 더하지 않으면 oldarch
|
|
47
|
+
// 소비자에서 "overrides nothing" 으로 컴파일이 깨진다 (example 은 newarch 라 안 보인다).
|
|
48
|
+
@ReactMethod
|
|
49
|
+
abstract fun showMediationDebuggerFor(mediation: String, promise: Promise)
|
|
50
|
+
|
|
51
|
+
@ReactMethod
|
|
52
|
+
abstract fun getSdkVersion(promise: Promise)
|
|
53
|
+
|
|
54
|
+
@ReactMethod
|
|
55
|
+
abstract fun getMediationStatuses(promise: Promise)
|
|
56
|
+
|
|
57
|
+
@ReactMethod
|
|
58
|
+
abstract fun getMediationWeights(promise: Promise)
|
|
59
|
+
|
|
60
|
+
@ReactMethod
|
|
61
|
+
abstract fun getAdapterInventory(promise: Promise)
|
|
62
|
+
|
|
63
|
+
@ReactMethod
|
|
64
|
+
abstract fun getIntegrationKeyInfo(promise: Promise)
|
|
65
|
+
|
|
36
66
|
@ReactMethod
|
|
37
67
|
abstract fun isInterstitialReady(requestId: Double, adUnitId: String, promise: Promise)
|
|
38
68
|
|
|
@@ -66,7 +96,6 @@ abstract class DaroMModuleSpec(reactContext: ReactApplicationContext) :
|
|
|
66
96
|
abstract fun showAppOpenAd(
|
|
67
97
|
requestId: Double,
|
|
68
98
|
adUnitId: String,
|
|
69
|
-
placement: String?,
|
|
70
99
|
customData: String?,
|
|
71
100
|
promise: Promise,
|
|
72
101
|
)
|
|
@@ -2,7 +2,7 @@ import React
|
|
|
2
2
|
|
|
3
3
|
// MARK: - AppOpen Ad Methods
|
|
4
4
|
|
|
5
|
-
extension
|
|
5
|
+
extension DaroRNModule {
|
|
6
6
|
|
|
7
7
|
@objc(isAppOpenAdReady:adUnitId:resolve:reject:)
|
|
8
8
|
func isAppOpenAdReady(
|
|
@@ -13,7 +13,7 @@ extension DaroMModule {
|
|
|
13
13
|
) {
|
|
14
14
|
let requestId = Int(requestId)
|
|
15
15
|
if !Self.sdkInitialized {
|
|
16
|
-
|
|
16
|
+
DaroRNModule.logger.debug("[DaroRNMobileAds] !isInitialized")
|
|
17
17
|
resolve(false)
|
|
18
18
|
return
|
|
19
19
|
}
|
|
@@ -33,11 +33,10 @@ extension DaroMModule {
|
|
|
33
33
|
resolve(Void())
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
@objc(showAppOpenAd:adUnitId:
|
|
36
|
+
@objc(showAppOpenAd:adUnitId:customData:resolve:reject:)
|
|
37
37
|
func showAppOpenAd(
|
|
38
38
|
requestId: Double,
|
|
39
39
|
adUnitId: String,
|
|
40
|
-
placement: String?,
|
|
41
40
|
customData: String?,
|
|
42
41
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
43
42
|
reject: RCTPromiseRejectBlock
|
|
@@ -59,25 +58,25 @@ extension DaroMModule {
|
|
|
59
58
|
} else {
|
|
60
59
|
let appOpenLoader = DaroAppOpenAdLoader(unit: DaroAdUnit(unitId: adUnitId))
|
|
61
60
|
appOpenLoader.listener.onAdClicked = { [weak self] adInfo in
|
|
62
|
-
|
|
61
|
+
DaroRNModule.logger.debug("[DARO] Sample AppOpen Ad clicked")
|
|
63
62
|
self?.sendEvent(with: .onAppOpenAdClickedEvent, body: adInfo?.toBody(requestId: requestId))
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
appOpenLoader.listener.onAdImpression = { [weak self] adInfo in
|
|
67
|
-
|
|
66
|
+
DaroRNModule.logger.debug("[DARO] Sample AppOpen Ad impression")
|
|
68
67
|
self?.appOpenAds[requestId]?.loadedAd = nil
|
|
69
68
|
self?.sendEvent(with: .onAppOpenAdImpressionRecordedEvent, body: adInfo?.toBody(requestId: requestId))
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
appOpenLoader.listener.onAdLoadSuccess = { [weak self] ad, adInfo in
|
|
73
|
-
|
|
72
|
+
DaroRNModule.logger.debug("[DARO] Sample AppOpen Ad loaded")
|
|
74
73
|
self?.appOpenAds[requestId]?.loadedAd = ad
|
|
75
74
|
self?.sendEvent(with: .onAppOpenAdLoadedEvent, body: adInfo?.toBody(requestId: requestId))
|
|
76
75
|
self?.setupAppOpenListener(for: ad, requestId: requestId)
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
appOpenLoader.listener.onAdLoadFail = { [weak self] error in
|
|
80
|
-
|
|
79
|
+
DaroRNModule.logger.debug("[DARO] Sample AppOpen Ad failed: \(error)")
|
|
81
80
|
self?.sendEvent(with: .onAppOpenAdLoadFailedEvent, body: error.toBody(requestId: requestId))
|
|
82
81
|
}
|
|
83
82
|
self.appOpenAds[requestId] = AppOpenAd(loader: appOpenLoader)
|
|
@@ -87,17 +86,17 @@ extension DaroMModule {
|
|
|
87
86
|
|
|
88
87
|
private func setupAppOpenListener(for ad: DaroAppOpenAd, requestId: Int) {
|
|
89
88
|
ad.appOpenAdListener.onShown = { [weak self] adInfo in
|
|
90
|
-
|
|
89
|
+
DaroRNModule.logger.debug("[DARO] Sample AppOpen Ad displayed")
|
|
91
90
|
self?.sendEvent(with: .onAppOpenAdDisplayedEvent, body: adInfo?.toBody(requestId: requestId))
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
ad.appOpenAdListener.onDismiss = { [weak self] adInfo in
|
|
95
|
-
|
|
94
|
+
DaroRNModule.logger.debug("[DARO] Sample AppOpen Ad hidden")
|
|
96
95
|
self?.sendEvent(with: .onAppOpenAdHiddenEvent, body: adInfo?.toBody(requestId: requestId))
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
ad.appOpenAdListener.onFailedToShow = { [weak self] adInfo, error in
|
|
100
|
-
|
|
99
|
+
DaroRNModule.logger.debug("[DARO] Sample AppOpen Ad failed to display")
|
|
101
100
|
self?.sendEvent(with: .onAppOpenAdFailedToDisplayEvent, body: adInfo?.toBody(requestId: requestId))
|
|
102
101
|
}
|
|
103
102
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import Daro
|
|
2
|
+
import Dispatch
|
|
3
|
+
import React
|
|
4
|
+
|
|
5
|
+
/// 동의 표면(GDPR·CCPA·COPPA) — JS 의 플랫폼 중립 문자열을 iOS 표현으로 옮긴다.
|
|
6
|
+
///
|
|
7
|
+
/// **JS 가 raw Int 를 넘기지 않는다.** `DaroGdprStatus` 는 `@objc enum … : Int` 라
|
|
8
|
+
/// 브리지로 정수를 흘리는 게 제일 짧지만, 그러면 iOS 의 case 순서가 JS 계약이 되어
|
|
9
|
+
/// Android(enum 이름 기반)가 그 정수를 되옮겨야 한다. 되옮김이 빠지거나 낡으면 **에러
|
|
10
|
+
/// 없이 틀린 동의 상태**가 전달된다 — Flutter 가 그 반대 방향(Android enum 이름을 그대로
|
|
11
|
+
/// iOS 로)으로 겪은 사고가 DARO-1471 이다. 그래서 오가는 것은 어느 플랫폼 것도 아닌
|
|
12
|
+
/// 문자열이고, 옮기는 표가 여기 한 장 있다.
|
|
13
|
+
extension DaroRNModule {
|
|
14
|
+
|
|
15
|
+
private static let gdprStatuses: [String: DaroGdprStatus] = [
|
|
16
|
+
"optIn": .optIn,
|
|
17
|
+
"optOut": .optOut,
|
|
18
|
+
"notApplicable": .notApplicable,
|
|
19
|
+
"unknown": .unknown,
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
private static let ccpaStatuses: [String: DaroCcpaStatus] = [
|
|
23
|
+
"optIn": .optIn,
|
|
24
|
+
"optOut": .optOut,
|
|
25
|
+
"notApplicable": .notApplicable,
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
private static let coppaStatuses: [String: DaroCoppaStatus] = [
|
|
29
|
+
"child": .child,
|
|
30
|
+
"notChild": .notChild,
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
// 모르는 상태는 **기본값으로 떨어뜨리지 않고 reject 한다.** 동의는 조용히 틀리면 안 되는
|
|
34
|
+
// 값이다 — 오타가 "일단 옵트인"으로 흡수되면 호출자는 성공 응답을 받고 규제 대상
|
|
35
|
+
// 트래픽에 개인화 광고를 계속 태운다. 실패가 화면에 남는 쪽이 싸다.
|
|
36
|
+
@objc(setGdpr:consentString:resolve:reject:)
|
|
37
|
+
func setGdpr(
|
|
38
|
+
status: String,
|
|
39
|
+
consentString: String,
|
|
40
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
41
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
42
|
+
) {
|
|
43
|
+
guard let parsed = Self.gdprStatuses[status] else {
|
|
44
|
+
reject("INVALID_CONSENT_STATUS", "Unknown GdprStatus: \(status)", nil)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
// `DaroAds.setGdpr` 는 메인 스레드 호출을 요구한다(SDK 주석). 이 모듈의 메서드는
|
|
48
|
+
// RN 이 모듈 전용 큐에서 부르므로 여기서 옮겨야 한다 — 안 옮기면 백그라운드에서
|
|
49
|
+
// 네트워크 SDK 의 프라이버시 설정을 만지게 된다.
|
|
50
|
+
DispatchQueue.main.async {
|
|
51
|
+
DaroAds.shared.setGdpr(parsed, consentString: consentString)
|
|
52
|
+
resolve(nil)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@objc(setCcpa:consentString:resolve:reject:)
|
|
57
|
+
func setCcpa(
|
|
58
|
+
status: String,
|
|
59
|
+
consentString: String,
|
|
60
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
61
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
62
|
+
) {
|
|
63
|
+
guard let parsed = Self.ccpaStatuses[status] else {
|
|
64
|
+
reject("INVALID_CONSENT_STATUS", "Unknown CcpaStatus: \(status)", nil)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
DispatchQueue.main.async {
|
|
68
|
+
DaroAds.shared.setCcpa(parsed, consentString: consentString)
|
|
69
|
+
resolve(nil)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@objc(setCoppa:resolve:reject:)
|
|
74
|
+
func setCoppa(
|
|
75
|
+
status: String,
|
|
76
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
77
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
78
|
+
) {
|
|
79
|
+
guard let parsed = Self.coppaStatuses[status] else {
|
|
80
|
+
reject("INVALID_CONSENT_STATUS", "Unknown CoppaStatus: \(status)", nil)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
DispatchQueue.main.async {
|
|
84
|
+
DaroAds.shared.setCoppa(parsed)
|
|
85
|
+
resolve(nil)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -2,7 +2,7 @@ import React
|
|
|
2
2
|
|
|
3
3
|
// MARK: - Interstitial Ad Methods
|
|
4
4
|
|
|
5
|
-
extension
|
|
5
|
+
extension DaroRNModule {
|
|
6
6
|
|
|
7
7
|
@objc(isInterstitialReady:adUnitId:resolve:reject:)
|
|
8
8
|
func isInterstitialReady(
|
|
@@ -13,7 +13,7 @@ extension DaroMModule {
|
|
|
13
13
|
) {
|
|
14
14
|
let requestId = Int(requestId)
|
|
15
15
|
if !Self.sdkInitialized {
|
|
16
|
-
|
|
16
|
+
DaroRNModule.logger.debug("[DaroRNMobileAds] !isInitialized")
|
|
17
17
|
resolve(false)
|
|
18
18
|
return
|
|
19
19
|
}
|
|
@@ -61,25 +61,25 @@ extension DaroMModule {
|
|
|
61
61
|
} else {
|
|
62
62
|
let interstitialLoader = DaroInterstitialAdLoader(unit: DaroAdUnit(unitId: adUnitId))
|
|
63
63
|
interstitialLoader.listener.onAdClicked = { [weak self] adInfo in
|
|
64
|
-
|
|
64
|
+
DaroRNModule.logger.debug("[DARO] Sample Interstitial Ad clicked")
|
|
65
65
|
self?.sendEvent(with: .onInterstitialClickedEvent, body: adInfo?.toBody(requestId: requestId))
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
interstitialLoader.listener.onAdImpression = { [weak self] adInfo in
|
|
69
|
-
|
|
69
|
+
DaroRNModule.logger.debug("[DARO] Sample Interstitial Ad impression")
|
|
70
70
|
self?.interstitials[requestId]?.loadedAd = nil
|
|
71
71
|
self?.sendEvent(with: .onInterstitialAdImpressionRecordedEvent, body: adInfo?.toBody(requestId: requestId))
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
interstitialLoader.listener.onAdLoadSuccess = { [weak self] ad, adInfo in
|
|
75
|
-
|
|
75
|
+
DaroRNModule.logger.debug("[DARO] Sample Interstitial Ad loaded")
|
|
76
76
|
self?.interstitials[requestId]?.loadedAd = ad
|
|
77
77
|
self?.sendEvent(with: .onInterstitialLoadedEvent, body: adInfo?.toBody(requestId: requestId))
|
|
78
78
|
self?.setupInterstitialListener(for: ad, requestId: requestId)
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
interstitialLoader.listener.onAdLoadFail = { [weak self] error in
|
|
82
|
-
|
|
82
|
+
DaroRNModule.logger.debug("[DARO] Sample Interstitial Ad failed: \(error)")
|
|
83
83
|
self?.sendEvent(with: .onInterstitialLoadFailedEvent, body: error.toBody(requestId: requestId))
|
|
84
84
|
}
|
|
85
85
|
self.interstitials[requestId] = InterstitialAd(loader: interstitialLoader)
|
|
@@ -89,17 +89,17 @@ extension DaroMModule {
|
|
|
89
89
|
|
|
90
90
|
private func setupInterstitialListener(for ad: DaroInterstitialAd, requestId: Int) {
|
|
91
91
|
ad.interstitialListener.onShown = { [weak self] adInfo in
|
|
92
|
-
|
|
92
|
+
DaroRNModule.logger.debug("[DARO] Sample Interstitial Ad displayed")
|
|
93
93
|
self?.sendEvent(with: .onInterstitialDisplayedEvent, body: adInfo?.toBody(requestId: requestId))
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
ad.interstitialListener.onDismiss = { [weak self] adInfo in
|
|
97
|
-
|
|
97
|
+
DaroRNModule.logger.debug("[DARO] Sample Interstitial Ad hidden")
|
|
98
98
|
self?.sendEvent(with: .onInterstitialHiddenEvent, body: adInfo?.toBody(requestId: requestId))
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
ad.interstitialListener.onFailedToShow = { [weak self] adInfo, error in
|
|
102
|
-
|
|
102
|
+
DaroRNModule.logger.debug("[DARO] Sample Interstitial Ad failed to display")
|
|
103
103
|
self?.sendEvent(with: .onInterstitialAdFailedToDisplayEvent, body: adInfo?.toBody(requestId: requestId))
|
|
104
104
|
}
|
|
105
105
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React
|
|
2
2
|
import UIKit
|
|
3
3
|
// MARK: - LightPopup Ad
|
|
4
|
-
extension
|
|
4
|
+
extension DaroRNModule {
|
|
5
5
|
|
|
6
6
|
@objc(isLightPopupReady:adUnitId:resolve:reject:)
|
|
7
7
|
func isLightPopupReady(
|
|
@@ -12,7 +12,7 @@ extension DaroMModule {
|
|
|
12
12
|
) {
|
|
13
13
|
let requestId = Int(requestId)
|
|
14
14
|
if !Self.sdkInitialized {
|
|
15
|
-
|
|
15
|
+
DaroRNModule.logger.debug("[DaroRNMobileAds] !isInitialized")
|
|
16
16
|
resolve(false)
|
|
17
17
|
return
|
|
18
18
|
}
|