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