@tenqube/visual-reward-react-native 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +201 -606
- package/android/build.gradle +4 -0
- package/android/src/main/AndroidManifest.xml +9 -1
- package/android/src/main/java/com/tenqube/visualreward/VisualRewardModule.kt +141 -0
- package/ios/VisualReward.m +24 -0
- package/ios/VisualReward.swift +158 -0
- package/package.json +2 -8
- package/src/ConfigCache.ts +1 -7
- package/src/NativeVisualReward.ts +87 -0
- package/src/RewardWebView.tsx +22 -66
- package/src/VisualReward.ts +4 -6
- package/src/VisualRewardContainer.tsx +42 -18
- package/src/types.ts +12 -0
package/src/RewardWebView.tsx
CHANGED
|
@@ -15,38 +15,22 @@ import {
|
|
|
15
15
|
} from "react-native";
|
|
16
16
|
import WebView, { WebViewMessageEvent } from "react-native-webview";
|
|
17
17
|
import type { WebViewNavigation } from "react-native-webview/lib/WebViewTypes";
|
|
18
|
-
import DeviceInfo from "react-native-device-info";
|
|
19
|
-
import AdvertisingIdAaid from "react-native-idfa-aaid";
|
|
20
|
-
import NetInfo from "@react-native-community/netinfo";
|
|
21
|
-
import Orientation from "react-native-orientation-locker";
|
|
22
18
|
|
|
23
19
|
import type { RewardWebViewHandle, RewardWebViewProps } from "./types";
|
|
24
20
|
import { isAllowedDomain, normalizeUrl } from "./utils";
|
|
25
21
|
import {
|
|
26
22
|
attachAdBridges,
|
|
23
|
+
getAdvertisingId,
|
|
24
|
+
getAppVersion,
|
|
25
|
+
getDeviceInfo,
|
|
26
|
+
getIDFV,
|
|
27
27
|
handleIntentScheme,
|
|
28
28
|
hasTrackingDescription,
|
|
29
|
+
lockPortrait,
|
|
30
|
+
requestTrackingAuthorization,
|
|
31
|
+
unlockOrientations,
|
|
29
32
|
} from "./NativeVisualReward";
|
|
30
33
|
|
|
31
|
-
// ─── ATT 요청 (iOS) ──────────────────────────────────────────────────────────
|
|
32
|
-
// react-native-permissions로 ATT 권한 요청.
|
|
33
|
-
// NSUserTrackingUsageDescription이 없으면 try/catch로 스킵.
|
|
34
|
-
let requestTrackingPermission: (() => Promise<void>) | null = null;
|
|
35
|
-
if (Platform.OS === "ios") {
|
|
36
|
-
try {
|
|
37
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
38
|
-
const { request, PERMISSIONS } = require("react-native-permissions") as {
|
|
39
|
-
request: (permission: string) => Promise<string>;
|
|
40
|
-
PERMISSIONS: { IOS: { APP_TRACKING_TRANSPARENCY: string } };
|
|
41
|
-
};
|
|
42
|
-
requestTrackingPermission = async () => {
|
|
43
|
-
await request(PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY);
|
|
44
|
-
};
|
|
45
|
-
} catch (_) {
|
|
46
|
-
requestTrackingPermission = null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
34
|
// ─── 폴리필 스크립트 ──────────────────────────────────────────────────────────
|
|
51
35
|
/**
|
|
52
36
|
* Android / iOS 양쪽 호출 방식을 통합하는 폴리필을 웹뷰에 주입합니다.
|
|
@@ -166,9 +150,9 @@ const RewardWebView = React.forwardRef<RewardWebViewHandle, RewardWebViewProps>(
|
|
|
166
150
|
|
|
167
151
|
// ── 세로 모드 고정 ──────────────────────────────────────────────────────────
|
|
168
152
|
useEffect(() => {
|
|
169
|
-
|
|
153
|
+
lockPortrait();
|
|
170
154
|
return () => {
|
|
171
|
-
|
|
155
|
+
unlockOrientations();
|
|
172
156
|
};
|
|
173
157
|
}, []);
|
|
174
158
|
|
|
@@ -280,10 +264,7 @@ const RewardWebView = React.forwardRef<RewardWebViewHandle, RewardWebViewProps>(
|
|
|
280
264
|
switch (fncName) {
|
|
281
265
|
// ── getAppVersion ──────────────────────────────────────────────────
|
|
282
266
|
case "getAppVersion": {
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
version = DeviceInfo.getVersion();
|
|
286
|
-
} catch (_) {}
|
|
267
|
+
const version = await getAppVersion();
|
|
287
268
|
callNativeCallback("getAppVersion", version);
|
|
288
269
|
break;
|
|
289
270
|
}
|
|
@@ -292,11 +273,8 @@ const RewardWebView = React.forwardRef<RewardWebViewHandle, RewardWebViewProps>(
|
|
|
292
273
|
case "getADID": {
|
|
293
274
|
let adId = "";
|
|
294
275
|
if (Platform.OS === "android") {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const id = result.id ?? "";
|
|
298
|
-
if (id !== "00000000-0000-0000-0000-000000000000") adId = id;
|
|
299
|
-
} catch (_) {}
|
|
276
|
+
// 네이티브(AdvertisingIdClient)에 위임. 제로 UUID 필터링도 네이티브에서 처리.
|
|
277
|
+
adId = await getAdvertisingId();
|
|
300
278
|
}
|
|
301
279
|
callNativeCallback("getADID", adId);
|
|
302
280
|
break;
|
|
@@ -306,11 +284,8 @@ const RewardWebView = React.forwardRef<RewardWebViewHandle, RewardWebViewProps>(
|
|
|
306
284
|
case "getIDFA": {
|
|
307
285
|
let idfa = "";
|
|
308
286
|
if (Platform.OS === "ios") {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const id = result.id ?? "";
|
|
312
|
-
if (id !== "00000000-0000-0000-0000-000000000000") idfa = id;
|
|
313
|
-
} catch (_) {}
|
|
287
|
+
// 네이티브(ASIdentifierManager)에 위임. Swift 와 동일하게 ATT 승인 시에만 반환.
|
|
288
|
+
idfa = await getAdvertisingId();
|
|
314
289
|
}
|
|
315
290
|
callNativeCallback("getIDFA", idfa);
|
|
316
291
|
break;
|
|
@@ -318,37 +293,17 @@ const RewardWebView = React.forwardRef<RewardWebViewHandle, RewardWebViewProps>(
|
|
|
318
293
|
|
|
319
294
|
// ── getIDFV (iOS only) ────────────────────────────────────────────
|
|
320
295
|
case "getIDFV": {
|
|
321
|
-
|
|
322
|
-
if (Platform.OS === "ios") {
|
|
323
|
-
try {
|
|
324
|
-
idfv = await DeviceInfo.getUniqueId();
|
|
325
|
-
} catch (_) {}
|
|
326
|
-
}
|
|
296
|
+
const idfv = await getIDFV();
|
|
327
297
|
callNativeCallback("getIDFV", idfv);
|
|
328
298
|
break;
|
|
329
299
|
}
|
|
330
300
|
|
|
331
301
|
// ── getDeviceInfo ─────────────────────────────────────────────────
|
|
332
302
|
case "getDeviceInfo": {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
else if (netState.type === "cellular") info.network = "mobile";
|
|
338
|
-
} catch (_) {}
|
|
339
|
-
try {
|
|
340
|
-
if (Platform.OS === "android") {
|
|
341
|
-
info.model = DeviceInfo.getModel();
|
|
342
|
-
info.manufacturer = await DeviceInfo.getManufacturer();
|
|
343
|
-
info.osVersion = DeviceInfo.getSystemVersion();
|
|
344
|
-
} else {
|
|
345
|
-
// iOS: hardware id (예: "iPhone16,1")
|
|
346
|
-
info.model = await DeviceInfo.getDeviceId();
|
|
347
|
-
info.manufacturer = "Apple";
|
|
348
|
-
info.osVersion = DeviceInfo.getSystemVersion();
|
|
349
|
-
}
|
|
350
|
-
} catch (_) {}
|
|
351
|
-
callNativeCallback("getDeviceInfo", JSON.stringify(info));
|
|
303
|
+
// 네이티브에서 network/model/manufacturer/osVersion 을 JSON 문자열로 조립해 반환.
|
|
304
|
+
// 웹 콜백은 문자열을 받아 JSON.parse 하므로 rawValue=false(기본)로 감싼다.
|
|
305
|
+
const info = await getDeviceInfo();
|
|
306
|
+
callNativeCallback("getDeviceInfo", info);
|
|
352
307
|
break;
|
|
353
308
|
}
|
|
354
309
|
|
|
@@ -469,10 +424,11 @@ const RewardWebView = React.forwardRef<RewardWebViewHandle, RewardWebViewProps>(
|
|
|
469
424
|
const handleLoadEnd = useCallback(async () => {
|
|
470
425
|
if (attachStartedRef.current) return; // 실제 URL 로드 완료 시엔 재실행 안 함
|
|
471
426
|
attachStartedRef.current = true;
|
|
472
|
-
if (Platform.OS === "ios"
|
|
427
|
+
if (Platform.OS === "ios") {
|
|
428
|
+
// 네이티브 ATTrackingManager 로 ATT 요청. NSUserTrackingUsageDescription 이 있을 때만.
|
|
473
429
|
try {
|
|
474
430
|
if (await hasTrackingDescription()) {
|
|
475
|
-
await
|
|
431
|
+
await requestTrackingAuthorization();
|
|
476
432
|
}
|
|
477
433
|
} catch (_) {}
|
|
478
434
|
}
|
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;
|
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
StyleSheet,
|
|
8
8
|
View,
|
|
9
9
|
} from "react-native";
|
|
10
|
-
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
|
|
11
10
|
import RewardWebView from "./RewardWebView";
|
|
12
11
|
import { VisualReward } from "./VisualReward";
|
|
12
|
+
import { getSafeAreaInsets, type SafeAreaInsets } from "./NativeVisualReward";
|
|
13
13
|
import type { RewardWebViewHandle, RewardWebViewProps } from "./types";
|
|
14
14
|
|
|
15
|
+
const ZERO_INSETS: SafeAreaInsets = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* VisualRewardContainer
|
|
17
19
|
*
|
|
@@ -41,6 +43,20 @@ const VisualRewardContainer: React.FC = () => {
|
|
|
41
43
|
const optionsRef = useRef<RewardWebViewProps | null>(null);
|
|
42
44
|
optionsRef.current = options;
|
|
43
45
|
|
|
46
|
+
// safe-area inset — react-native-safe-area-context 대신 네이티브에서 조회한다.
|
|
47
|
+
// (Modal 은 별도 window 라 상/하단 inset 을 다시 적용해야 상태바·제스처바에 안 가린다.)
|
|
48
|
+
const [insets, setInsets] = useState<SafeAreaInsets>(ZERO_INSETS);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!visible) return;
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
getSafeAreaInsets().then((v) => {
|
|
53
|
+
if (!cancelled) setInsets(v);
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
cancelled = true;
|
|
57
|
+
};
|
|
58
|
+
}, [visible]);
|
|
59
|
+
|
|
44
60
|
// VisualReward 싱글톤에 핸들러 등록
|
|
45
61
|
useEffect(() => {
|
|
46
62
|
VisualReward._register((opts) => {
|
|
@@ -80,28 +96,36 @@ const VisualRewardContainer: React.FC = () => {
|
|
|
80
96
|
visible={visible}
|
|
81
97
|
animationType="slide"
|
|
82
98
|
presentationStyle="fullScreen"
|
|
99
|
+
// Android: 상/하단 모두 edge-to-edge 로 둔다. targetSdk 35+ 는 Activity 가 edge-to-edge 로
|
|
100
|
+
// 강제되므로, Modal 하단도 네비게이션 바 아래까지 그려지게 해야(navigationBarTranslucent)
|
|
101
|
+
// 네이티브 systemBars inset 을 padding 으로 적용했을 때 이중 여백이 생기지 않는다.
|
|
102
|
+
// (navigationBarTranslucent 는 statusBarTranslucent 가 true 여야 동작)
|
|
83
103
|
statusBarTranslucent={Platform.OS === "android"}
|
|
104
|
+
navigationBarTranslucent={Platform.OS === "android"}
|
|
84
105
|
onRequestClose={handleBackPress}
|
|
85
106
|
>
|
|
86
|
-
{/* Modal 은 별도 window 라
|
|
107
|
+
{/* Modal 은 별도 window 라 네이티브에서 얻은 inset 을 상/하단 padding 으로 다시 적용한다.
|
|
87
108
|
상단 safe-area 를 흰색으로 두고 상태바 아이콘을 dark-content(검정)로 표시한다. */}
|
|
88
|
-
<
|
|
109
|
+
<View
|
|
110
|
+
style={[
|
|
111
|
+
styles.safeArea,
|
|
112
|
+
{ paddingTop: insets.top, paddingBottom: insets.bottom },
|
|
113
|
+
]}
|
|
114
|
+
>
|
|
89
115
|
<StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
</SafeAreaView>
|
|
104
|
-
</SafeAreaProvider>
|
|
116
|
+
<View style={styles.container}>
|
|
117
|
+
{options && (
|
|
118
|
+
<RewardWebView
|
|
119
|
+
ref={rewardWebViewRef}
|
|
120
|
+
key={options.url}
|
|
121
|
+
url={options.url}
|
|
122
|
+
allowedDomains={options.allowedDomains}
|
|
123
|
+
debugEnabled={options.debugEnabled ?? false}
|
|
124
|
+
onClose={handleClose}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
105
129
|
</Modal>
|
|
106
130
|
);
|
|
107
131
|
};
|
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
|
/** 초기화 성공 콜백 (선택). */
|