@apps-in-toss/native-modules 0.0.0-dev.1752049503789 → 0.0.0-dev.1757040677030

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.
Files changed (76) hide show
  1. package/dist/bridges-meta.json +36 -13
  2. package/dist/index.cjs +321 -121
  3. package/dist/index.d.cts +1189 -419
  4. package/dist/index.d.ts +1189 -419
  5. package/dist/index.js +298 -104
  6. package/package.json +7 -8
  7. package/src/AppsInTossModule/constants.ts +6 -0
  8. package/src/AppsInTossModule/native-event-emitter/appsInTossEvent.ts +13 -0
  9. package/src/AppsInTossModule/native-event-emitter/contactsViral.ts +140 -0
  10. package/src/AppsInTossModule/native-event-emitter/event-plugins/EntryMessageExitedEvent.ts +10 -0
  11. package/src/AppsInTossModule/native-event-emitter/event-plugins/UpdateLocationEvent.ts +60 -0
  12. package/src/AppsInTossModule/native-event-emitter/index.ts +5 -0
  13. package/src/AppsInTossModule/native-event-emitter/internal/AppBridgeCallbackEvent.ts +45 -0
  14. package/src/AppsInTossModule/native-event-emitter/internal/VisibilityChangedByTransparentServiceWebEvent.ts +50 -0
  15. package/src/AppsInTossModule/native-event-emitter/internal/appBridge.spec.ts +135 -0
  16. package/src/AppsInTossModule/native-event-emitter/internal/appBridge.ts +79 -0
  17. package/src/AppsInTossModule/native-event-emitter/internal/onVisibilityChangedByTransparentServiceWeb.ts +20 -0
  18. package/src/AppsInTossModule/native-event-emitter/nativeEventEmitter.ts +35 -0
  19. package/src/AppsInTossModule/native-event-emitter/startUpdateLocation.ts +98 -0
  20. package/src/AppsInTossModule/native-event-emitter/types.ts +4 -0
  21. package/src/AppsInTossModule/native-modules/AppsInTossModule.ts +89 -0
  22. package/src/AppsInTossModule/native-modules/ads/googleAdMob.ts +681 -0
  23. package/src/AppsInTossModule/native-modules/ads/googleAdMobV2.ts +363 -0
  24. package/src/AppsInTossModule/native-modules/ads/types.ts +123 -0
  25. package/src/AppsInTossModule/native-modules/appLogin.ts +29 -0
  26. package/src/AppsInTossModule/native-modules/checkoutPayment.ts +80 -0
  27. package/src/AppsInTossModule/native-modules/eventLog.spec.ts +300 -0
  28. package/src/AppsInTossModule/native-modules/eventLog.ts +77 -0
  29. package/src/AppsInTossModule/native-modules/fetchAlbumPhotos.ts +88 -0
  30. package/src/AppsInTossModule/native-modules/fetchContacts.ts +121 -0
  31. package/src/AppsInTossModule/native-modules/getClipboardText.ts +47 -0
  32. package/src/AppsInTossModule/native-modules/getCurrentLocation.ts +65 -0
  33. package/src/AppsInTossModule/native-modules/getDeviceId.ts +33 -0
  34. package/src/AppsInTossModule/native-modules/getGameCenterGameProfile.ts +68 -0
  35. package/src/AppsInTossModule/native-modules/getOperationalEnvironment.ts +37 -0
  36. package/src/AppsInTossModule/native-modules/getPermission.ts +58 -0
  37. package/src/AppsInTossModule/native-modules/getTossAppVersion.ts +33 -0
  38. package/src/AppsInTossModule/native-modules/getTossShareLink.ts +39 -0
  39. package/src/AppsInTossModule/native-modules/iap.ts +213 -0
  40. package/src/AppsInTossModule/native-modules/index.ts +104 -0
  41. package/src/AppsInTossModule/native-modules/isMinVersionSupported.spec.ts +190 -0
  42. package/src/AppsInTossModule/native-modules/isMinVersionSupported.ts +68 -0
  43. package/src/AppsInTossModule/native-modules/openCamera.ts +81 -0
  44. package/src/AppsInTossModule/native-modules/openGameCenterLeaderboard.ts +44 -0
  45. package/src/AppsInTossModule/native-modules/openPermissionDialog.ts +54 -0
  46. package/src/AppsInTossModule/native-modules/requestPermission.ts +63 -0
  47. package/src/AppsInTossModule/native-modules/saveBase64Data.ts +57 -0
  48. package/src/AppsInTossModule/native-modules/setClipboardText.ts +39 -0
  49. package/src/AppsInTossModule/native-modules/setDeviceOrientation.ts +74 -0
  50. package/src/AppsInTossModule/native-modules/storage.ts +100 -0
  51. package/src/AppsInTossModule/native-modules/submitGameCenterLeaderBoardScore.ts +74 -0
  52. package/src/AppsInTossModule/native-modules/tossCore.ts +29 -0
  53. package/src/BedrockModule/native-modules/core/BedrockCoreModule.ts +8 -0
  54. package/src/BedrockModule/native-modules/index.ts +4 -0
  55. package/src/BedrockModule/native-modules/natives/BedrockModule.ts +20 -0
  56. package/src/BedrockModule/native-modules/natives/closeView.ts +25 -0
  57. package/src/BedrockModule/native-modules/natives/generateHapticFeedback/index.ts +27 -0
  58. package/src/BedrockModule/native-modules/natives/generateHapticFeedback/types.ts +38 -0
  59. package/src/BedrockModule/native-modules/natives/getLocale.ts +46 -0
  60. package/src/BedrockModule/native-modules/natives/getNetworkStatus/index.ts +59 -0
  61. package/src/BedrockModule/native-modules/natives/getNetworkStatus/types.ts +1 -0
  62. package/src/BedrockModule/native-modules/natives/getPlatformOS.ts +37 -0
  63. package/src/BedrockModule/native-modules/natives/getSchemeUri.ts +27 -0
  64. package/src/BedrockModule/native-modules/natives/index.ts +11 -0
  65. package/src/BedrockModule/native-modules/natives/openURL.ts +40 -0
  66. package/src/BedrockModule/native-modules/natives/setIosSwipeGestureEnabled.ts +43 -0
  67. package/src/BedrockModule/native-modules/natives/setScreenAwakeMode.ts +66 -0
  68. package/src/BedrockModule/native-modules/natives/setSecureScreen.ts +31 -0
  69. package/src/BedrockModule/native-modules/natives/share.ts +36 -0
  70. package/src/async-bridges.ts +3 -0
  71. package/src/event-bridges.ts +2 -0
  72. package/src/index.ts +16 -0
  73. package/src/types.ts +108 -0
  74. package/src/utils/compareVersion.spec.ts +176 -0
  75. package/src/utils/compareVersion.ts +104 -0
  76. package/src/utils/generateUUID.ts +5 -0
