@tenqube/visual-reward-react-native 1.1.3
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 +68 -0
- package/README.md +690 -0
- package/android/build.gradle +47 -0
- package/android/consumer-rules.pro +9 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/tenqube/visualreward/VisualRewardModule.kt +223 -0
- package/android/src/main/java/com/tenqube/visualreward/VisualRewardPackage.kt +14 -0
- package/index.ts +8 -0
- package/ios/VisualReward.m +12 -0
- package/ios/VisualReward.swift +135 -0
- package/package.json +52 -0
- package/src/ConfigCache.ts +45 -0
- package/src/NativeVisualReward.ts +52 -0
- package/src/RewardWebView.tsx +552 -0
- package/src/VisualReward.ts +190 -0
- package/src/VisualRewardContainer.tsx +121 -0
- package/src/types.ts +66 -0
- package/src/utils.ts +24 -0
- package/visual-reward-react-native.podspec +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
# React Native
|
|
2
|
+
|
|
3
|
+
React Native WebView 기반 리워드 광고 SDK입니다.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 요구사항
|
|
8
|
+
|
|
9
|
+
- React Native 0.72+
|
|
10
|
+
- iOS 13.0+
|
|
11
|
+
- Android minSdk 21
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 설치
|
|
16
|
+
|
|
17
|
+
### 방법 1) npm (권장)
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install @tenqube/visual-reward-react-native
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
SDK가 의존하는 패키지(`react-native-webview`, `react-native-device-info`, `react-native-idfa-aaid`, `react-native-orientation-locker`, `react-native-permissions`, `@react-native-community/netinfo`)는 함께 설치됩니다. 아래 **peer dependency**만 호스트 앱에 직접 설치합니다.
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install react-native-safe-area-context
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 방법 2) 수동 설치 (GitHub Release)
|
|
30
|
+
|
|
31
|
+
npm을 쓰지 않는 경우, [GitHub Release](https://github.com/tenqube/visual-reward-sdk-react-native/releases)에서 `visual-reward-react-native-<버전>.zip`을 내려받아 압축을 풀고 프로젝트 내 원하는 위치에 복사합니다. (zip에는 `android/`, `ios/` 네이티브 모듈이 포함되어 있습니다.) 그런 다음 `package.json`에 `file:` 경로로 추가합니다.
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@tenqube/visual-reward-react-native": "file:./visual-reward-react-native",
|
|
36
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 공통) iOS pod install
|
|
45
|
+
|
|
46
|
+
두 방법 모두 iOS는 추가로 pod install을 실행합니다.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
cd ios && pod install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> 이 패키지는 GMA registerWebView / AdPopcorn 브릿지 등록을 위한 **네이티브 모듈**(iOS Swift / Android Kotlin)을 포함하며, React Native 오토링크로 자동 연결됩니다. (iOS는 `pod install`, Android는 Gradle sync 시 자동 반영)
|
|
53
|
+
|
|
54
|
+
### VisualRewardContainer 루트 추가
|
|
55
|
+
|
|
56
|
+
앱의 루트 컴포넌트(예: `App.tsx`)에 `<VisualRewardContainer />`을 추가합니다.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { VisualRewardContainer } from '@tenqube/visual-reward-react-native';
|
|
60
|
+
|
|
61
|
+
export default function App() {
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
<YourNavigator />
|
|
65
|
+
<VisualRewardContainer />
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## R8 / ProGuard
|
|
74
|
+
|
|
75
|
+
GMA / AdPopcorn 리플렉션 대상 보존 규칙(`AdPopcornSSPJsBridge`, `MobileAds`)은 SDK의 `consumer-rules.pro`에 포함되어 호스트 앱 R8에 **자동 전파**됩니다. 별도 추가가 필요 없습니다.
|
|
76
|
+
|
|
77
|
+
`minifyEnabled = true`를 쓰는 경우 호스트 앱의 WebView JavascriptInterface 보존을 위해 아래만 확인합니다.
|
|
78
|
+
|
|
79
|
+
```proguard
|
|
80
|
+
# react-native-webview
|
|
81
|
+
-keep class com.reactnativecommunity.webview.** { *; }
|
|
82
|
+
|
|
83
|
+
# WebView JavascriptInterface
|
|
84
|
+
-keepclassmembers class * {
|
|
85
|
+
@android.webkit.JavascriptInterface <methods>;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## AdMob / Ad Manager WebView 광고 연동 (선택)
|
|
92
|
+
|
|
93
|
+
WebView 내부 광고(AdSense / Google Publisher Tag / IMA HTML5)의 수익화·측정 신호를 개선하는 WebView API for Ads를 지원합니다. AdMob과 Google Ad Manager(GAM)는 동일한 Google Mobile Ads SDK를 사용하므로 양쪽 모두 동작합니다.
|
|
94
|
+
|
|
95
|
+
이 SDK는 호스트 앱에 **GMA SDK가 있을 때만** 네이티브 모듈에서 리플렉션으로 `registerWebView`를 자동 호출합니다. AdPopcorn SSP 오퍼월을 쓰는 경우에는 **AdPopcorn 네이티브 브릿지(`AdPopcornSSPJsBridge`)도 자동 주입**합니다(오퍼월의 웹 배너 SDK가 네이티브 컨텍스트를 필요로 하므로 **실제 네이티브 브릿지**를 연결 — 순수 JS shim 으로는 불가). SDK가 하는 일은 이 연결뿐이며, **SDK 의존성·App ID·초기화는 모두 호스트 앱에서 설정**해야 합니다.
|
|
96
|
+
|
|
97
|
+
> react-native-webview는 "로드 전 네이티브 인터페이스 주입" 훅이 없어, SDK는 실제 오퍼월 URL 로드 전에 `about:blank`를 먼저 띄워 브릿지를 주입한 뒤 URL을 로드합니다. 덕분에 오퍼월은 한 번만 로드되고 첫 페이지부터 브릿지가 바인딩됩니다(리로드 불필요).
|
|
98
|
+
|
|
99
|
+
### 1. 의존성 (호스트 앱)
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
# GMA WebView API for Ads
|
|
103
|
+
npm install react-native-google-mobile-ads
|
|
104
|
+
# AdPopcorn SSP (오퍼월 배너/전면 광고를 쓰면 필요 — 네이티브 SSP SDK 번들)
|
|
105
|
+
npm install react-native-adpopcorn-ssp
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
AdPopcorn 미디에이션으로 **AppLovin MAX** 를 쓰면 네트워크 SDK 를 추가합니다 (어댑터는 AdPopcorn SSP 코어에 번들, 네트워크 SDK 만 추가). flutter/kotlin/swift 와 동일 버전대.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# iOS — ios/Podfile 의 target 안
|
|
112
|
+
pod 'AppLovinSDK', '~> 13.6.3'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```gradle
|
|
116
|
+
// Android — android/app/build.gradle 의 dependencies
|
|
117
|
+
implementation("com.applovin:applovin-sdk:13.6.2")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> 이 SDK는 위 패키지에 **컴파일 의존하지 않습니다**(리플렉션·뷰탐색만 사용). 호스트가 링크하면 동작하고, 없으면 무시됩니다. `react-native-google-mobile-ads`는 GMA 네이티브 심볼을 앱에 링크하는 용도이며, 등록 자체는 이 SDK가 직접 수행합니다. AdPopcorn 은 `react-native-adpopcorn-ssp` 가 번들한 **네이티브 SSP SDK** 를 SDK 가 리플렉션으로 웹뷰에 연결합니다.
|
|
121
|
+
|
|
122
|
+
### 2. App ID / 초기화 — 아래 두 경우 중 해당하는 쪽으로 설정
|
|
123
|
+
|
|
124
|
+
- **WebView 신호 전용**: 이 앱으로 **자체 광고를 서빙하지 않고** WebView 안 광고 신호만 쓰는 경우
|
|
125
|
+
- **자체 광고 병행**: 호스트 앱이 GMA로 자기 앱에 **자체 인앱 광고(배너·전면 등)도 함께** 서빙하는 경우
|
|
126
|
+
|
|
127
|
+
| 경우 | iOS (`Info.plist`) | Android (`AndroidManifest.xml`) |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| **WebView 신호 전용** | 실제 App ID(`GADApplicationIdentifier`) + `GADIntegrationManager=webview` | 실제 App ID(`APPLICATION_ID`) + `INTEGRATION_MANAGER=webview` |
|
|
130
|
+
| **자체 광고 병행** | 실제 App ID만 | 실제 App ID만 (**`INTEGRATION_MANAGER` 생략**) |
|
|
131
|
+
|
|
132
|
+
> WebView 전용이어도 **실제 App ID가 필수**입니다(없으면 시작 크래시).
|
|
133
|
+
|
|
134
|
+
호스트 앱 시작 시 각 SDK를 1회 초기화합니다. (앱키·App ID는 호스트 설정 — 다른 플랫폼과 동일)
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import mobileAds from 'react-native-google-mobile-ads';
|
|
138
|
+
import { AdPopcornSSP } from 'react-native-adpopcorn-ssp';
|
|
139
|
+
import { Platform } from 'react-native';
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
mobileAds().initialize();
|
|
143
|
+
AdPopcornSSP.init(Platform.OS === 'ios' ? IOS_APP_KEY : ANDROID_APP_KEY);
|
|
144
|
+
}, []);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**iOS** — `ios/<App>/Info.plist` (아래는 *WebView 전용* 예시)
|
|
148
|
+
|
|
149
|
+
```xml
|
|
150
|
+
<key>GADApplicationIdentifier</key>
|
|
151
|
+
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>
|
|
152
|
+
<key>GADIntegrationManager</key>
|
|
153
|
+
<string>webview</string>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Android** — `android/app/src/main/AndroidManifest.xml` (아래는 *WebView 전용* 예시)
|
|
157
|
+
|
|
158
|
+
```xml
|
|
159
|
+
<!-- App ID 누락 시 시작 크래시 -->
|
|
160
|
+
<meta-data
|
|
161
|
+
android:name="com.google.android.gms.ads.APPLICATION_ID"
|
|
162
|
+
android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy" />
|
|
163
|
+
<!-- WebView 전용일 때만 추가. 자체 광고도 서빙하면 이 메타데이터는 삭제 -->
|
|
164
|
+
<meta-data
|
|
165
|
+
android:name="com.google.android.gms.ads.INTEGRATION_MANAGER"
|
|
166
|
+
android:value="webview" />
|
|
167
|
+
<!-- 애드팝콘 SSP 앱 키 -->
|
|
168
|
+
<meta-data android:name="adpopcorn_ssp_app_key" android:value="YOUR_APP_KEY" />
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 연동 검증
|
|
172
|
+
|
|
173
|
+
등록이 실행되면 네이티브 로그(프리픽스 `VisualReward`)가 남습니다. (Android: Logcat / iOS: Xcode 콘솔)
|
|
174
|
+
|
|
175
|
+
- 성공(GMA): `AdMob registerWebView registered` — Android는 괄호 안에 사용된 클래스명 표시
|
|
176
|
+
- 성공(AdPopcorn): `AdPopcornSSPJsBridge registered`
|
|
177
|
+
- 미링크(GMA): `Google Mobile Ads SDK not linked / not on classpath — AdMob WebView API not registered`
|
|
178
|
+
- 미링크(AdPopcorn): `AdPopcornSSPJsBridge class not on classpath — bridge not registered`
|
|
179
|
+
- WebView 미검출: `attachAdBridges skipped (WebView 미검출)`
|
|
180
|
+
|
|
181
|
+
Google [테스트 페이지](https://google.github.io/webview-ads/test/#api-for-ads-tests)로 초록 상태 바까지 확인하려면, 해당 도메인(`google.github.io`)을 원격 설정 `allowedDomains`에 추가한 상태에서 그 페이지를 SDK 웹뷰로 로드해야 합니다. (설정 캐시 TTL 1시간 주의)
|
|
182
|
+
|
|
183
|
+
> ⚠️ react-native-webview가 내부 WebView 인스턴스를 노출하지 않아, 네이티브 뷰 계층에서 WebView를 탐색해 등록하는 **best-effort** 방식입니다. 화면에 보이는 가장 큰 WebView를 리워드 웹뷰로 간주합니다.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 권한
|
|
188
|
+
|
|
189
|
+
### Android
|
|
190
|
+
|
|
191
|
+
`AndroidManifest.xml`에 광고 ID 접근 권한을 추가합니다.
|
|
192
|
+
|
|
193
|
+
```xml
|
|
194
|
+
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### iOS
|
|
198
|
+
|
|
199
|
+
IDFA 조회(`getIDFA`)를 사용하려면 `Info.plist`에 ATT 사용 목적 문자열을 추가해야 합니다.
|
|
200
|
+
|
|
201
|
+
```xml
|
|
202
|
+
<key>NSUserTrackingUsageDescription</key>
|
|
203
|
+
<string>맞춤형 광고 제공을 위해 기기 식별자를 사용합니다.</string>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
> ATT 권한 요청은 SDK가 웹뷰 로드 전 자동으로 처리합니다. 이미 권한이 결정된 경우 팝업 없이 즉시 진행됩니다. `NSUserTrackingUsageDescription`이 없으면 요청을 스킵하고 바로 로드합니다.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 사용법
|
|
211
|
+
|
|
212
|
+
### 스토리지 설정
|
|
213
|
+
|
|
214
|
+
config 캐싱을 위해 `initialize()` 전에 `AsyncStorage`를 주입합니다. 설정하지 않으면 매번 네트워크에서 config를 fetch합니다.
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
import { VisualReward } from '@tenqube/visual-reward-react-native';
|
|
218
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
219
|
+
|
|
220
|
+
VisualReward.setStorage(AsyncStorage);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
> `@react-native-async-storage/async-storage`가 설치되어 있지 않다면 `npm install @react-native-async-storage/async-storage`로 설치합니다.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
### 디버그 설정
|
|
228
|
+
|
|
229
|
+
`initialize()` 전에 호출합니다.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { VisualReward } from '@tenqube/visual-reward-react-native';
|
|
233
|
+
|
|
234
|
+
VisualReward.setDebugEnabled(__DEV__);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### 초기화
|
|
240
|
+
|
|
241
|
+
`App.tsx`의 `useEffect` 등 앱 시작 시 한 번만 호출합니다.
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import { VisualReward } from '@tenqube/visual-reward-react-native';
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
VisualReward.initialize({
|
|
248
|
+
appKey: 'tq_live_xxx',
|
|
249
|
+
timeoutMs: 5000, // 기본값 (ms)
|
|
250
|
+
onSuccess: () => { /* SDK 준비 완료 */ },
|
|
251
|
+
onFailure: (e) => { /* 초기화 실패 */ },
|
|
252
|
+
});
|
|
253
|
+
}, []);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### initialize 파라미터
|
|
257
|
+
|
|
258
|
+
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|
|
259
|
+
| --------------- | --------------------------- | ---- | -------------- | ------------------------------------------- |
|
|
260
|
+
| `appKey` | `string` | ✅ | | 앱 식별 키 |
|
|
261
|
+
| `timeoutMs` | `number` | | `5000` | 초기화 완료 대기 시간 (ms). 초과 시 `INITIALIZE_TIMEOUT` 에러 발생 |
|
|
262
|
+
| `onSuccess` | `() => void` | | `undefined` | 초기화 성공 콜백 (선택) |
|
|
263
|
+
| `onFailure` | `(error: VisualRewardError) => void` | | `undefined` | 초기화 실패 콜백 (선택) |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### 사용자 설정
|
|
268
|
+
|
|
269
|
+
로그인 후 또는 사용자가 변경될 때 호출합니다. `open()` 전에 반드시 호출해야 합니다.
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
VisualReward.setUserId(user.id);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### 웹뷰 열기
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
import { VisualReward } from '@tenqube/visual-reward-react-native';
|
|
281
|
+
|
|
282
|
+
await VisualReward.open({
|
|
283
|
+
serviceName: 'reward_home',
|
|
284
|
+
locale: 'ko',
|
|
285
|
+
extra: undefined,
|
|
286
|
+
onClose: () => { /* 웹뷰 닫힘 */ },
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### open 파라미터
|
|
291
|
+
|
|
292
|
+
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|
|
293
|
+
| ------------- | --------------------- | ---- | ----------- | ------------------------------------ |
|
|
294
|
+
| `serviceName` | `string` | ✅ | | 서비스 이름 |
|
|
295
|
+
| `locale` | `string \| undefined` | | `'ko'` | 언어 코드 |
|
|
296
|
+
| `extra` | `string \| undefined` | | `undefined` | 추가 query parameter (선택) |
|
|
297
|
+
| `onClose` | `() => void` | | `undefined` | 웹뷰가 닫힐 때 호출되는 콜백 (선택) |
|
|
298
|
+
| `onError` | `(error: VisualRewardError) => void` | | `undefined` | 열기 실패 콜백 (선택). `open` 은 예외를 던지지 않음 |
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 에러 처리
|
|
303
|
+
|
|
304
|
+
### 에러 타입
|
|
305
|
+
|
|
306
|
+
`VisualRewardError`의 `code` 프로퍼티로 에러 종류를 구분합니다.
|
|
307
|
+
|
|
308
|
+
| `code` | 설명 |
|
|
309
|
+
| -------------------- | ----------------------------------------- |
|
|
310
|
+
| `NETWORK_ERROR` | 네트워크 연결 실패 |
|
|
311
|
+
| `INITIALIZE_TIMEOUT` | `initialize()` 호출 후 완료까지 타임아웃 |
|
|
312
|
+
| `INVALID_APP_KEY` | 존재하지 않는 앱 키 (404) |
|
|
313
|
+
| `SERVER_ERROR` | 서버 오류 (5xx) |
|
|
314
|
+
| `PARSE_ERROR` | 응답 파싱 실패 |
|
|
315
|
+
| `NOT_INITIALIZED` | `initialize()` 호출 없이 `open()` 호출 |
|
|
316
|
+
| `USER_ID_NOT_SET` | `setUserId()` 호출 없이 `open()` 호출 |
|
|
317
|
+
|
|
318
|
+
### initialize 에러 처리
|
|
319
|
+
|
|
320
|
+
`onFailure` 콜백에서 `VisualRewardError`의 `code`를 분기합니다.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { VisualReward, VisualRewardError } from '@tenqube/visual-reward-react-native';
|
|
324
|
+
|
|
325
|
+
VisualReward.initialize({
|
|
326
|
+
appKey: 'tq_live_xxx',
|
|
327
|
+
timeoutMs: 5000,
|
|
328
|
+
onSuccess: () => { /* SDK 준비 완료 */ },
|
|
329
|
+
onFailure: (e) => {
|
|
330
|
+
if (e instanceof VisualRewardError) {
|
|
331
|
+
switch (e.code) {
|
|
332
|
+
case 'NETWORK_ERROR':
|
|
333
|
+
break; // 네트워크 연결 실패
|
|
334
|
+
case 'INITIALIZE_TIMEOUT':
|
|
335
|
+
break; // 초기화 타임아웃
|
|
336
|
+
case 'INVALID_APP_KEY':
|
|
337
|
+
break; // 존재하지 않는 앱 키
|
|
338
|
+
case 'SERVER_ERROR':
|
|
339
|
+
break; // 서버 오류
|
|
340
|
+
case 'PARSE_ERROR':
|
|
341
|
+
break; // 응답 파싱 실패
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### open 에러 처리
|
|
349
|
+
|
|
350
|
+
`open()`은 예외를 던지지 않습니다. 오류는 `onError` 콜백으로 전달됩니다 (flutter/kotlin/swift SDK 와 동일).
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
import { VisualReward, VisualRewardError } from '@tenqube/visual-reward-react-native';
|
|
354
|
+
|
|
355
|
+
await VisualReward.open({
|
|
356
|
+
serviceName: 'reward_home',
|
|
357
|
+
locale: 'ko',
|
|
358
|
+
extra: undefined,
|
|
359
|
+
onClose: () => { /* 웹뷰 닫힘 */ },
|
|
360
|
+
onError: (e: VisualRewardError) => {
|
|
361
|
+
switch (e.code) {
|
|
362
|
+
case 'NOT_INITIALIZED':
|
|
363
|
+
break; // initialize() 미호출
|
|
364
|
+
case 'USER_ID_NOT_SET':
|
|
365
|
+
break; // setUserId() 미호출
|
|
366
|
+
case 'INITIALIZE_TIMEOUT':
|
|
367
|
+
break; // 초기화 대기 타임아웃
|
|
368
|
+
case 'NETWORK_ERROR':
|
|
369
|
+
break; // 네트워크 연결 실패
|
|
370
|
+
default:
|
|
371
|
+
break; // 기타 오류
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## SDK 동작
|
|
380
|
+
|
|
381
|
+
아래 동작은 모두 `VisualReward.open`으로 열린 웹뷰 화면에만 적용됩니다. 호스트 앱의 다른 화면이나 웹뷰에는 영향을 주지 않습니다.
|
|
382
|
+
|
|
383
|
+
### 가로 모드 차단
|
|
384
|
+
|
|
385
|
+
웹뷰 화면은 항상 세로 모드로 고정됩니다(`react-native-orientation-locker`). 화면을 닫으면 기존 방향 설정이 자동으로 복원됩니다.
|
|
386
|
+
|
|
387
|
+
### 영상 광고 자동재생
|
|
388
|
+
|
|
389
|
+
사용자 제스처 없이 미디어(영상 광고)가 자동으로 인라인 재생됩니다.
|
|
390
|
+
|
|
391
|
+
- `javaScriptEnabled` — JavaScript 활성화
|
|
392
|
+
- `domStorageEnabled` — DOM Storage 활성화
|
|
393
|
+
- `mediaPlaybackRequiresUserAction={false}` — 제스처 없이 자동재생 허용
|
|
394
|
+
- `allowsInlineMediaPlayback` (iOS) — 전체화면 강제 전환 없이 인라인 재생
|
|
395
|
+
|
|
396
|
+
### ATT 권한 요청 (iOS)
|
|
397
|
+
|
|
398
|
+
웹뷰 로드 전 `react-native-permissions`를 통해 `NSUserTrackingUsageDescription`이 설정된 경우 ATT 권한을 자동으로 요청합니다. 이미 권한이 결정된 경우 팝업 없이 즉시 진행됩니다. `NSUserTrackingUsageDescription`이 없으면 요청을 스킵하고 바로 로드합니다.
|
|
399
|
+
|
|
400
|
+
### 스와이프 백 (iOS)
|
|
401
|
+
|
|
402
|
+
웹뷰 내 이전 페이지로 돌아가는 스와이프 백 제스처가 기본으로 활성화되어 있습니다(`allowsBackForwardNavigationGestures`). `swipeBackEnabled` JS 핸들러로 웹에서 활성화 여부를 제어할 수 있습니다.
|
|
403
|
+
|
|
404
|
+
### Mixed Content 허용 (Android)
|
|
405
|
+
|
|
406
|
+
HTTPS 페이지 내 HTTP 리소스 로드를 허용합니다(`mixedContentMode="always"`). Android 5.0 이상에서 기본 차단되는 mixed content를 광고 크리에이티브가 정상적으로 로드될 수 있도록 허용합니다.
|
|
407
|
+
|
|
408
|
+
### 서드파티 쿠키 허용
|
|
409
|
+
|
|
410
|
+
WebView 내부 광고의 쿠키 기반 타게팅·빈도제한·전환측정을 위해 서드파티 쿠키를 허용합니다(`thirdPartyCookiesEnabled` / `sharedCookiesEnabled`). 다른 플랫폼과 동일합니다.
|
|
411
|
+
|
|
412
|
+
### 서브프레임(iframe) 링크 처리 (iOS)
|
|
413
|
+
|
|
414
|
+
메인 프레임 네비게이션은 항상 도메인 정책으로 평가하고, 서브프레임(iframe)은 사용자가 직접 누른 링크(`navigationType === 'click'`)가 외부 대상일 때만 가로챕니다. 광고 iframe의 자체 네비게이션이 끊기지 않도록 하기 위함이며, Swift SDK와 동일합니다.
|
|
415
|
+
|
|
416
|
+
### 스크롤 바운스 비활성화
|
|
417
|
+
|
|
418
|
+
웹뷰 스크롤이 콘텐츠 끝에 도달해도 바운스 효과가 발생하지 않습니다(`bounces={false}`).
|
|
419
|
+
|
|
420
|
+
### 백그라운드 미디어 중단
|
|
421
|
+
|
|
422
|
+
앱이 백그라운드로 진입하면(`AppState` 감지) JS로 페이지 내 모든 `<video>`, `<audio>` 요소에 `.pause()`를 호출합니다.
|
|
423
|
+
|
|
424
|
+
### URL 처리
|
|
425
|
+
|
|
426
|
+
| 케이스 | 동작 |
|
|
427
|
+
| --------------------------------------------------- | ------------------------------------- |
|
|
428
|
+
| 허용 도메인 링크 | 내부 웹뷰에서 로드 |
|
|
429
|
+
| 비허용 도메인 링크 | 외부 브라우저로 오픈 |
|
|
430
|
+
| `window.open` / `target="_blank"` | 도메인 정책에 따라 내부/외부 분기 |
|
|
431
|
+
| `intent://` (Android) | 네이티브에서 `Intent.parseUri` 로 앱 실행, 미설치 시 `browser_fallback_url` → Play Store(`market://`) 폴백 |
|
|
432
|
+
| `market://` (Android) | 스토어 앱으로 오픈 |
|
|
433
|
+
| `mailto:`, `tel:`, `itms-apps:` 등 비 http(s) 스킴 | 외부로 전달 |
|
|
434
|
+
|
|
435
|
+
> 허용 도메인은 `initialize()` 시 서버에서 수신한 `allowedDomains` 설정이 적용됩니다.
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## JS Interface
|
|
440
|
+
|
|
441
|
+
웹에서 네이티브 기능을 호출하는 인터페이스입니다.
|
|
442
|
+
|
|
443
|
+
### 호출 형식
|
|
444
|
+
|
|
445
|
+
SDK는 웹뷰 로드 전 폴리필을 주입하여 Android 방식과 iOS 방식을 모두 지원합니다.
|
|
446
|
+
|
|
447
|
+
**Android 방식**
|
|
448
|
+
|
|
449
|
+
```js
|
|
450
|
+
// 파라미터 없음
|
|
451
|
+
window.nativeBridge.getAppVersion();
|
|
452
|
+
|
|
453
|
+
// 파라미터 있음 (JSON 문자열)
|
|
454
|
+
window.nativeBridge.pushUrl(JSON.stringify({ url: 'https://example.com', replace: false }));
|
|
455
|
+
|
|
456
|
+
// openExternalBrowser: url 직접 전달
|
|
457
|
+
window.nativeBridge.openExternalBrowser('https://example.com');
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**iOS 방식**
|
|
461
|
+
|
|
462
|
+
```js
|
|
463
|
+
// 파라미터 없음
|
|
464
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(
|
|
465
|
+
JSON.stringify({ fncName: 'getAppVersion' })
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// 파라미터 있음
|
|
469
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(
|
|
470
|
+
JSON.stringify({ fncName: 'pushUrl', params: { url: 'https://example.com', replace: false } })
|
|
471
|
+
);
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
콜백은 `window.nativeCallback` 객체로 수신합니다.
|
|
475
|
+
|
|
476
|
+
```js
|
|
477
|
+
window.nativeCallback.fncName = (result) => { ... };
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
### getAppVersion()
|
|
483
|
+
|
|
484
|
+
앱 버전을 가져옵니다.
|
|
485
|
+
|
|
486
|
+
```js
|
|
487
|
+
// Android
|
|
488
|
+
window.nativeBridge.getAppVersion();
|
|
489
|
+
// iOS
|
|
490
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify({ fncName: 'getAppVersion' }));
|
|
491
|
+
|
|
492
|
+
window.nativeCallback.getAppVersion = (version) => {
|
|
493
|
+
console.log(version); // "1.0.0"
|
|
494
|
+
};
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
### getADID() — Android Only
|
|
500
|
+
|
|
501
|
+
디바이스 광고 식별자(Advertising ID)를 가져옵니다. iOS는 빈 문자열을 반환합니다.
|
|
502
|
+
|
|
503
|
+
```js
|
|
504
|
+
// Android
|
|
505
|
+
window.nativeBridge.getADID();
|
|
506
|
+
// iOS
|
|
507
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify({ fncName: 'getADID' }));
|
|
508
|
+
|
|
509
|
+
window.nativeCallback.getADID = (adId) => {
|
|
510
|
+
console.log(adId); // "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
|
511
|
+
};
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### getIDFA() — iOS Only
|
|
517
|
+
|
|
518
|
+
디바이스 광고 식별자(IDFA)를 가져옵니다. ATT 권한 허용 시 IDFA 반환, 미허용 시 빈 문자열을 반환합니다. Android는 빈 문자열을 반환합니다.
|
|
519
|
+
|
|
520
|
+
```js
|
|
521
|
+
// Android
|
|
522
|
+
window.nativeBridge.getIDFA();
|
|
523
|
+
// iOS
|
|
524
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify({ fncName: 'getIDFA' }));
|
|
525
|
+
|
|
526
|
+
window.nativeCallback.getIDFA = (idfa) => {
|
|
527
|
+
console.log(idfa); // "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
|
528
|
+
};
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
### getIDFV() — iOS Only
|
|
534
|
+
|
|
535
|
+
디바이스 벤더 식별자(IDFV)를 가져옵니다. Android는 빈 문자열을 반환합니다.
|
|
536
|
+
|
|
537
|
+
```js
|
|
538
|
+
// Android
|
|
539
|
+
window.nativeBridge.getIDFV();
|
|
540
|
+
// iOS
|
|
541
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify({ fncName: 'getIDFV' }));
|
|
542
|
+
|
|
543
|
+
window.nativeCallback.getIDFV = (idfv) => {
|
|
544
|
+
console.log(idfv); // "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
|
545
|
+
};
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
### getDeviceInfo()
|
|
551
|
+
|
|
552
|
+
기기 정보를 JSON 문자열로 가져옵니다.
|
|
553
|
+
|
|
554
|
+
```js
|
|
555
|
+
// Android
|
|
556
|
+
window.nativeBridge.getDeviceInfo();
|
|
557
|
+
// iOS
|
|
558
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify({ fncName: 'getDeviceInfo' }));
|
|
559
|
+
|
|
560
|
+
window.nativeCallback.getDeviceInfo = (deviceInfo) => {
|
|
561
|
+
const info = JSON.parse(deviceInfo);
|
|
562
|
+
// Android: { network: "wifi", model: "SM-S911N", manufacturer: "Samsung", osVersion: "14" }
|
|
563
|
+
// iOS: { network: "wifi", model: "iPhone16,1", manufacturer: "Apple", osVersion: "17.2" }
|
|
564
|
+
};
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
| 필드 | 타입 | 값 |
|
|
568
|
+
| -------------- | -------- | ----------------------------------------- |
|
|
569
|
+
| `network` | `string` | `"wifi"` \| `"mobile"`. 연결 없으면 키 생략 |
|
|
570
|
+
| `model` | `string` | 기기 모델명 |
|
|
571
|
+
| `manufacturer` | `string` | 제조사 |
|
|
572
|
+
| `osVersion` | `string` | OS 버전 |
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
### registerBackKeyListener() — Android Only
|
|
577
|
+
|
|
578
|
+
디바이스 Back Key 이벤트 권한을 얻습니다. 등록 후 Back Key가 눌리면 `onBackKeyPress`가 호출됩니다. iOS는 항상 `"0"`을 반환합니다.
|
|
579
|
+
|
|
580
|
+
```js
|
|
581
|
+
// Android
|
|
582
|
+
window.nativeBridge.registerBackKeyListener();
|
|
583
|
+
// iOS
|
|
584
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify({ fncName: 'registerBackKeyListener' }));
|
|
585
|
+
|
|
586
|
+
window.nativeCallback.registerBackKeyListener = (success) => {
|
|
587
|
+
// "1": 권한 획득 성공
|
|
588
|
+
// "0": 권한 획득 실패 (또는 iOS)
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
window.nativeCallback.onBackKeyPress = () => {
|
|
592
|
+
// Back Key 눌렸을 때 처리
|
|
593
|
+
};
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
### unregisterBackKeyListener() — Android Only
|
|
599
|
+
|
|
600
|
+
Back Key 이벤트 권한을 해제합니다. 호출 후 Back Key는 웹뷰 히스토리 뒤로가기 또는 화면 종료로 동작합니다. iOS는 항상 no-op입니다.
|
|
601
|
+
|
|
602
|
+
```js
|
|
603
|
+
// Android
|
|
604
|
+
window.nativeBridge.unregisterBackKeyListener();
|
|
605
|
+
// iOS
|
|
606
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify({ fncName: 'unregisterBackKeyListener' }));
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
### swipeBackEnabled() — iOS Only
|
|
612
|
+
|
|
613
|
+
iOS 스와이프 백 기능 활성화 여부를 설정합니다.
|
|
614
|
+
|
|
615
|
+
```js
|
|
616
|
+
// Android
|
|
617
|
+
window.nativeBridge.swipeBackEnabled(JSON.stringify({ enabled: false }));
|
|
618
|
+
// iOS
|
|
619
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(
|
|
620
|
+
JSON.stringify({ fncName: 'swipeBackEnabled', params: { enabled: false } })
|
|
621
|
+
);
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
### finish()
|
|
627
|
+
|
|
628
|
+
현재 웹뷰를 종료합니다.
|
|
629
|
+
|
|
630
|
+
```js
|
|
631
|
+
// Android
|
|
632
|
+
window.nativeBridge.finish();
|
|
633
|
+
// iOS
|
|
634
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify({ fncName: 'finish' }));
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
### openTrackingSettings()
|
|
640
|
+
|
|
641
|
+
앱 설정 화면을 엽니다. 광고 추적 권한을 변경할 수 있도록 안내할 때 사용합니다.
|
|
642
|
+
|
|
643
|
+
```js
|
|
644
|
+
// Android
|
|
645
|
+
window.nativeBridge.openTrackingSettings();
|
|
646
|
+
// iOS
|
|
647
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(
|
|
648
|
+
JSON.stringify({ fncName: 'openTrackingSettings' })
|
|
649
|
+
);
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
### openExternalBrowser()
|
|
655
|
+
|
|
656
|
+
외부 브라우저를 엽니다.
|
|
657
|
+
|
|
658
|
+
```js
|
|
659
|
+
// Android
|
|
660
|
+
window.nativeBridge.openExternalBrowser('https://example.com');
|
|
661
|
+
// iOS
|
|
662
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(
|
|
663
|
+
JSON.stringify({ fncName: 'openExternalBrowser', params: { url: 'https://example.com' } })
|
|
664
|
+
);
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
### pushUrl()
|
|
670
|
+
|
|
671
|
+
현재 웹뷰에서 URL을 변경합니다.
|
|
672
|
+
|
|
673
|
+
```js
|
|
674
|
+
// Android
|
|
675
|
+
window.nativeBridge.pushUrl(JSON.stringify({ url: 'https://example.com', replace: false }));
|
|
676
|
+
// iOS
|
|
677
|
+
window.webkit.messageHandlers.nativeBridge.postMessage(
|
|
678
|
+
JSON.stringify({
|
|
679
|
+
fncName: 'pushUrl',
|
|
680
|
+
params: { url: 'https://example.com', replace: false }
|
|
681
|
+
})
|
|
682
|
+
);
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
| 파라미터 | 타입 | 설명 |
|
|
686
|
+
| --------- | --------- | --------------------------------------------------------- |
|
|
687
|
+
| `url` | `string` | 이동할 URL |
|
|
688
|
+
| `replace` | `boolean` | `true`: history 교체, `false`: history 스택에 추가 (기본) |
|
|
689
|
+
|
|
690
|
+
---
|