@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.
@@ -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
- Orientation.lockToPortrait();
153
+ lockPortrait();
170
154
  return () => {
171
- Orientation.unlockAllOrientations();
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
- let version = "";
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
- try {
296
- const result = await AdvertisingIdAaid.getAdvertisingInfo();
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
- try {
310
- const result = await AdvertisingIdAaid.getAdvertisingInfo();
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
- let idfv = "";
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
- const info: Record<string, string> = {};
334
- try {
335
- const netState = await NetInfo.fetch();
336
- if (netState.type === "wifi") info.network = "wifi";
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" && requestTrackingPermission) {
427
+ if (Platform.OS === "ios") {
428
+ // 네이티브 ATTrackingManager 로 ATT 요청. NSUserTrackingUsageDescription 이 있을 때만.
473
429
  try {
474
430
  if (await hasTrackingDescription()) {
475
- await requestTrackingPermission();
431
+ await requestTrackingAuthorization();
476
432
  }
477
433
  } catch (_) {}
478
434
  }
@@ -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;
@@ -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 라 자체 SafeAreaProvider inset 을 다시 측정한다.
107
+ {/* Modal 은 별도 window 라 네이티브에서 얻은 inset 을 상/하단 padding 으로 다시 적용한다.
87
108
  상단 safe-area 를 흰색으로 두고 상태바 아이콘을 dark-content(검정)로 표시한다. */}
88
- <SafeAreaProvider style={styles.safeArea}>
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
- <SafeAreaView style={styles.safeArea} edges={["top", "bottom"]}>
91
- <View style={styles.container}>
92
- {options && (
93
- <RewardWebView
94
- ref={rewardWebViewRef}
95
- key={options.url}
96
- url={options.url}
97
- allowedDomains={options.allowedDomains}
98
- debugEnabled={options.debugEnabled ?? false}
99
- onClose={handleClose}
100
- />
101
- )}
102
- </View>
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
  /** 초기화 성공 콜백 (선택). */