@@ -0,0 +1,140 @@
1
+ import { INTERNAL__appBridgeHandler } from './internal/appBridge';
2
+ import { isMinVersionSupported } from '../native-modules/isMinVersionSupported';
3
+
4
+ /**
5
+ * @public
6
+ * @category 친구초대
7
+ * @name RewardFromContactsViralEvent
8
+ * @description 친구에게 공유하기를 완료했을 때 지급할 리워드 정보를 담는 타입이에요. 이 타입을 사용하면 공유가 완료됐을 때 지급할 리워드 정보를 확인할 수 있어요.
9
+ * @property {'sendViral'} type - 이벤트의 타입이에요. `'sendViral'`은 사용자가 친구에게 공유를 완료했을 때 돌아와요.
10
+ * @property {Object} data - 지급할 리워드 관련 정보를 담고 있어요.
11
+ * @property {number} data.rewardAmount - 지급할 리워드 수량이에요. 앱인토스 콘솔에서 설정한 수량 및 금액 값이에요.
12
+ * @property {string} data.rewardUnit - 리워드의 단위예요. 앱인토스 콘솔에 설정된 리워드 이름인 '하트', '보석' 등이 리워드 단위예요.
13
+ */
14
+ type RewardFromContactsViralEvent = {
15
+ type: 'sendViral';
16
+ data: {
17
+ rewardAmount: number;
18
+ rewardUnit: string;
19
+ };
20
+ };
21
+
22
+ /**
23
+ * @public
24
+ * @category 친구초대
25
+ * @name ContactsViralSuccessEvent
26
+ * @description 연락처 공유 모듈이 정상적으로 종료됐을 때 전달되는 이벤트 객체예요. 종료 이유와 함께 리워드 상태 및 남은 친구 수 등 관련 정보를 제공해요.
27
+ * @property {'close'} type - 이벤트의 타입이에요. `'close'`는 공유 모듈이 종료됐을 때 돌아와요.
28
+ * @property {Object} data - 모듈 종료와 관련된 세부 정보를 담고 있어요.
29
+ * @property {'clickBackButton' | 'noReward'} data.closeReason - 모듈이 종료된 이유예요. `'clickBackButton'`은 사용자가 뒤로 가기 버튼을 눌러 종료한 경우이고, `'noReward'`는 받을 수 있는 리워드가 없어서 종료된 경우예요.
30
+ * @property {number} data.sentRewardAmount - 사용자가 받은 전체 리워드 수량이에요.
31
+ * @property {number} data.sendableRewardsCount - 아직 공유할 수 있는 친구 수예요.
32
+ * @property {number} data.sentRewardsCount - 사용자가 공유를 완료한 친구 수예요.
33
+ * @property {string} data.rewardUnit - 리워드의 단위예요. 앱인토스 콘솔에 설정된 리워드 이름인 '하트', '보석' 등이 리워드 단위예요.
34
+ */
35
+ type ContactsViralSuccessEvent = {
36
+ type: 'close';
37
+ data: {
38
+ closeReason: 'clickBackButton' | 'noReward';
39
+ sentRewardAmount?: number;
40
+ sendableRewardsCount?: number;
41
+ sentRewardsCount: number;
42
+ rewardUnit?: string;
43
+ };
44
+ };
45
+
46
+ type ContactsViralEvent = RewardFromContactsViralEvent | ContactsViralSuccessEvent;
47
+
48
+ /**
49
+ * @public
50
+ * @category 친구초대
51
+ * @name ContactsViralOption
52
+ * @description [연락처 공유 기능](/react-native/reference/native-modules/친구초대/contactsViral.html)을 사용할 때 필요한 옵션이에요.
53
+ * @property {string} moduleId - 공유 리워드를 구분하는 UUID 형식의 고유 ID예요. 앱인토스 콘솔의 미니앱 > 공유 리워드 메뉴에서 확인할 수 있어요.
54
+ */
55
+ type ContactsViralOption = {
56
+ moduleId: string;
57
+ };
58
+
59
+ /**
60
+ * @public
61
+ * @category 친구초대
62
+ * @name ContactsViralParams
63
+ * @description `ContactsViralParams`는 연락처 공유 기능을 사용할 때 전달해야 하는 파라미터 타입이에요. 옵션을 설정하고, 이벤트 및 에러 처리 콜백을 지정할 수 있어요.
64
+ * @property {ContactsViralOption} options - 공유 기능에 사용할 옵션 객체예요.
65
+ * @property {(event: ContactsViralEvent) => void} onEvent - 공유 이벤트가 발생했을 때 실행되는 함수예요. [`RewardFromContactsViralEvent`](/bedrock/reference/native-modules/친구초대/RewardFromContactsViralEvent.html) 또는 [`ContactsViralSuccessEvent`](/react-native/reference/native-modules/친구초대/ContactsViralSuccessEvent.html) 타입의 이벤트 객체가 전달돼요.
66
+ * @property {(error: unknown) => void} onError - 예기치 않은 에러가 발생했을 때 실행되는 함수예요.
67
+ */
68
+ export interface ContactsViralParams {
69
+ options: ContactsViralOption;
70
+ onEvent: (event: ContactsViralEvent) => void;
71
+ onError: (error: unknown) => void;
72
+ }
73
+
74
+ /**
75
+ * @public
76
+ * @category 친구초대
77
+ * @name contactsViral
78
+ * @description 친구에게 공유하고 리워드를 받을 수 있는 기능을 제공해요. 사용자가 친구에게 공유를 완료하면 앱브릿지가 이벤트를 통해 리워드 정보를 전달해요.
79
+ * @param {ContactsViralParams} params - 연락처 공유 기능을 실행할 때 사용하는 파라미터예요. 옵션 설정과 이벤트 핸들러를 포함해요. 자세한 내용은 [ContactsViralParams](/bedrock/reference/native-modules/친구초대/ContactsViralParams.html) 문서를 참고하세요.
80
+ * @returns {() => void} 앱브릿지 cleanup 함수를 반환해요. 공유 기능이 끝나면 반드시 이 함수를 호출해서 리소스를 해제해야 해요.
81
+ *
82
+ * @example
83
+ * ### 친구에게 공유하고 리워드 받기
84
+ *
85
+ * ```tsx
86
+ * import { useCallback } from 'react';
87
+ * import { Button } from 'react-native';
88
+ * import { contactsViral } from '@apps-in-toss/framework';
89
+ *
90
+ * function ContactsViralButton({ moduleId }: { moduleId: string }) {
91
+ * const handleContactsViral = useCallback(() => {
92
+ * try {
93
+ * const cleanup = contactsViral({
94
+ * options: { moduleId: moduleId.trim() },
95
+ * onEvent: (event) => {
96
+ * if (event.type === 'sendViral') {
97
+ * console.log('리워드 지급:', event.data.rewardAmount, event.data.rewardUnit);
98
+ * } else if (event.type === 'close') {
99
+ * console.log('모듈 종료:', event.data.closeReason);
100
+ * }
101
+ * },
102
+ * onError: (error) => {
103
+ * console.error('에러 발생:', error);
104
+ * },
105
+ * });
106
+ *
107
+ * return cleanup;
108
+ * } catch (error) {
109
+ * console.error('실행 중 에러:', error);
110
+ * }
111
+ * }, [moduleId]);
112
+ *
113
+ * return <Button title="친구에게 공유하고 리워드 받기" onPress={handleContactsViral} />;
114
+ * }
115
+ * ```
116
+ */
117
+ export function contactsViral(params: ContactsViralParams) {
118
+ const isSupported = isMinVersionSupported({
119
+ android: '5.223.0',
120
+ ios: '5.223.0',
121
+ });
122
+
123
+ if (!isSupported) {
124
+ return () => {};
125
+ }
126
+
127
+ const { onEvent, onError, options } = params;
128
+
129
+ const unregisterCallbacks = INTERNAL__appBridgeHandler.invokeAppBridgeMethod('contactsViral', options, {
130
+ onRewardFromContactsViral: (result: RewardFromContactsViralEvent['data']) => {
131
+ onEvent({ type: 'sendViral', data: result });
132
+ },
133
+ onSuccess: (result: ContactsViralSuccessEvent['data']) => {
134
+ onEvent({ type: 'close', data: result });
135
+ },
136
+ onError,
137
+ });
138
+
139
+ return unregisterCallbacks;
140
+ }
@@ -0,0 +1,10 @@
1
+ import { GraniteEventDefinition } from '@granite-js/react-native';
2
+
3
+ export class EntryMessageExitedEvent extends GraniteEventDefinition<undefined, undefined> {
4
+ name = 'entryMessageExited' as const;
5
+
6
+ remove() {}
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
9
+ listener(_: undefined) {}
10
+ }
@@ -0,0 +1,60 @@
1
+ import { GraniteEventDefinition } from '@granite-js/react-native';
2
+ import { Accuracy, Location } from '../../../types';
3
+ import { AppsInTossModuleInstance } from '../../native-modules/AppsInTossModule';
4
+ import { requestPermission } from '../../native-modules/requestPermission';
5
+ import { nativeEventEmitter } from '../nativeEventEmitter';
6
+
7
+ export interface StartUpdateLocationOptions {
8
+ /**
9
+ * 위치 정확도를 설정해요.
10
+ */
11
+ accuracy: Accuracy;
12
+ /**
13
+ * 위치 업데이트 주기를 밀리초(ms) 단위로 설정해요.
14
+ */
15
+ timeInterval: number;
16
+ /**
17
+ * 위치 변경 거리를 미터(m) 단위로 설정해요.
18
+ */
19
+ distanceInterval: number;
20
+ }
21
+
22
+ export class UpdateLocationEvent extends GraniteEventDefinition<StartUpdateLocationOptions, Location> {
23
+ name = 'updateLocationEvent' as const;
24
+
25
+ subscriptionCount = 0;
26
+
27
+ ref = {
28
+ remove: () => {},
29
+ };
30
+ remove() {
31
+ if (--this.subscriptionCount === 0) {
32
+ AppsInTossModuleInstance.stopUpdateLocation({});
33
+ }
34
+ this.ref.remove();
35
+ }
36
+
37
+ listener(
38
+ options: StartUpdateLocationOptions,
39
+ onEvent: (response: Location) => void,
40
+ onError: (error: unknown) => void
41
+ ): void {
42
+ requestPermission({ name: 'geolocation', access: 'access' })
43
+ .then((permissionStatus) => {
44
+ if (permissionStatus === 'denied') {
45
+ onError(new Error('위치 권한이 거부되었어요.'));
46
+ return;
47
+ }
48
+
49
+ // @internal
50
+ void AppsInTossModuleInstance.startUpdateLocation(options).catch(onError);
51
+ const subscription = nativeEventEmitter.addListener('updateLocation', onEvent);
52
+
53
+ this.ref = {
54
+ remove: () => subscription?.remove(),
55
+ };
56
+ this.subscriptionCount++;
57
+ })
58
+ .catch(onError);
59
+ }
60
+ }
@@ -0,0 +1,5 @@
1
+ export * from './startUpdateLocation';
2
+ export * from './contactsViral';
3
+
4
+ export { appsInTossEvent } from './appsInTossEvent';
5
+ export { onVisibilityChangedByTransparentServiceWeb } from './internal/onVisibilityChangedByTransparentServiceWeb';
@@ -0,0 +1,45 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+ import { GraniteEventDefinition } from '@granite-js/react-native';
3
+ import { EmitterSubscription, NativeEventEmitter } from 'react-native';
4
+ import { INTERNAL__appBridgeHandler } from './appBridge';
5
+ import { nativeEventEmitter } from '../nativeEventEmitter';
6
+
7
+ export interface AppBridgeCallbackResult {
8
+ name: string;
9
+ params?: any;
10
+ }
11
+
12
+ const UNSAFE__nativeEventEmitter = nativeEventEmitter as unknown as NativeEventEmitter;
13
+
14
+ export class AppBridgeCallbackEvent extends GraniteEventDefinition<void, AppBridgeCallbackResult> {
15
+ private static INTERNAL__appBridgeSubscription?: EmitterSubscription;
16
+
17
+ name = 'appBridgeCallbackEvent' as const;
18
+
19
+ constructor() {
20
+ super();
21
+ this.registerAppBridgeCallbackEventListener();
22
+ }
23
+
24
+ remove() {}
25
+ listener() {}
26
+
27
+ private registerAppBridgeCallbackEventListener() {
28
+ if (AppBridgeCallbackEvent.INTERNAL__appBridgeSubscription != null) {
29
+ return;
30
+ }
31
+
32
+ AppBridgeCallbackEvent.INTERNAL__appBridgeSubscription = UNSAFE__nativeEventEmitter.addListener(
33
+ 'appBridgeCallback',
34
+ this.ensureInvokeAppBridgeCallback
35
+ );
36
+ }
37
+
38
+ private ensureInvokeAppBridgeCallback(result: AppBridgeCallbackResult) {
39
+ if (typeof result === 'object' && typeof result.name === 'string') {
40
+ INTERNAL__appBridgeHandler.invokeAppBridgeCallback(result.name, result.params);
41
+ } else {
42
+ console.warn('Invalid app bridge callback result:', result);
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,50 @@
1
+ import { GraniteEventDefinition } from '@granite-js/react-native';
2
+ import type { EmitterSubscription } from 'react-native';
3
+ import { nativeEventEmitter } from '../nativeEventEmitter';
4
+
5
+ export interface VisibilityChangedByTransparentServiceWebOptions {
6
+ callbackId: string;
7
+ }
8
+
9
+ export interface VisibilityChangedByTransparentServiceWebResult {
10
+ callbackId: string;
11
+ isVisible: boolean;
12
+ }
13
+
14
+ export class VisibilityChangedByTransparentServiceWebEvent extends GraniteEventDefinition<
15
+ VisibilityChangedByTransparentServiceWebOptions,
16
+ boolean
17
+ > {
18
+ name = 'onVisibilityChangedByTransparentServiceWeb' as const;
19
+
20
+ subscription: EmitterSubscription | null = null;
21
+
22
+ remove() {
23
+ this.subscription?.remove();
24
+ this.subscription = null;
25
+ }
26
+
27
+ listener(
28
+ options: VisibilityChangedByTransparentServiceWebOptions,
29
+ onEvent: (isVisible: boolean) => void,
30
+ onError: (error: unknown) => void
31
+ ) {
32
+ const subscription = nativeEventEmitter.addListener('visibilityChangedByTransparentServiceWeb', (params) => {
33
+ if (this.isVisibilityChangedByTransparentServiceWebResult(params)) {
34
+ if (params.callbackId === options.callbackId) {
35
+ onEvent(params.isVisible);
36
+ }
37
+ } else {
38
+ onError(new Error('Invalid visibility changed by transparent service web result'));
39
+ }
40
+ });
41
+
42
+ this.subscription = subscription;
43
+ }
44
+
45
+ private isVisibilityChangedByTransparentServiceWebResult(
46
+ params: any
47
+ ): params is VisibilityChangedByTransparentServiceWebResult {
48
+ return typeof params === 'object' && typeof params.callbackId === 'string' && typeof params.isVisible === 'boolean';
49
+ }
50
+ }
@@ -0,0 +1,135 @@
1
+ import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
2
+ import { INTERNAL__appBridgeHandler } from './appBridge.js';
3
+
4
+ const mocks = vi.hoisted(() => {
5
+ const mockedNativeMethodName = 'mockedNativeMethod';
6
+ const mockedReturnValue = new Date().getTime();
7
+ const mockedNativeMethod = vi.fn().mockImplementation(async () => ({ value: mockedReturnValue }));
8
+
9
+ return { mockedNativeMethodName, mockedReturnValue, mockedNativeMethod };
10
+ });
11
+
12
+ vi.mock('react-native', () => {
13
+ return {
14
+ NativeModules: {
15
+ AppsInTossModule: {
16
+ [mocks.mockedNativeMethodName]: mocks.mockedNativeMethod,
17
+ },
18
+ },
19
+ };
20
+ });
21
+
22
+ describe('appBridge', () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ describe('invokeAppBridgeMethod', () => {
28
+ it('should register callbacks', async () => {
29
+ await new Promise((resolve, reject) => {
30
+ INTERNAL__appBridgeHandler.invokeAppBridgeMethod(
31
+ mocks.mockedNativeMethodName,
32
+ {},
33
+ {
34
+ onSuccess: resolve,
35
+ onError: reject,
36
+ onFoo: () => {},
37
+ onBar: () => {},
38
+ onBaz: () => {},
39
+ }
40
+ );
41
+ });
42
+
43
+ // `onSuccess` and `onError` aren't registered as callbacks
44
+ // because they are used as the resolve and reject of the promise.
45
+ //
46
+ // `onFoo`, `onBar`, `onBaz` are registered as callbacks (length = 3)
47
+ expect(INTERNAL__appBridgeHandler.getCallbackIds()).toHaveLength(3);
48
+ });
49
+
50
+ it('should invoke the native method with the correct parameters', async () => {
51
+ const task = new Promise((resolve, reject) => {
52
+ INTERNAL__appBridgeHandler.invokeAppBridgeMethod(
53
+ mocks.mockedNativeMethodName,
54
+ {
55
+ value_1: 1,
56
+ value_2: 2,
57
+ value_3: 3,
58
+ },
59
+ {
60
+ onSuccess: resolve,
61
+ onError: reject,
62
+ onFoo: () => {},
63
+ onBar: () => {},
64
+ onBaz: () => {},
65
+ }
66
+ );
67
+ });
68
+
69
+ await expect(task).resolves.toEqual({ value: mocks.mockedReturnValue });
70
+ expect(mocks.mockedNativeMethod).toHaveBeenCalledTimes(1);
71
+ expect(mocks.mockedNativeMethod).toHaveBeenCalledWith({
72
+ params: {
73
+ value_1: 1,
74
+ value_2: 2,
75
+ value_3: 3,
76
+ },
77
+ callbacks: {
78
+ // Callbacks are replaced with their unique IDs
79
+ onFoo: expect.any(String),
80
+ onBar: expect.any(String),
81
+ onBaz: expect.any(String),
82
+ },
83
+ });
84
+ });
85
+
86
+ it('when the native method is rejected, the error callback is called', async () => {
87
+ mocks.mockedNativeMethod.mockImplementationOnce(() => Promise.reject(new Error('Mocked error')));
88
+
89
+ const task = new Promise((resolve, reject) => {
90
+ INTERNAL__appBridgeHandler.invokeAppBridgeMethod(
91
+ mocks.mockedNativeMethodName,
92
+ {},
93
+ {
94
+ onSuccess: resolve,
95
+ onError: reject,
96
+ }
97
+ );
98
+ });
99
+
100
+ await expect(task).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Mocked error]`);
101
+ });
102
+ });
103
+
104
+ describe('invokeAppBridgeCallback', () => {
105
+ const CALLBACK_NAME = 'onTest';
106
+ let mockedCallback: Mock;
107
+
108
+ // Register a callback before invoking the callback by its ID.
109
+ beforeEach(() => {
110
+ mockedCallback = vi.fn();
111
+
112
+ return new Promise<void>((resolve, reject) => {
113
+ INTERNAL__appBridgeHandler.invokeAppBridgeMethod(
114
+ mocks.mockedNativeMethodName,
115
+ {},
116
+ {
117
+ onSuccess: resolve,
118
+ onError: reject,
119
+ [CALLBACK_NAME]: mockedCallback,
120
+ }
121
+ );
122
+ });
123
+ });
124
+
125
+ it('should invoke the callback with the correct parameters', async () => {
126
+ const callbackId = INTERNAL__appBridgeHandler.getCallbackIds().pop()!;
127
+ const mockedValue = new Date().getTime();
128
+
129
+ INTERNAL__appBridgeHandler.invokeAppBridgeCallback(callbackId, { value: mockedValue });
130
+
131
+ expect(mockedCallback).toHaveBeenCalledTimes(1);
132
+ expect(mockedCallback).toHaveBeenCalledWith({ value: mockedValue });
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,79 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+ import { generateUUID } from '../../../utils/generateUUID';
3
+ import { AppsInTossModuleInstance } from '../../native-modules/AppsInTossModule';
4
+
5
+ export interface AppBridgeCompatCallbacks<Result> {
6
+ onSuccess: (result: Result) => void;
7
+ onError: (reason: unknown) => void;
8
+ }
9
+
10
+ type AppBridgeCallback = (...args: any[]) => void;
11
+ type AppBridgeCallbackId = string;
12
+
13
+ const INTERNAL__callbacks = new Map<AppBridgeCallbackId, AppBridgeCallback>();
14
+
15
+ function invokeAppBridgeCallback(id: string, ...args: any[]): boolean {
16
+ const callback = INTERNAL__callbacks.get(id);
17
+
18
+ callback?.call(null, ...args);
19
+
20
+ return Boolean(callback);
21
+ }
22
+
23
+ function invokeAppBridgeMethod<Result = any, Params = any>(
24
+ methodName: string,
25
+ params: Params,
26
+ callbacks: AppBridgeCompatCallbacks<Result> & Record<string, AppBridgeCallback>
27
+ ) {
28
+ const { onSuccess, onError, ...appBridgeCallbacks } = callbacks;
29
+ const { callbackMap, unregisterAll } = registerCallbacks(appBridgeCallbacks);
30
+
31
+ const promise = AppsInTossModuleInstance[methodName]({
32
+ params,
33
+ callbacks: callbackMap,
34
+ }) as Promise<Result>;
35
+
36
+ void promise.then(onSuccess).catch(onError);
37
+
38
+ return unregisterAll;
39
+ }
40
+
41
+ function registerCallbacks(callbacks: Record<string, AppBridgeCallback>) {
42
+ const callbackMap: Record<string, AppBridgeCallbackId> = {};
43
+
44
+ for (const [callbackName, callback] of Object.entries(callbacks)) {
45
+ const id = registerCallback(callback, callbackName);
46
+ callbackMap[callbackName] = id;
47
+ }
48
+
49
+ const unregisterAll = () => {
50
+ Object.values(callbackMap).forEach(unregisterCallback);
51
+ };
52
+
53
+ return { callbackMap, unregisterAll };
54
+ }
55
+
56
+ function registerCallback(callback: AppBridgeCallback, name = 'unnamed') {
57
+ const uniqueId = generateUUID();
58
+ const callbackId = `${uniqueId}__${name}`;
59
+
60
+ INTERNAL__callbacks.set(callbackId, callback);
61
+
62
+ return callbackId;
63
+ }
64
+
65
+ function unregisterCallback(id: string) {
66
+ INTERNAL__callbacks.delete(id);
67
+ }
68
+
69
+ function getCallbackIds() {
70
+ return Array.from(INTERNAL__callbacks.keys());
71
+ }
72
+
73
+ export const INTERNAL__appBridgeHandler = {
74
+ invokeAppBridgeCallback,
75
+ invokeAppBridgeMethod,
76
+ registerCallback,
77
+ unregisterCallback,
78
+ getCallbackIds,
79
+ };
@@ -0,0 +1,20 @@
1
+ import type { EmitterSubscription } from 'react-native';
2
+ import { appsInTossEvent } from '../appsInTossEvent';
3
+ import type { EventEmitterSchema } from '../types';
4
+
5
+ export interface OnVisibilityChangedByTransparentServiceWebSubscription extends EmitterSubscription {
6
+ remove: () => void;
7
+ }
8
+
9
+ export type OnVisibilityChangedByTransparentServiceWebEventEmitter = EventEmitterSchema<
10
+ 'visibilityChangedByTransparentServiceWeb',
11
+ [boolean]
12
+ >;
13
+
14
+ export function onVisibilityChangedByTransparentServiceWeb(eventParams: {
15
+ options: { callbackId: string };
16
+ onEvent: (isVisible: boolean) => void;
17
+ onError: (error: unknown) => void;
18
+ }): () => void {
19
+ return appsInTossEvent.addEventListener('onVisibilityChangedByTransparentServiceWeb', eventParams);
20
+ }
@@ -0,0 +1,35 @@
1
+ import { NativeEventEmitter, type EmitterSubscription } from 'react-native';
2
+ import type { OnVisibilityChangedByTransparentServiceWebEventEmitter } from './internal/onVisibilityChangedByTransparentServiceWeb';
3
+ import type { UpdateLocationEventEmitter } from './startUpdateLocation';
4
+ import type { EventEmitterSchema } from './types';
5
+ import { AppsInTossModuleInstance } from '../native-modules/AppsInTossModule';
6
+
7
+ type EventEmitters = UpdateLocationEventEmitter | OnVisibilityChangedByTransparentServiceWebEventEmitter;
8
+
9
+ type MapOf<T> = T extends EventEmitterSchema<infer K, any> ? { [key in K]: T } : never;
10
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
11
+ type EventEmittersMap = UnionToIntersection<MapOf<EventEmitters>>;
12
+ type EventKeys = keyof EventEmittersMap;
13
+ type ParamOf<K extends EventKeys> = EventEmittersMap[K]['params'];
14
+ /**
15
+ * @interface AppsInTossEventEmitter
16
+ * @description
17
+ * 네이티브 플랫폼에서 발생하는 이벤트들을 처리하는 NativeEventEmitter를 App In Toss 프레임워크에서 사용하는 형태에 맞게 정의한 인터페이스에요.
18
+ * @property {(event: EventKeys, callback: (...params: ParamOf<E>) => void) => EmitterSubscription} addListener - 이벤트 리스너를 추가하는 함수
19
+ * @property {(subscription: EmitterSubscription) => void} removeSubscription - 이벤트 구독을 제거하는 함수
20
+ */
21
+ interface AppsInTossEventEmitter {
22
+ addListener<Event extends EventKeys>(
23
+ event: Event,
24
+ callback: (...params: ParamOf<Event>) => void
25
+ ): EmitterSubscription;
26
+ }
27
+
28
+ /**
29
+ * @kind constant
30
+ * @name nativeEventEmitter
31
+ * @description
32
+ * App In Toss 프레임워크에서 제공하는 react-native의 NativeEventEmitter instance에요.
33
+ * @type {AppsInTossEventEmitter}
34
+ */
35
+ export const nativeEventEmitter = new NativeEventEmitter(AppsInTossModuleInstance) as unknown as AppsInTossEventEmitter;
@@ -0,0 +1,98 @@
1
+ import type { EmitterSubscription } from 'react-native';
2
+ import { appsInTossEvent } from './appsInTossEvent';
3
+ import type { EventEmitterSchema } from './types';
4
+ import type { Accuracy, Location } from '../../types';
5
+
6
+ export interface StartUpdateLocationOptions {
7
+ /**
8
+ * 위치 정확도를 설정해요.
9
+ */
10
+ accuracy: Accuracy;
11
+ /**
12
+ * 위치 업데이트 주기를 밀리초(ms) 단위로 설정해요.
13
+ */
14
+ timeInterval: number;
15
+ /**
16
+ * 위치 변경 거리를 미터(m) 단위로 설정해요.
17
+ */
18
+ distanceInterval: number;
19
+ }
20
+
21
+ export interface StartUpdateLocationSubscription extends EmitterSubscription {
22
+ remove: () => Promise<void>;
23
+ }
24
+
25
+ /**
26
+ * @name UpdateLocationEventEmitter
27
+ * @kind typedef
28
+ * @description
29
+ * 디바이스의 위치 정보 변경을 감지해요
30
+ */
31
+ export type UpdateLocationEventEmitter = EventEmitterSchema<'updateLocation', [Location]>;
32
+
33
+ /**
34
+ * @public
35
+ * @category 위치 정보
36
+ * @name startUpdateLocation
37
+ * @description 디바이스의 위치 정보를 지속적으로 감지하고, 위치가 변경되면 콜백을 실행하는 함수예요. 콜백 함수를 등록하면 위치가 변경될 때마다 자동으로 호출돼요.
38
+ * 실시간 위치 추적이 필요한 기능을 구현할 때 사용할 수 있어요. 예를 들어 지도 앱에서 사용자의 현재 위치를 실시간으로 업데이트할 때, 운동 앱에서 사용자의 이동 거리를 기록할 때 등이에요.
39
+ * 위치 업데이트 주기와 정확도를 조정해 배터리 소모를 최소화하면서도 필요한 정보를 얻을 수 있어요.
40
+ *
41
+ * @param {StartUpdateLocationOptions} options - 위치 정보 감지에 필요한 설정 객체에요.
42
+ * @param {number} [options.accuracy] 위치 정확도를 설정해요.
43
+ * @param {number} [options.timeInterval] 위치 정보를 업데이트하는 최소 주기로, 단위는 밀리초(ms)예요. 이 값은 위치 업데이트가 발생하는 가장 짧은 간격을 설정하지만, 시스템이나 환경의 영향을 받아 지정한 주기보다 더 긴 간격으로 업데이트될 수 있어요.
44
+ * @param {number} [options.distanceInterval] 위치 변경 거리를 미터(m) 단위로 설정해요.
45
+ * @param {(location: Location) => void} [options.callback] 위치 정보가 변경될 때 호출되는 콜백 함수예요. 자세한 내용은 [Location](/react-native/reference/native-modules/Types/Location.html)을 참고해주세요.
46
+ *
47
+ * @example
48
+ * ### 위치 정보 변경 감지하기
49
+ *
50
+ * ```tsx
51
+ * import React, { useState, useEffect } from 'react';
52
+ * import { View, Text, Button } from 'react-native';
53
+ * import { startUpdateLocation } from '@apps-in-toss/framework';
54
+ *
55
+ * // 위치 정보 변경 감지하기
56
+ * function LocationWatcher() {
57
+ * const [location, setLocation] = useState(null);
58
+ *
59
+ * useEffect(() => {
60
+ * return startUpdateLocation({
61
+ * options: {
62
+ * accuracy: Accuracy.Balanced,
63
+ * timeInterval: 3000,
64
+ * distanceInterval: 10,
65
+ * },
66
+ * onEvent: (location) => {
67
+ * setLocation(location);
68
+ * },
69
+ * onError: (error) => {
70
+ * console.error('위치 정보를 가져오는데 실패했어요:', error);
71
+ * },
72
+ * });
73
+ * }, []);
74
+ *
75
+ * if (location == null) {
76
+ * return <Text>위치 정보를 가져오는 중이에요...</Text>;
77
+ * }
78
+ *
79
+ * return (
80
+ * <View>
81
+ * <Text>위도: {location.coords.latitude}</Text>
82
+ * <Text>경도: {location.coords.longitude}</Text>
83
+ * <Text>위치 정확도: {location.coords.accuracy}m</Text>
84
+ * <Text>높이: {location.coords.altitude}m</Text>
85
+ * <Text>고도 정확도: {location.coords.altitudeAccuracy}m</Text>
86
+ * <Text>방향: {location.coords.heading}°</Text>
87
+ * </View>
88
+ * );
89
+ * }
90
+ * ```
91
+ */
92
+ export function startUpdateLocation(eventParams: {
93
+ onEvent: (response: Location) => void;
94
+ onError: (error: unknown) => void;
95
+ options: StartUpdateLocationOptions;
96
+ }): () => void {
97
+ return appsInTossEvent.addEventListener('updateLocationEvent', eventParams);
98
+ }