@apps-in-toss/native-modules 0.0.0-dev.1752115036458 → 0.0.0-dev.1757056983098

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 (77) hide show
  1. package/dist/bridges-meta.json +36 -13
  2. package/dist/index.cjs +343 -129
  3. package/dist/index.d.cts +1212 -418
  4. package/dist/index.d.ts +1212 -418
  5. package/dist/index.js +315 -108
  6. package/package.json +7 -8
  7. package/src/AppsInTossModule/constants.ts +6 -0
  8. package/src/AppsInTossModule/native-event-emitter/appsInTossEvent.ts +15 -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/HomeIconButtonClickHandleEvent.ts +10 -0
  12. package/src/AppsInTossModule/native-event-emitter/event-plugins/UpdateLocationEvent.ts +60 -0
  13. package/src/AppsInTossModule/native-event-emitter/index.ts +6 -0
  14. package/src/AppsInTossModule/native-event-emitter/internal/AppBridgeCallbackEvent.ts +45 -0
  15. package/src/AppsInTossModule/native-event-emitter/internal/VisibilityChangedByTransparentServiceWebEvent.ts +50 -0
  16. package/src/AppsInTossModule/native-event-emitter/internal/appBridge.spec.ts +135 -0
  17. package/src/AppsInTossModule/native-event-emitter/internal/appBridge.ts +79 -0
  18. package/src/AppsInTossModule/native-event-emitter/internal/onVisibilityChangedByTransparentServiceWeb.ts +20 -0
  19. package/src/AppsInTossModule/native-event-emitter/nativeEventEmitter.ts +35 -0
  20. package/src/AppsInTossModule/native-event-emitter/startUpdateLocation.ts +98 -0
  21. package/src/AppsInTossModule/native-event-emitter/types.ts +4 -0
  22. package/src/AppsInTossModule/native-modules/AppsInTossModule.ts +89 -0
  23. package/src/AppsInTossModule/native-modules/ads/googleAdMob.ts +681 -0
  24. package/src/AppsInTossModule/native-modules/ads/googleAdMobV2.ts +363 -0
  25. package/src/AppsInTossModule/native-modules/ads/types.ts +123 -0
  26. package/src/AppsInTossModule/native-modules/appLogin.ts +29 -0
  27. package/src/AppsInTossModule/native-modules/checkoutPayment.ts +80 -0
  28. package/src/AppsInTossModule/native-modules/eventLog.spec.ts +300 -0
  29. package/src/AppsInTossModule/native-modules/eventLog.ts +77 -0
  30. package/src/AppsInTossModule/native-modules/fetchAlbumPhotos.ts +88 -0
  31. package/src/AppsInTossModule/native-modules/fetchContacts.ts +121 -0
  32. package/src/AppsInTossModule/native-modules/getClipboardText.ts +47 -0
  33. package/src/AppsInTossModule/native-modules/getCurrentLocation.ts +65 -0
  34. package/src/AppsInTossModule/native-modules/getDeviceId.ts +33 -0
  35. package/src/AppsInTossModule/native-modules/getGameCenterGameProfile.ts +68 -0
  36. package/src/AppsInTossModule/native-modules/getOperationalEnvironment.ts +37 -0
  37. package/src/AppsInTossModule/native-modules/getPermission.ts +58 -0
  38. package/src/AppsInTossModule/native-modules/getTossAppVersion.ts +33 -0
  39. package/src/AppsInTossModule/native-modules/getTossShareLink.ts +39 -0
  40. package/src/AppsInTossModule/native-modules/iap.ts +213 -0
  41. package/src/AppsInTossModule/native-modules/index.ts +104 -0
  42. package/src/AppsInTossModule/native-modules/isMinVersionSupported.spec.ts +190 -0
  43. package/src/AppsInTossModule/native-modules/isMinVersionSupported.ts +68 -0
  44. package/src/AppsInTossModule/native-modules/openCamera.ts +81 -0
  45. package/src/AppsInTossModule/native-modules/openGameCenterLeaderboard.ts +44 -0
  46. package/src/AppsInTossModule/native-modules/openPermissionDialog.ts +54 -0
  47. package/src/AppsInTossModule/native-modules/requestPermission.ts +63 -0
  48. package/src/AppsInTossModule/native-modules/saveBase64Data.ts +57 -0
  49. package/src/AppsInTossModule/native-modules/setClipboardText.ts +39 -0
  50. package/src/AppsInTossModule/native-modules/setDeviceOrientation.ts +74 -0
  51. package/src/AppsInTossModule/native-modules/storage.ts +100 -0
  52. package/src/AppsInTossModule/native-modules/submitGameCenterLeaderBoardScore.ts +74 -0
  53. package/src/AppsInTossModule/native-modules/tossCore.ts +29 -0
  54. package/src/BedrockModule/native-modules/core/BedrockCoreModule.ts +8 -0
  55. package/src/BedrockModule/native-modules/index.ts +4 -0
  56. package/src/BedrockModule/native-modules/natives/BedrockModule.ts +20 -0
  57. package/src/BedrockModule/native-modules/natives/closeView.ts +25 -0
  58. package/src/BedrockModule/native-modules/natives/generateHapticFeedback/index.ts +27 -0
  59. package/src/BedrockModule/native-modules/natives/generateHapticFeedback/types.ts +38 -0
  60. package/src/BedrockModule/native-modules/natives/getLocale.ts +46 -0
  61. package/src/BedrockModule/native-modules/natives/getNetworkStatus/index.ts +59 -0
  62. package/src/BedrockModule/native-modules/natives/getNetworkStatus/types.ts +1 -0
  63. package/src/BedrockModule/native-modules/natives/getPlatformOS.ts +37 -0
  64. package/src/BedrockModule/native-modules/natives/getSchemeUri.ts +27 -0
  65. package/src/BedrockModule/native-modules/natives/index.ts +11 -0
  66. package/src/BedrockModule/native-modules/natives/openURL.ts +40 -0
  67. package/src/BedrockModule/native-modules/natives/setIosSwipeGestureEnabled.ts +43 -0
  68. package/src/BedrockModule/native-modules/natives/setScreenAwakeMode.ts +66 -0
  69. package/src/BedrockModule/native-modules/natives/setSecureScreen.ts +31 -0
  70. package/src/BedrockModule/native-modules/natives/share.ts +36 -0
  71. package/src/async-bridges.ts +3 -0
  72. package/src/event-bridges.ts +2 -0
  73. package/src/index.ts +16 -0
  74. package/src/types.ts +108 -0
  75. package/src/utils/compareVersion.spec.ts +176 -0
  76. package/src/utils/compareVersion.ts +104 -0
  77. 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,10 @@
1
+ import { GraniteEventDefinition } from '@granite-js/react-native';
2
+
3
+ export class HomeIconButtonClickHandleEvent extends GraniteEventDefinition<undefined, undefined> {
4
+ name = 'homeIconButtonClickEvent' 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,6 @@
1
+ export * from './startUpdateLocation';
2
+ export * from './contactsViral';
3
+
4
+ export { appsInTossEvent } from './appsInTossEvent';
5
+ export { onVisibilityChangedByTransparentServiceWeb } from './internal/onVisibilityChangedByTransparentServiceWeb';
6
+ export { INTERNAL__appBridgeHandler } from './internal/appBridge';
@@ -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;