@tenqube/visual-reward-react-native 1.1.4 → 1.1.6
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/CHANGELOG.md +12 -0
- package/README.md +201 -606
- package/android/build.gradle +4 -0
- package/android/src/main/AndroidManifest.xml +9 -1
- package/android/src/main/java/com/tenqube/visualreward/VisualRewardModule.kt +141 -0
- package/ios/VisualReward.m +24 -0
- package/ios/VisualReward.swift +158 -0
- package/package.json +2 -8
- package/src/ConfigCache.ts +1 -7
- package/src/NativeVisualReward.ts +87 -0
- package/src/RewardWebView.tsx +22 -66
- package/src/VisualReward.ts +4 -6
- package/src/VisualRewardContainer.tsx +42 -18
- package/src/types.ts +12 -0
package/android/build.gradle
CHANGED
|
@@ -42,6 +42,10 @@ repositories {
|
|
|
42
42
|
dependencies {
|
|
43
43
|
// React Native (autolink 이 버전 주입)
|
|
44
44
|
implementation "com.facebook.react:react-native:+"
|
|
45
|
+
// ADID(AAID) 조회용. react-native-idfa-aaid 제거에 따라 직접 선언해 ADID 를 계속 보장한다.
|
|
46
|
+
// (이 라이브러리가 com.google.android.gms.permission.AD_ID 권한을 매니페스트 머지로 가져오며,
|
|
47
|
+
// ADID 를 수집하는 앱이 해당 권한을 선언하는 것은 Play 정책상 올바른 동작이다.)
|
|
48
|
+
implementation "com.google.android.gms:play-services-ads-identifier:18.2.0"
|
|
45
49
|
// GMA / AdPopcorn / react-native-webview 는 리플렉션·뷰탐색으로만 접근하므로
|
|
46
50
|
// 컴파일 의존성 없음(호스트 앱이 각 SDK 를 링크하면 리플렉션이 동작, 없으면 no-op).
|
|
47
51
|
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
-
package="com.tenqube.visualreward"
|
|
2
|
+
package="com.tenqube.visualreward">
|
|
3
|
+
<!-- WebView 원격 URL 로드 / config HTTP fetch 용. -->
|
|
4
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
5
|
+
<!-- 네트워크 타입(wifi/mobile) 조회용 — @react-native-community/netinfo 제거에 따라 직접 선언. -->
|
|
6
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
7
|
+
<!-- 광고 ID(AAID) 조회용. play-services-ads-identifier 가 병합으로도 가져오지만,
|
|
8
|
+
Kotlin 네이티브 SDK 와 동일하게 명시 선언한다. -->
|
|
9
|
+
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
|
|
10
|
+
</manifest>
|
|
@@ -4,16 +4,27 @@ import android.app.Activity
|
|
|
4
4
|
import android.content.ActivityNotFoundException
|
|
5
5
|
import android.content.Context
|
|
6
6
|
import android.content.Intent
|
|
7
|
+
import android.content.pm.ActivityInfo
|
|
8
|
+
import android.content.pm.PackageManager
|
|
9
|
+
import android.net.ConnectivityManager
|
|
10
|
+
import android.net.NetworkCapabilities
|
|
7
11
|
import android.net.Uri
|
|
12
|
+
import android.os.Build
|
|
8
13
|
import android.util.Log
|
|
9
14
|
import android.view.View
|
|
10
15
|
import android.view.ViewGroup
|
|
11
16
|
import android.webkit.WebView
|
|
17
|
+
import androidx.core.view.ViewCompat
|
|
18
|
+
import androidx.core.view.WindowInsetsCompat
|
|
19
|
+
import com.facebook.react.bridge.Arguments
|
|
12
20
|
import com.facebook.react.bridge.Promise
|
|
13
21
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
14
22
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
15
23
|
import com.facebook.react.bridge.ReactMethod
|
|
16
24
|
import com.facebook.react.bridge.UiThreadUtil
|
|
25
|
+
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
|
26
|
+
import org.json.JSONException
|
|
27
|
+
import org.json.JSONObject
|
|
17
28
|
|
|
18
29
|
/**
|
|
19
30
|
* Tenqube Visual Reward — React Native 네이티브 모듈 (Android).
|
|
@@ -98,6 +109,136 @@ class VisualRewardModule(private val reactContext: ReactApplicationContext) :
|
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
111
|
|
|
112
|
+
// MARK: - 기기/앱 정보 · 광고 ID · ATT · 방향 · safe-area
|
|
113
|
+
// (react-native-device-info / idfa-aaid / netinfo / permissions / orientation-locker /
|
|
114
|
+
// safe-area-context 제거에 따라, Kotlin SDK 의 동일 로직을 네이티브 메서드로 직접 제공한다.)
|
|
115
|
+
|
|
116
|
+
@ReactMethod
|
|
117
|
+
fun getAppVersion(promise: Promise) {
|
|
118
|
+
promise.resolve(getAppVersionName())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@ReactMethod
|
|
122
|
+
fun getAdvertisingId(promise: Promise) {
|
|
123
|
+
// AdvertisingIdClient.getAdvertisingIdInfo 는 블로킹 호출 → 별도 스레드에서 실행.
|
|
124
|
+
Thread {
|
|
125
|
+
var adId = ""
|
|
126
|
+
try {
|
|
127
|
+
val id = AdvertisingIdClient.getAdvertisingIdInfo(reactApplicationContext).id ?: ""
|
|
128
|
+
if (id != "00000000-0000-0000-0000-000000000000") adId = id
|
|
129
|
+
} catch (_: Throwable) {
|
|
130
|
+
}
|
|
131
|
+
promise.resolve(adId)
|
|
132
|
+
}.start()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@ReactMethod
|
|
136
|
+
fun getIDFV(promise: Promise) {
|
|
137
|
+
promise.resolve("") // iOS 전용 — Android no-op
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@ReactMethod
|
|
141
|
+
fun getDeviceInfo(promise: Promise) {
|
|
142
|
+
promise.resolve(getDeviceInfoJson())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@ReactMethod
|
|
146
|
+
fun requestTrackingAuthorization(promise: Promise) {
|
|
147
|
+
promise.resolve(null) // iOS 전용 — Android no-op
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@ReactMethod
|
|
151
|
+
fun lockPortrait(promise: Promise) {
|
|
152
|
+
UiThreadUtil.runOnUiThread {
|
|
153
|
+
reactApplicationContext.currentActivity?.requestedOrientation =
|
|
154
|
+
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
155
|
+
promise.resolve(null)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@ReactMethod
|
|
160
|
+
fun unlockOrientations(promise: Promise) {
|
|
161
|
+
UiThreadUtil.runOnUiThread {
|
|
162
|
+
// 기존 방향 설정 복원 (orientation-locker.unlockAllOrientations 와 동일)
|
|
163
|
+
reactApplicationContext.currentActivity?.requestedOrientation =
|
|
164
|
+
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
165
|
+
promise.resolve(null)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@ReactMethod
|
|
170
|
+
fun getSafeAreaInsets(promise: Promise) {
|
|
171
|
+
UiThreadUtil.runOnUiThread {
|
|
172
|
+
val map = Arguments.createMap()
|
|
173
|
+
var top = 0.0
|
|
174
|
+
var bottom = 0.0
|
|
175
|
+
var left = 0.0
|
|
176
|
+
var right = 0.0
|
|
177
|
+
try {
|
|
178
|
+
val decor = reactApplicationContext.currentActivity?.window?.decorView
|
|
179
|
+
val density = reactApplicationContext.resources.displayMetrics.density.toDouble()
|
|
180
|
+
val insets = decor
|
|
181
|
+
?.let { ViewCompat.getRootWindowInsets(it) }
|
|
182
|
+
?.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
183
|
+
if (insets != null && density > 0) {
|
|
184
|
+
top = insets.top / density
|
|
185
|
+
bottom = insets.bottom / density
|
|
186
|
+
left = insets.left / density
|
|
187
|
+
right = insets.right / density
|
|
188
|
+
}
|
|
189
|
+
} catch (_: Exception) {
|
|
190
|
+
}
|
|
191
|
+
map.putDouble("top", top)
|
|
192
|
+
map.putDouble("bottom", bottom)
|
|
193
|
+
map.putDouble("left", left)
|
|
194
|
+
map.putDouble("right", right)
|
|
195
|
+
promise.resolve(map)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private fun getAppVersionName(): String {
|
|
200
|
+
return try {
|
|
201
|
+
val ctx = reactApplicationContext
|
|
202
|
+
val pm = ctx.packageManager
|
|
203
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
204
|
+
pm.getPackageInfo(ctx.packageName, PackageManager.PackageInfoFlags.of(0)).versionName ?: ""
|
|
205
|
+
} else {
|
|
206
|
+
@Suppress("DEPRECATION")
|
|
207
|
+
pm.getPackageInfo(ctx.packageName, 0).versionName ?: ""
|
|
208
|
+
}
|
|
209
|
+
} catch (_: Exception) {
|
|
210
|
+
""
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private fun getNetworkType(): String {
|
|
215
|
+
val cm = reactApplicationContext.getSystemService(Context.CONNECTIVITY_SERVICE)
|
|
216
|
+
as? ConnectivityManager ?: return ""
|
|
217
|
+
return try {
|
|
218
|
+
val caps = cm.getNetworkCapabilities(cm.activeNetwork ?: return "") ?: return ""
|
|
219
|
+
when {
|
|
220
|
+
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
|
|
221
|
+
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "mobile"
|
|
222
|
+
else -> ""
|
|
223
|
+
}
|
|
224
|
+
} catch (_: Exception) {
|
|
225
|
+
""
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private fun getDeviceInfoJson(): String {
|
|
230
|
+
val info = JSONObject()
|
|
231
|
+
try {
|
|
232
|
+
val net = getNetworkType()
|
|
233
|
+
if (net.isNotEmpty()) info.put("network", net)
|
|
234
|
+
info.put("model", Build.MODEL)
|
|
235
|
+
info.put("manufacturer", Build.MANUFACTURER)
|
|
236
|
+
info.put("osVersion", Build.VERSION.RELEASE)
|
|
237
|
+
} catch (_: JSONException) {
|
|
238
|
+
}
|
|
239
|
+
return info.toString()
|
|
240
|
+
}
|
|
241
|
+
|
|
101
242
|
// MARK: - GMA registerWebView (리플렉션)
|
|
102
243
|
// Kotlin SDK RewardWebViewActivity.onCreate 의 등록 로직과 동일.
|
|
103
244
|
|
package/ios/VisualReward.m
CHANGED
|
@@ -9,4 +9,28 @@ RCT_EXTERN_METHOD(attachAdBridges:(RCTPromiseResolveBlock)resolve
|
|
|
9
9
|
RCT_EXTERN_METHOD(hasTrackingDescription:(RCTPromiseResolveBlock)resolve
|
|
10
10
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
11
11
|
|
|
12
|
+
RCT_EXTERN_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve
|
|
13
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
14
|
+
|
|
15
|
+
RCT_EXTERN_METHOD(getAdvertisingId:(RCTPromiseResolveBlock)resolve
|
|
16
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
17
|
+
|
|
18
|
+
RCT_EXTERN_METHOD(getIDFV:(RCTPromiseResolveBlock)resolve
|
|
19
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
20
|
+
|
|
21
|
+
RCT_EXTERN_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
|
|
22
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
23
|
+
|
|
24
|
+
RCT_EXTERN_METHOD(requestTrackingAuthorization:(RCTPromiseResolveBlock)resolve
|
|
25
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
26
|
+
|
|
27
|
+
RCT_EXTERN_METHOD(lockPortrait:(RCTPromiseResolveBlock)resolve
|
|
28
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
29
|
+
|
|
30
|
+
RCT_EXTERN_METHOD(unlockOrientations:(RCTPromiseResolveBlock)resolve
|
|
31
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
32
|
+
|
|
33
|
+
RCT_EXTERN_METHOD(getSafeAreaInsets:(RCTPromiseResolveBlock)resolve
|
|
34
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
35
|
+
|
|
12
36
|
@end
|
package/ios/VisualReward.swift
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import AdSupport
|
|
2
|
+
import AppTrackingTransparency
|
|
1
3
|
import Foundation
|
|
4
|
+
import Network
|
|
2
5
|
import React
|
|
3
6
|
import UIKit
|
|
4
7
|
import WebKit
|
|
@@ -14,6 +17,11 @@ import WebKit
|
|
|
14
17
|
@objc(VisualReward)
|
|
15
18
|
class VisualReward: NSObject {
|
|
16
19
|
|
|
20
|
+
override init() {
|
|
21
|
+
super.init()
|
|
22
|
+
VisualReward.startNetworkMonitor()
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
@objc static func requiresMainQueueSetup() -> Bool {
|
|
18
26
|
return false
|
|
19
27
|
}
|
|
@@ -46,6 +54,156 @@ class VisualReward: NSObject {
|
|
|
46
54
|
resolve(has)
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
// MARK: - 기기/앱 정보 · IDFA/IDFV · ATT · 방향 · safe-area
|
|
58
|
+
// (react-native-device-info / idfa-aaid / netinfo / permissions / orientation-locker /
|
|
59
|
+
// safe-area-context 제거에 따라, Swift SDK 의 동일 로직을 네이티브 메서드로 직접 제공한다.)
|
|
60
|
+
|
|
61
|
+
@objc(getAppVersion:rejecter:)
|
|
62
|
+
func getAppVersion(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
63
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
64
|
+
resolve(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@objc(getAdvertisingId:rejecter:)
|
|
68
|
+
func getAdvertisingId(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
69
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
70
|
+
resolve(VisualReward.advertisingId())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@objc(getIDFV:rejecter:)
|
|
74
|
+
func getIDFV(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
75
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
76
|
+
resolve(UIDevice.current.identifierForVendor?.uuidString ?? "")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@objc(getDeviceInfo:rejecter:)
|
|
80
|
+
func getDeviceInfo(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
81
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
82
|
+
resolve(VisualReward.deviceInfoJson())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Swift SDK 의 ATTrackingManager.requestTrackingAuthorization 과 동일.
|
|
86
|
+
@objc(requestTrackingAuthorization:rejecter:)
|
|
87
|
+
func requestTrackingAuthorization(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
88
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
89
|
+
if #available(iOS 14, *) {
|
|
90
|
+
ATTrackingManager.requestTrackingAuthorization { _ in
|
|
91
|
+
DispatchQueue.main.async { resolve(nil) }
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
resolve(nil)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// 세로 모드 고정. iOS 16+ 공개 API requestGeometryUpdate 로 best-effort 회전 요청.
|
|
99
|
+
/// (비공개 API·스위즐링을 쓰지 않으므로, 최상단 VC 가 세로를 허용하지 않으면 조용히 무시된다.)
|
|
100
|
+
@objc(lockPortrait:rejecter:)
|
|
101
|
+
func lockPortrait(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
102
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
103
|
+
DispatchQueue.main.async {
|
|
104
|
+
if #available(iOS 16.0, *) {
|
|
105
|
+
VisualReward.windowScene()?.requestGeometryUpdate(
|
|
106
|
+
.iOS(interfaceOrientations: .portrait)
|
|
107
|
+
) { _ in }
|
|
108
|
+
}
|
|
109
|
+
resolve(nil)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@objc(unlockOrientations:rejecter:)
|
|
114
|
+
func unlockOrientations(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
115
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
116
|
+
DispatchQueue.main.async {
|
|
117
|
+
if #available(iOS 16.0, *) {
|
|
118
|
+
VisualReward.windowScene()?.requestGeometryUpdate(
|
|
119
|
+
.iOS(interfaceOrientations: .all)
|
|
120
|
+
) { _ in }
|
|
121
|
+
}
|
|
122
|
+
resolve(nil)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@objc(getSafeAreaInsets:rejecter:)
|
|
127
|
+
func getSafeAreaInsets(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
128
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
129
|
+
DispatchQueue.main.async {
|
|
130
|
+
let insets = VisualReward.keyWindow()?.safeAreaInsets ?? .zero
|
|
131
|
+
resolve([
|
|
132
|
+
"top": insets.top,
|
|
133
|
+
"bottom": insets.bottom,
|
|
134
|
+
"left": insets.left,
|
|
135
|
+
"right": insets.right,
|
|
136
|
+
])
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// MARK: - 기기/광고 정보 헬퍼 (Swift SDK RewardWebViewController 의 동일 로직 복제)
|
|
141
|
+
|
|
142
|
+
private static var networkMonitor: NWPathMonitor?
|
|
143
|
+
// networkType 은 NWPathMonitor 백그라운드 큐에서 쓰이고 브릿지 큐에서 읽히므로
|
|
144
|
+
// Swift SDK(RewardWebViewController)와 동일하게 serial 큐로 접근을 보호한다.
|
|
145
|
+
private static let networkQueue = DispatchQueue(label: "com.tenqube.visualreward.network", qos: .utility)
|
|
146
|
+
private static var _networkType = ""
|
|
147
|
+
private static var networkType: String {
|
|
148
|
+
get { networkQueue.sync { _networkType } }
|
|
149
|
+
set { networkQueue.async { _networkType = newValue } }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private static func startNetworkMonitor() {
|
|
153
|
+
if networkMonitor != nil { return }
|
|
154
|
+
let monitor = NWPathMonitor()
|
|
155
|
+
monitor.pathUpdateHandler = { path in
|
|
156
|
+
if path.usesInterfaceType(.wifi) {
|
|
157
|
+
VisualReward.networkType = "wifi"
|
|
158
|
+
} else if path.usesInterfaceType(.cellular) {
|
|
159
|
+
VisualReward.networkType = "mobile"
|
|
160
|
+
} else {
|
|
161
|
+
VisualReward.networkType = ""
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
monitor.start(queue: DispatchQueue.global(qos: .background))
|
|
165
|
+
networkMonitor = monitor
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private static func deviceInfoJson() -> String {
|
|
169
|
+
var info: [String: Any] = [:]
|
|
170
|
+
if !networkType.isEmpty { info["network"] = networkType }
|
|
171
|
+
info["model"] = machineIdentifier()
|
|
172
|
+
info["manufacturer"] = "Apple"
|
|
173
|
+
info["osVersion"] = UIDevice.current.systemVersion
|
|
174
|
+
guard let data = try? JSONSerialization.data(withJSONObject: info),
|
|
175
|
+
let json = String(data: data, encoding: .utf8) else { return "{}" }
|
|
176
|
+
return json
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private static func advertisingId() -> String {
|
|
180
|
+
// Swift SDK getIDFA 와 동일: iOS14+ 는 ATT 승인 시에만, 이하는 추적 허용 시에만 반환.
|
|
181
|
+
let idfa: String
|
|
182
|
+
if #available(iOS 14, *) {
|
|
183
|
+
idfa = (ATTrackingManager.trackingAuthorizationStatus == .authorized)
|
|
184
|
+
? ASIdentifierManager.shared().advertisingIdentifier.uuidString : ""
|
|
185
|
+
} else {
|
|
186
|
+
idfa = ASIdentifierManager.shared().isAdvertisingTrackingEnabled
|
|
187
|
+
? ASIdentifierManager.shared().advertisingIdentifier.uuidString : ""
|
|
188
|
+
}
|
|
189
|
+
return (idfa == "00000000-0000-0000-0000-000000000000") ? "" : idfa
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private static func machineIdentifier() -> String {
|
|
193
|
+
var size = 0
|
|
194
|
+
sysctlbyname("hw.machine", nil, &size, nil, 0)
|
|
195
|
+
var machine = [CChar](repeating: 0, count: size)
|
|
196
|
+
sysctlbyname("hw.machine", &machine, &size, nil, 0)
|
|
197
|
+
return String(cString: machine)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private static func windowScene() -> UIWindowScene? {
|
|
201
|
+
return UIApplication.shared.connectedScenes
|
|
202
|
+
.compactMap { $0 as? UIWindowScene }
|
|
203
|
+
.first { $0.activationState == .foregroundActive }
|
|
204
|
+
?? UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first
|
|
205
|
+
}
|
|
206
|
+
|
|
49
207
|
// MARK: - GMA registerWebView (리플렉션)
|
|
50
208
|
// Swift SDK RewardWebViewController.setupLayout 의 GADMobileAds.registerWebView 와 동일.
|
|
51
209
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenqube/visual-reward-react-native",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Tenqube Visual Reward SDK for React Native",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"types": "index.ts",
|
|
@@ -24,14 +24,8 @@
|
|
|
24
24
|
],
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"react": ">=18.0.0",
|
|
27
|
-
"react-native": ">=0.
|
|
27
|
+
"react-native": ">=0.76.0",
|
|
28
28
|
"react-native-webview": ">=13.0.0",
|
|
29
|
-
"react-native-safe-area-context": ">=4.0.0",
|
|
30
|
-
"react-native-device-info": ">=10.0.0",
|
|
31
|
-
"react-native-idfa-aaid": ">=0.1.1",
|
|
32
|
-
"react-native-orientation-locker": ">=1.7.0",
|
|
33
|
-
"react-native-permissions": ">=4.0.0",
|
|
34
|
-
"@react-native-community/netinfo": ">=11.0.0",
|
|
35
29
|
"react-native-google-mobile-ads": "*",
|
|
36
30
|
"react-native-adpopcorn-ssp": "*"
|
|
37
31
|
},
|
package/src/ConfigCache.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RemoteConfig } from './types';
|
|
1
|
+
import type { AsyncStorageLike, RemoteConfig } from './types';
|
|
2
2
|
|
|
3
3
|
const TTL_MS = 3600 * 1000;
|
|
4
4
|
const KEY_PREFIX = 'visual_reward_config_';
|
|
@@ -10,12 +10,6 @@ interface CacheEntry {
|
|
|
10
10
|
|
|
11
11
|
let _storage: AsyncStorageLike | null = null;
|
|
12
12
|
|
|
13
|
-
export interface AsyncStorageLike {
|
|
14
|
-
getItem(key: string): Promise<string | null>;
|
|
15
|
-
setItem(key: string, value: string): Promise<void>;
|
|
16
|
-
removeItem(key: string): Promise<void>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
13
|
export function setStorage(storage: AsyncStorageLike): void {
|
|
20
14
|
_storage = storage;
|
|
21
15
|
}
|
|
@@ -9,10 +9,25 @@ import { NativeModules, Platform } from "react-native";
|
|
|
9
9
|
*
|
|
10
10
|
* 네이티브 모듈이 링크되지 않은 환경(예: 자동링크 누락)에서도 앱이 죽지 않도록 모든 호출을 방어한다.
|
|
11
11
|
*/
|
|
12
|
+
export interface SafeAreaInsets {
|
|
13
|
+
top: number;
|
|
14
|
+
bottom: number;
|
|
15
|
+
left: number;
|
|
16
|
+
right: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
interface VisualRewardNativeModule {
|
|
13
20
|
attachAdBridges(): Promise<boolean>;
|
|
14
21
|
hasTrackingDescription(): Promise<boolean>;
|
|
15
22
|
handleIntentScheme(url: string): Promise<boolean>;
|
|
23
|
+
getAppVersion(): Promise<string>;
|
|
24
|
+
getAdvertisingId(): Promise<string>;
|
|
25
|
+
getIDFV(): Promise<string>;
|
|
26
|
+
getDeviceInfo(): Promise<string>;
|
|
27
|
+
requestTrackingAuthorization(): Promise<void>;
|
|
28
|
+
lockPortrait(): Promise<void>;
|
|
29
|
+
unlockOrientations(): Promise<void>;
|
|
30
|
+
getSafeAreaInsets(): Promise<SafeAreaInsets>;
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
const NativeModule: Partial<VisualRewardNativeModule> | undefined =
|
|
@@ -50,3 +65,75 @@ export async function handleIntentScheme(url: string): Promise<boolean> {
|
|
|
50
65
|
return false;
|
|
51
66
|
}
|
|
52
67
|
}
|
|
68
|
+
|
|
69
|
+
/** 앱 버전(CFBundleShortVersionString / versionName). */
|
|
70
|
+
export async function getAppVersion(): Promise<string> {
|
|
71
|
+
try {
|
|
72
|
+
return (await NativeModule?.getAppVersion?.()) ?? "";
|
|
73
|
+
} catch (_) {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 광고 식별자. Android: AAID(AdvertisingIdClient). iOS: IDFA(ASIdentifierManager, ATT 승인 시에만).
|
|
80
|
+
* opt-out/실패 시 빈 문자열. (제로 UUID 필터링은 네이티브에서 처리)
|
|
81
|
+
*/
|
|
82
|
+
export async function getAdvertisingId(): Promise<string> {
|
|
83
|
+
try {
|
|
84
|
+
return (await NativeModule?.getAdvertisingId?.()) ?? "";
|
|
85
|
+
} catch (_) {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** iOS: IDFV(identifierForVendor). (Android/미링크 → 빈 문자열) */
|
|
91
|
+
export async function getIDFV(): Promise<string> {
|
|
92
|
+
if (Platform.OS !== "ios") return "";
|
|
93
|
+
try {
|
|
94
|
+
return (await NativeModule?.getIDFV?.()) ?? "";
|
|
95
|
+
} catch (_) {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 기기 정보 JSON 문자열(network/model/manufacturer/osVersion). 네트워크 타입도 네이티브에서 합쳐 반환. */
|
|
101
|
+
export async function getDeviceInfo(): Promise<string> {
|
|
102
|
+
try {
|
|
103
|
+
return (await NativeModule?.getDeviceInfo?.()) ?? "{}";
|
|
104
|
+
} catch (_) {
|
|
105
|
+
return "{}";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** iOS: ATT 추적 권한 요청(ATTrackingManager). (Android/미링크 → no-op) */
|
|
110
|
+
export async function requestTrackingAuthorization(): Promise<void> {
|
|
111
|
+
if (Platform.OS !== "ios") return;
|
|
112
|
+
try {
|
|
113
|
+
await NativeModule?.requestTrackingAuthorization?.();
|
|
114
|
+
} catch (_) {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 세로 모드로 고정. Android: requestedOrientation. iOS: requestGeometryUpdate(iOS16+, best-effort). */
|
|
118
|
+
export async function lockPortrait(): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
await NativeModule?.lockPortrait?.();
|
|
121
|
+
} catch (_) {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 방향 고정 해제(기존 설정 복원). */
|
|
125
|
+
export async function unlockOrientations(): Promise<void> {
|
|
126
|
+
try {
|
|
127
|
+
await NativeModule?.unlockOrientations?.();
|
|
128
|
+
} catch (_) {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 현재 safe-area inset(top/bottom/left/right, dp/pt). 미링크 시 0. */
|
|
132
|
+
export async function getSafeAreaInsets(): Promise<SafeAreaInsets> {
|
|
133
|
+
const zero = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
134
|
+
try {
|
|
135
|
+
return (await NativeModule?.getSafeAreaInsets?.()) ?? zero;
|
|
136
|
+
} catch (_) {
|
|
137
|
+
return zero;
|
|
138
|
+
}
|
|
139
|
+
}
|