@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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @tenqube/visual-reward
2
2
 
3
+ ## 1.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 4c13cbd: feat: setStorage 제거 및 initialize storage 파라미터 추가합니다.
8
+
3
9
  ## 1.1.5
4
10
 
5
11
  ### Patch Changes
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 21
12
-
13
- ---
9
+ - Android minSdk 24
14
10
 
15
11
  ## 설치
16
12
 
17
- ### 방법 1) npm (권장)
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
- > 광고 연동(GMA / AdPopcorn SSP)은 **선택적 peer dependency**이며, 사용할 때만 설치합니다. ([AdMob / Ad Manager WebView 광고 연동](#admob--ad-manager-webview-광고-연동-선택) 참고)
15
+ - https://www.npmjs.com/package/@tenqube/visual-reward-react-native
32
16
 
33
- ### 방법 2) 수동 설치 (GitHub Release)
17
+ ```bash
18
+ npm install @tenqube/visual-reward-react-native react-native-webview
19
+ ```
34
20
 
35
- npm을 쓰지 않는 경우, [GitHub Release](https://github.com/tenqube/visual-reward-sdk-react-native/releases)에서 `visual-reward-react-native-<버전>.zip`을 내려받아 압축을 풀고 프로젝트 내 원하는 위치에 복사합니다. (zip에는 `android/`, `ios/` 네이티브 모듈이 포함되어 있습니다.) 그런 다음 `package.json`에 `file:` 경로 및 `react-native-webview`를 추가합니다.
21
+ ### 수동 설치
36
22
 
37
- ```json
38
- "dependencies": {
39
- "@tenqube/visual-reward-react-native": "file:./visual-reward-react-native",
40
- "react-native-webview": ">=13.0.0"
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
- ```sh
45
- npm install
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
- ### 공통) iOS pod install
35
+ ```bash
36
+ npm install
37
+ ```
49
38
 
50
- 방법 모두 iOS 추가로 pod install을 실행합니다.
39
+ ### iOS pod install
51
40
 
52
- ```sh
53
- cd ios && pod install
54
- ```
41
+ - 두 방법 모두 iOS는 추가로 `pod install`을 실행합니다.
55
42
 
56
- > 이 패키지는 GMA registerWebView / AdPopcorn 브릿지 등록을 위한 **네이티브 모듈**(iOS Swift / Android Kotlin)을 포함하며, React Native 오토링크로 자동 연결됩니다. (iOS는 `pod install`, Android는 Gradle sync 시 자동 반영)
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
- # WebView JavascriptInterface
88
- -keepclassmembers class * {
89
- @android.webkit.JavascriptInterface <methods>;
90
- }
91
- ```
66
+ ### iOS
92
67
 
93
- ---
68
+ - IDFA 조회(`getIDFA`)를 사용하려면 `Info.plist`에 ATT 사용 목적 문자열을 추가해야 합니다.
94
69
 
95
- ## AdMob / Ad Manager WebView 광고 연동 (선택)
70
+ ```xml
71
+ <key>NSUserTrackingUsageDescription</key>
72
+ <string>맞춤형 광고 제공을 위해 기기 식별자를 사용합니다.</string>
73
+ ```
96
74
 
97
- WebView 내부 광고(AdSense / Google Publisher Tag / IMA HTML5)의 수익화·측정 신호를 개선하는 WebView API for Ads를 지원합니다. AdMob과 Google Ad Manager(GAM)는 동일한 Google Mobile Ads SDK를 사용하므로 양쪽 모두 동작합니다.
75
+ > ATT 권한 요청은 SDK가 웹뷰 로드 네이티브 `ATTrackingManager`로 자동 처리합니다. 이미 권한이 결정된 경우 팝업 없이 즉시 진행됩니다.
98
76
 
99
- SDK는 호스트 앱에 **GMA SDK가 있을 때만** 네이티브 모듈에서 리플렉션으로 `registerWebView`를 자동 호출합니다. AdPopcorn SSP 오퍼월을 쓰는 경우에는 **AdPopcorn 네이티브 브릿지(`AdPopcornSSPJsBridge`)도 자동 주입**합니다(오퍼월의 웹 배너 SDK가 네이티브 컨텍스트를 필요로 하므로 **실제 네이티브 브릿지**를 연결 — 순수 JS shim 으로는 불가). SDK가 하는 일은 이 연결뿐이며, **SDK 의존성·App ID·초기화는 모두 호스트 앱에서 설정**해야 합니다.
77
+ ## Ad Manager WebView 광고 연동
100
78
 
101
- > react-native-webview는 "로드 전 네이티브 인터페이스 주입" 훅이 없어, SDK 실제 오퍼월 URL 로드 전에 `about:blank`를 먼저 띄워 브릿지를 주입한 뒤 URL을 로드합니다. 덕분에 오퍼월은 한 번만 로드되고 첫 페이지부터 브릿지가 바인딩됩니다(리로드 불필요).
79
+ - **SDK 의존성·App ID·초기화는 모두 호스트 앱에서 설정이 필요합니다.**
80
+ - 호스트 앱에 Google Mobile Ads SDK가 링크돼 있으면, SDK가 리워드 웹뷰를 자동으로 `registerWebView`에 등록해 WebView 내부 광고(AdSense/GPT/IMA HTML5)의 수익화·측정 신호를 개선합니다.
81
+ - 기존에 애드몹 등을 사용 중인 경우 정상 적용 여부만 확인해주시면 됩니다.
102
82
 
103
- ### 1. 의존성 (호스트 앱)
83
+ ### 의존성
104
84
 
105
- ```sh
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
- AdPopcorn 미디에이션으로 **AppLovin MAX** 를 쓰면 네트워크 SDK 를 추가합니다 (어댑터는 AdPopcorn SSP 코어에 번들, 네트워크 SDK 만 추가). flutter/kotlin/swift 와 동일 버전대.
90
+ ### App ID
113
91
 
114
- ```ruby
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
- > 이 SDK는 위 패키지에 **컴파일 의존하지 않습니다**(리플렉션·뷰탐색만 사용). 호스트가 링크하면 동작하고, 없으면 무시됩니다. `react-native-google-mobile-ads`는 GMA 네이티브 심볼을 앱에 링크하는 용도이며, 등록 자체는 이 SDK가 직접 수행합니다. AdPopcorn 은 `react-native-adpopcorn-ssp` 가 번들한 **네이티브 SSP SDK** 를 SDK 가 리플렉션으로 웹뷰에 연결합니다.
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
- ### 2. App ID / 초기화 — 아래 두 경우 중 해당하는 쪽으로 설정
105
+ 2. Info.plist
127
106
 
128
- - **WebView 신호 전용**: 이 앱으로 **자체 광고를 서빙하지 않고** WebView 안 광고 신호만 쓰는 경우
129
- - **자체 광고 병행**: 호스트 앱이 GMA로 자기 앱에 **자체 인앱 광고(배너·전면 등)도 함께** 서빙하는 경우
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
- | 경우 | iOS (`Info.plist`) | Android (`AndroidManifest.xml`) |
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
- > ⚠️ react-native-webview가 내부 WebView 인스턴스를 노출하지 않아, 네이티브 뷰 계층에서 WebView를 탐색해 등록하는 **best-effort** 방식입니다. 화면에 보이는 가장 큰 WebView를 리워드 웹뷰로 간주합니다.
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
- ### Android
128
+ ## 시퀀스 다이어그램
194
129
 
195
- 광고 ID 접근 권한(`com.google.android.gms.permission.AD_ID`)은 SDK가 의존하는 `play-services-ads-identifier`가 매니페스트 병합으로 **자동 추가**하므로, 호스트가 별도로 선언할 필요가 없습니다. (광고 ID를 수집하는 앱이 이 권한을 선언하는 것은 Play 정책상 올바른 동작입니다.)
130
+ ```mermaid
131
+ sequenceDiagram
132
+ title interaction VisualReward SDK
196
133
 
197
- 아동 대상 광고 ID 권한을 원치 않는 경우에만 호스트 `AndroidManifest.xml`에서 명시적으로 제거할 수 있습니다.
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
- ```xml
200
- <uses-permission android:name="com.google.android.gms.permission.AD_ID"
201
- tools:node="remove" />
202
- ```
139
+ A->>B: initialize(appKey, ...)
140
+ B->>C: config 요청
141
+ C-->>B: config 응답
142
+ B-->>A: onSuccess()
203
143
 
204
- ### iOS
144
+ A->>B: setUserId(userId)
205
145
 
206
- IDFA 조회(`getIDFA`)를 사용하려면 `Info.plist`에 ATT 사용 목적 문자열을 추가해야 합니다.
146
+ A->>B: open(serviceName, ...)
147
+ B->>D: 웹뷰 열기
207
148
 
208
- ```xml
209
- <key>NSUserTrackingUsageDescription</key>
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
- VisualReward.setDebugEnabled(__DEV__);
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
- useEffect(() => {
254
- VisualReward.initialize({
255
- appKey: 'tq_live_xxx',
256
- timeoutMs: 5000, // 기본값 (ms)
257
- onSuccess: () => { /* SDK 준비 완료 */ },
258
- onFailure: (e) => { /* 초기화 실패 */ },
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
- ### initialize 파라미터
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
- ```tsx
287
- import { VisualReward } from '@tenqube/visual-reward-react-native';
288
-
289
- await VisualReward.open({
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
- ### open 파라미터
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
- `VisualRewardError`의 `code` 프로퍼티로 에러 종류를 구분합니다.
314
-
315
- | `code` | 설명 |
316
- | -------------------- | ----------------------------------------- |
317
- | `NETWORK_ERROR` | 네트워크 연결 실패 |
318
- | `INITIALIZE_TIMEOUT` | `initialize()` 호출 후 완료까지 타임아웃 |
319
- | `INVALID_APP_KEY` | 존재하지 않는 키 (404) |
320
- | `SERVER_ERROR` | 서버 오류 (5xx) |
321
- | `PARSE_ERROR` | 응답 파싱 실패 |
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` 콜백에서 `VisualRewardError`의 `code`를 분기합니다.
328
-
329
- ```tsx
330
- import { VisualReward, VisualRewardError } from '@tenqube/visual-reward-react-native';
331
-
332
- VisualReward.initialize({
333
- appKey: 'tq_live_xxx',
334
- timeoutMs: 5000,
335
- onSuccess: () => { /* SDK 준비 완료 */ },
336
- onFailure: (e) => {
337
- if (e instanceof VisualRewardError) {
338
- switch (e.code) {
339
- case 'NETWORK_ERROR':
340
- break; // 네트워크 연결 실패
341
- case 'INITIALIZE_TIMEOUT':
342
- break; // 초기화 타임아웃
343
- case 'INVALID_APP_KEY':
344
- break; // 존재하지 않는 앱 키
345
- case 'SERVER_ERROR':
346
- break; // 서버 오류
347
- case 'PARSE_ERROR':
348
- break; // 응답 파싱 실패
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()`은 예외를 던지지 않습니다. 오류는 `onError` 콜백으로 전달됩니다 (flutter/kotlin/swift SDK 동일).
358
-
359
- ```tsx
360
- import { VisualReward, VisualRewardError } from '@tenqube/visual-reward-react-native';
361
-
362
- await VisualReward.open({
363
- serviceName: 'reward_home',
364
- locale: 'ko',
365
- extra: undefined,
366
- onClose: () => { /* 웹뷰 닫힘 */ },
367
- onError: (e: VisualRewardError) => {
368
- switch (e.code) {
369
- case 'NOT_INITIALIZED':
370
- break; // initialize() 미호출
371
- case 'USER_ID_NOT_SET':
372
- break; // setUserId() 미호출
373
- case 'INITIALIZE_TIMEOUT':
374
- break; // 초기화 대기 타임아웃
375
- case 'NETWORK_ERROR':
376
- break; // 네트워크 연결 실패
377
- default:
378
- break; // 기타 오류
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenqube/visual-reward-react-native",
3
- "version": "1.1.5",
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",
@@ -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
  }
@@ -5,7 +5,7 @@ import type {
5
5
  VisualRewardInitializeOptions,
6
6
  VisualRewardOpenOptions,
7
7
  } from './types';
8
- import { readCache, writeCache, setStorage, AsyncStorageLike } from './ConfigCache';
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
  /** 초기화 성공 콜백 (선택). */