@hayanmind/monetai-react-native 0.2.4

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/LICENSE +20 -0
  2. package/MonetaiReactNative.podspec +25 -0
  3. package/README.md +31 -0
  4. package/android/build.gradle +81 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/AndroidManifestNew.xml +2 -0
  8. package/android/src/main/java/com/hayanmind/monetaireactnative/MonetaiReactNativeModule.kt +110 -0
  9. package/android/src/main/java/com/hayanmind/monetaireactnative/MonetaiReactNativePackage.kt +17 -0
  10. package/ios/MonetaiReactNative-Bridging-Header.h +2 -0
  11. package/ios/MonetaiReactNative.mm +38 -0
  12. package/ios/MonetaiReactNative.swift +336 -0
  13. package/lib/commonjs/ApiRequest.js +120 -0
  14. package/lib/commonjs/ApiRequest.js.map +1 -0
  15. package/lib/commonjs/MonetaiProvider.js +67 -0
  16. package/lib/commonjs/MonetaiProvider.js.map +1 -0
  17. package/lib/commonjs/MonetaiSDK.js +198 -0
  18. package/lib/commonjs/MonetaiSDK.js.map +1 -0
  19. package/lib/commonjs/index.js +17 -0
  20. package/lib/commonjs/index.js.map +1 -0
  21. package/lib/commonjs/lib.js +24 -0
  22. package/lib/commonjs/lib.js.map +1 -0
  23. package/lib/commonjs/package.json +1 -0
  24. package/lib/commonjs/types/global.d.js +2 -0
  25. package/lib/commonjs/types/global.d.js.map +1 -0
  26. package/lib/commonjs/types.js +2 -0
  27. package/lib/commonjs/types.js.map +1 -0
  28. package/lib/module/ApiRequest.js +110 -0
  29. package/lib/module/ApiRequest.js.map +1 -0
  30. package/lib/module/MonetaiProvider.js +61 -0
  31. package/lib/module/MonetaiProvider.js.map +1 -0
  32. package/lib/module/MonetaiSDK.js +192 -0
  33. package/lib/module/MonetaiSDK.js.map +1 -0
  34. package/lib/module/index.js +7 -0
  35. package/lib/module/index.js.map +1 -0
  36. package/lib/module/lib.js +20 -0
  37. package/lib/module/lib.js.map +1 -0
  38. package/lib/module/package.json +1 -0
  39. package/lib/module/types/global.d.js +2 -0
  40. package/lib/module/types/global.d.js.map +1 -0
  41. package/lib/module/types.js +2 -0
  42. package/lib/module/types.js.map +1 -0
  43. package/lib/typescript/commonjs/package.json +1 -0
  44. package/lib/typescript/commonjs/src/ApiRequest.d.ts +52 -0
  45. package/lib/typescript/commonjs/src/ApiRequest.d.ts.map +1 -0
  46. package/lib/typescript/commonjs/src/MonetaiProvider.d.ts +18 -0
  47. package/lib/typescript/commonjs/src/MonetaiProvider.d.ts.map +1 -0
  48. package/lib/typescript/commonjs/src/MonetaiSDK.d.ts +45 -0
  49. package/lib/typescript/commonjs/src/MonetaiSDK.d.ts.map +1 -0
  50. package/lib/typescript/commonjs/src/index.d.ts +5 -0
  51. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/src/lib.d.ts +2 -0
  53. package/lib/typescript/commonjs/src/lib.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/src/types.d.ts +17 -0
  55. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  56. package/lib/typescript/module/package.json +1 -0
  57. package/lib/typescript/module/src/ApiRequest.d.ts +52 -0
  58. package/lib/typescript/module/src/ApiRequest.d.ts.map +1 -0
  59. package/lib/typescript/module/src/MonetaiProvider.d.ts +18 -0
  60. package/lib/typescript/module/src/MonetaiProvider.d.ts.map +1 -0
  61. package/lib/typescript/module/src/MonetaiSDK.d.ts +45 -0
  62. package/lib/typescript/module/src/MonetaiSDK.d.ts.map +1 -0
  63. package/lib/typescript/module/src/index.d.ts +5 -0
  64. package/lib/typescript/module/src/index.d.ts.map +1 -0
  65. package/lib/typescript/module/src/lib.d.ts +2 -0
  66. package/lib/typescript/module/src/lib.d.ts.map +1 -0
  67. package/lib/typescript/module/src/types.d.ts +17 -0
  68. package/lib/typescript/module/src/types.d.ts.map +1 -0
  69. package/package.json +176 -0
  70. package/src/ApiRequest.ts +182 -0
  71. package/src/MonetaiProvider.tsx +79 -0
  72. package/src/MonetaiSDK.ts +225 -0
  73. package/src/index.tsx +5 -0
  74. package/src/lib.ts +17 -0
  75. package/src/types/global.d.ts +2 -0
  76. package/src/types.ts +17 -0
package/package.json ADDED
@@ -0,0 +1,176 @@
1
+ {
2
+ "name": "@hayanmind/monetai-react-native",
3
+ "version": "0.2.4",
4
+ "description": ".",
5
+ "source": "./src/index.tsx",
6
+ "main": "./lib/commonjs/index.js",
7
+ "module": "./lib/module/index.js",
8
+ "types": "./lib/typescript/commonjs/src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./lib/typescript/module/src/index.d.ts",
13
+ "default": "./lib/module/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./lib/typescript/commonjs/src/index.d.ts",
17
+ "default": "./lib/commonjs/index.js"
18
+ }
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "src",
24
+ "lib",
25
+ "android",
26
+ "ios",
27
+ "cpp",
28
+ "*.podspec",
29
+ "react-native.config.js",
30
+ "!ios/build",
31
+ "!android/build",
32
+ "!android/gradle",
33
+ "!android/gradlew",
34
+ "!android/gradlew.bat",
35
+ "!android/local.properties",
36
+ "!**/__tests__",
37
+ "!**/__fixtures__",
38
+ "!**/__mocks__",
39
+ "!**/.*"
40
+ ],
41
+ "scripts": {
42
+ "example": "yarn workspace @hayanmind/monetai-react-native-example",
43
+ "test": "jest",
44
+ "typecheck": "tsc",
45
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
46
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
47
+ "prepare": "bob build",
48
+ "release": "release-it"
49
+ },
50
+ "keywords": [
51
+ "react-native",
52
+ "ios",
53
+ "android"
54
+ ],
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://monetai.io.git"
58
+ },
59
+ "author": "Daehoon Kim <dhkim@hayanmind.com> (https://monetai.io)",
60
+ "license": "MIT",
61
+ "bugs": {
62
+ "url": "https://monetai.io/issues"
63
+ },
64
+ "homepage": "https://monetai.io#readme",
65
+ "publishConfig": {
66
+ "registry": "https://registry.npmjs.org/",
67
+ "access": "public"
68
+ },
69
+ "devDependencies": {
70
+ "@commitlint/config-conventional": "^19.6.0",
71
+ "@eslint/compat": "^1.2.7",
72
+ "@eslint/eslintrc": "^3.3.0",
73
+ "@eslint/js": "^9.22.0",
74
+ "@evilmartians/lefthook": "^1.5.0",
75
+ "@react-native/eslint-config": "^0.78.0",
76
+ "@release-it/conventional-changelog": "^9.0.2",
77
+ "@types/jest": "^29.5.5",
78
+ "@types/react": "^19.0.0",
79
+ "babel-plugin-transform-define": "^2.1.4",
80
+ "commitlint": "^19.6.1",
81
+ "del-cli": "^5.1.0",
82
+ "eslint": "^9.22.0",
83
+ "eslint-config-prettier": "^10.1.1",
84
+ "eslint-plugin-prettier": "^5.2.3",
85
+ "jest": "^29.7.0",
86
+ "prettier": "^3.0.3",
87
+ "react": "19.0.0",
88
+ "react-native": "0.78.2",
89
+ "react-native-builder-bob": "^0.40.10",
90
+ "release-it": "^17.10.0",
91
+ "turbo": "^1.10.7",
92
+ "typescript": "^5.2.2"
93
+ },
94
+ "peerDependencies": {
95
+ "react": "*",
96
+ "react-native": "*"
97
+ },
98
+ "workspaces": [
99
+ "example"
100
+ ],
101
+ "packageManager": "yarn@3.6.1",
102
+ "jest": {
103
+ "preset": "react-native",
104
+ "modulePathIgnorePatterns": [
105
+ "<rootDir>/example/node_modules",
106
+ "<rootDir>/lib/"
107
+ ]
108
+ },
109
+ "commitlint": {
110
+ "extends": [
111
+ "@commitlint/config-conventional"
112
+ ]
113
+ },
114
+ "release-it": {
115
+ "git": {
116
+ "commitMessage": "chore: release ${version}",
117
+ "tagName": "v${version}"
118
+ },
119
+ "npm": {
120
+ "publish": true
121
+ },
122
+ "github": {
123
+ "release": true
124
+ },
125
+ "plugins": {
126
+ "@release-it/conventional-changelog": {
127
+ "preset": {
128
+ "name": "angular"
129
+ }
130
+ }
131
+ }
132
+ },
133
+ "prettier": {
134
+ "quoteProps": "consistent",
135
+ "singleQuote": true,
136
+ "tabWidth": 2,
137
+ "trailingComma": "es5",
138
+ "useTabs": false
139
+ },
140
+ "react-native-builder-bob": {
141
+ "source": "src",
142
+ "output": "lib",
143
+ "targets": [
144
+ [
145
+ "commonjs",
146
+ {
147
+ "esm": true,
148
+ "configFile": true
149
+ }
150
+ ],
151
+ [
152
+ "module",
153
+ {
154
+ "esm": true,
155
+ "configFile": true
156
+ }
157
+ ],
158
+ [
159
+ "typescript",
160
+ {
161
+ "project": "tsconfig.build.json"
162
+ }
163
+ ]
164
+ ]
165
+ },
166
+ "create-react-native-library": {
167
+ "type": "legacy-module",
168
+ "languages": "kotlin-swift",
169
+ "version": "0.49.8"
170
+ },
171
+ "dependencies": {
172
+ "axios": "^1.9.0",
173
+ "dayjs": "^1.11.13",
174
+ "mitt": "^3.0.1"
175
+ }
176
+ }
@@ -0,0 +1,182 @@
1
+ import axios from 'axios';
2
+ import type {
3
+ ABTestGroup,
4
+ AppUserDiscountResponse,
5
+ PredictResult,
6
+ } from './types';
7
+ import { getSDKVersion } from './lib';
8
+ import { Platform } from 'react-native';
9
+
10
+ const axiosInstance = axios.create({
11
+ baseURL: 'https://monetai-api-414410537412.us-central1.run.app/sdk', // 필요에 따라 베이스 URL 설정
12
+ });
13
+
14
+ export const initialize = async ({
15
+ sdkKey,
16
+ userId,
17
+ }: {
18
+ sdkKey: string;
19
+ userId: string;
20
+ }) => {
21
+ try {
22
+ const response = await axiosInstance.post<{
23
+ organization_id: number;
24
+ platform: 'react_native';
25
+ version: string;
26
+ }>('/sdk-integrations', {
27
+ sdkKey,
28
+ platform: 'react_native',
29
+ version: getSDKVersion(),
30
+ });
31
+
32
+ const abTestGroupResponse = await axiosInstance.post<{
33
+ group: ABTestGroup | null;
34
+ campaign: {
35
+ id: number;
36
+ created_at: Date | null;
37
+ organization_id: number;
38
+ campaign_name: string;
39
+ started_at: Date | null;
40
+ ended_at: Date | null;
41
+ traffic_ratio: number;
42
+ allocation_ratio: number;
43
+ discount_ratio: number;
44
+ exposure_time_sec: number;
45
+ model_accuracy: number | null;
46
+ } | null;
47
+ }>('/ab-test', {
48
+ sdkKey,
49
+ userId,
50
+ platform:
51
+ Platform.OS === 'ios'
52
+ ? 'ios'
53
+ : Platform.OS === 'android'
54
+ ? 'android'
55
+ : null,
56
+ });
57
+
58
+ return {
59
+ organizationId: response.data.organization_id,
60
+ platform: response.data.platform,
61
+ version: response.data.version,
62
+ group: abTestGroupResponse.data.group,
63
+ campaign: abTestGroupResponse.data.campaign,
64
+ };
65
+ } catch (error: any) {
66
+ console.error('Failed to initialize integration:', error.message);
67
+ throw new Error(
68
+ error.response?.data?.message || 'Integration initialization failed'
69
+ );
70
+ }
71
+ };
72
+
73
+ export const createEvent = async ({
74
+ sdkKey,
75
+ userId,
76
+ eventName,
77
+ createdAt,
78
+ value,
79
+ }: {
80
+ sdkKey: string;
81
+ userId: string;
82
+ eventName: string;
83
+ createdAt?: Date;
84
+ value?: number;
85
+ }) => {
86
+ try {
87
+ await axiosInstance.post('/events', {
88
+ sdkKey,
89
+ userId,
90
+ eventName,
91
+ createdAt: createdAt ?? new Date(),
92
+ value,
93
+ platform:
94
+ Platform.OS === 'ios'
95
+ ? 'ios'
96
+ : Platform.OS === 'android'
97
+ ? 'android'
98
+ : null,
99
+ });
100
+ } catch (error: any) {
101
+ console.error('Failed to log event:', error.message);
102
+ throw new Error(error.response?.data?.message || 'Failed to log event');
103
+ }
104
+ };
105
+
106
+ export const predict = async ({
107
+ userId,
108
+ sdkKey,
109
+ }: {
110
+ userId: string;
111
+ sdkKey: string;
112
+ }) => {
113
+ console.log('Predicting user:', userId);
114
+
115
+ try {
116
+ const response = await axiosInstance.post<{
117
+ prediction: PredictResult;
118
+ testGroup: ABTestGroup;
119
+ }>('/predict', {
120
+ sdkKey,
121
+ userId,
122
+ });
123
+
124
+ return response.data;
125
+ } catch (error: any) {
126
+ console.error('Failed to get A/B test group:', error.message);
127
+ throw new Error(
128
+ error.response?.data?.message || 'Failed to get A/B test group'
129
+ );
130
+ }
131
+ };
132
+
133
+ export const createAppUserDiscount = async (discountInfo: {
134
+ startedAt: Date;
135
+ endedAt: Date;
136
+ userId: string;
137
+ sdkKey: string;
138
+ }) => {
139
+ try {
140
+ const response = await axiosInstance.post<{
141
+ discount: AppUserDiscountResponse;
142
+ }>('/app-user-discounts', {
143
+ sdkKey: discountInfo.sdkKey,
144
+ appUserId: discountInfo.userId,
145
+ startedAt: discountInfo.startedAt,
146
+ endedAt: discountInfo.endedAt,
147
+ });
148
+ console.log('response', response);
149
+ return response.data.discount;
150
+ } catch (error: any) {
151
+ console.error('Failed to set discount info:', error.message);
152
+ throw new Error(
153
+ error.response?.data?.message || 'Failed to set discount info'
154
+ );
155
+ }
156
+ };
157
+
158
+ /**
159
+ * sdkKey와 app_user_id에 해당하는 가장 최근 할인 정보를 조회합니다.
160
+ * API GET /app-user-discounts/latest 엔드포인트를 호출합니다.
161
+ */
162
+ export const getAppUserDiscount = async ({
163
+ sdkKey,
164
+ userId,
165
+ }: {
166
+ sdkKey: string;
167
+ userId: string;
168
+ }) => {
169
+ try {
170
+ const response = await axiosInstance.get<{
171
+ discount: AppUserDiscountResponse | null;
172
+ }>('/app-user-discounts/latest', {
173
+ params: { sdkKey, appUserId: userId },
174
+ });
175
+ return response.data.discount;
176
+ } catch (error: any) {
177
+ console.error('Failed to get discount info:', error.message);
178
+ throw new Error(
179
+ error.response?.data?.message || 'Failed to get discount info'
180
+ );
181
+ }
182
+ };
@@ -0,0 +1,79 @@
1
+ // MonetaiProvider.tsx
2
+ import { useEffect, useState, type ReactNode } from 'react';
3
+ import { StyleSheet, View } from 'react-native';
4
+ import monetaiSDK from './MonetaiSDK';
5
+ import type { DiscountInfo } from './types';
6
+ import { getAppUserDiscount } from './ApiRequest';
7
+
8
+ const styles = StyleSheet.create({
9
+ container: { flex: 1 },
10
+ });
11
+
12
+ type MonetaiProviderProps = {
13
+ banner?: {
14
+ title: string;
15
+ discountTitle: string;
16
+ onPress: () => void;
17
+ };
18
+ children: ReactNode;
19
+ onDiscountInfoChange?: (discountInfo: DiscountInfo | null) => void;
20
+ };
21
+
22
+ /**
23
+ * 클라이언트 측에서 이 컴포넌트로 전체 앱을 감싸주면,
24
+ * SDK 초기화 + Event Emitter 구독 + 배너 렌더링을 한곳에서 처리할 수 있음
25
+ */
26
+ export default function MonetaiProvider({
27
+ children,
28
+ onDiscountInfoChange,
29
+ }: MonetaiProviderProps) {
30
+ const [discountInfo, setDiscountInfo] = useState<DiscountInfo | null>(null);
31
+
32
+ useEffect(() => {
33
+ const emitter = monetaiSDK.getEmitter();
34
+ const onLoadDiscountInfo = async () => {
35
+ const userId = await monetaiSDK.getUserId();
36
+ const sdkKey = await monetaiSDK.getSdkKey();
37
+
38
+ if (userId == null || sdkKey == null) {
39
+ console.warn('onLoadDiscountInfo: User ID or SDK Key is not set.');
40
+ return;
41
+ }
42
+
43
+ const discount = await getAppUserDiscount({ userId, sdkKey });
44
+ if (discount != null && discount.app_user_id !== userId) {
45
+ return;
46
+ }
47
+
48
+ setDiscountInfo(
49
+ discount == null
50
+ ? null
51
+ : {
52
+ startedAt: discount.started_at,
53
+ endedAt: discount.ended_at,
54
+ userId: discount.app_user_id,
55
+ sdkKey: discount.sdk_key,
56
+ }
57
+ );
58
+ };
59
+
60
+ emitter.on('LOAD_DISCOUNT_INFO', onLoadDiscountInfo);
61
+
62
+ return () => {
63
+ emitter.off('LOAD_DISCOUNT_INFO', onLoadDiscountInfo);
64
+ };
65
+ }, []);
66
+
67
+ useEffect(() => {
68
+ if (onDiscountInfoChange) {
69
+ onDiscountInfoChange(discountInfo);
70
+ }
71
+ }, [onDiscountInfoChange, discountInfo]);
72
+
73
+ return (
74
+ <View style={styles.container}>
75
+ {/** 자식(실제 앱 화면) 렌더 */}
76
+ {children}
77
+ </View>
78
+ );
79
+ }
@@ -0,0 +1,225 @@
1
+ import { NativeModules, Platform } from 'react-native';
2
+ import mitt from 'mitt';
3
+ import dayjs from 'dayjs';
4
+ import * as ApiRequest from './ApiRequest';
5
+ import { type ABTestGroup, type PredictResult } from './types';
6
+ import { createAppUserDiscount } from './ApiRequest';
7
+
8
+ const LINKING_ERROR =
9
+ `The package '@hayanmind/monetai-react-native' doesn't seem to be linked. Make sure: \n\n` +
10
+ Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
11
+ '- You rebuilt the app after installing the package\n' +
12
+ '- You are not using Expo Go\n';
13
+
14
+ const MonetaiReactNative = NativeModules.MonetaiReactNative
15
+ ? NativeModules.MonetaiReactNative
16
+ : new Proxy(
17
+ {},
18
+ {
19
+ get() {
20
+ throw new Error(LINKING_ERROR);
21
+ },
22
+ }
23
+ );
24
+
25
+ // Emitter로 사용할 이벤트 타입 정의
26
+ type MonetaiEvents = {
27
+ LOAD_DISCOUNT_INFO: void; // SDK 초기화 완료
28
+ };
29
+
30
+ class MonetaiSDK {
31
+ private exposureTimeSec: number | null = null;
32
+ private initialized = false;
33
+
34
+ // SDK 초기화 전에 로깅된 이벤트들을 저장할 큐
35
+ private pendingEvents: Array<{
36
+ eventName: string;
37
+ createdAt: Date;
38
+ value?: number;
39
+ }> = [];
40
+
41
+ // Event Emitter
42
+ private emitter = mitt<MonetaiEvents>();
43
+
44
+ // emitter를 노출해서 Provider가 구독할 수 있게 함
45
+ public getEmitter() {
46
+ return this.emitter;
47
+ }
48
+
49
+ public getExposureTimeSec() {
50
+ return this.exposureTimeSec;
51
+ }
52
+
53
+ /**
54
+ * 모넷AI SDK를 초기화합니다
55
+ * @param userId 사용자 고유 ID (필수)
56
+ * @param useStoreKit2 StoreKit2를 사용할지 여부 (기본값: false)
57
+ * - true: iOS 15+ 기기에서 StoreKit2 사용 (권장)
58
+ * - false: 모든 기기에서 StoreKit1 사용
59
+ * @returns 성공 시 Promise<void>
60
+ */
61
+ public async initialize({
62
+ sdkKey,
63
+ userId,
64
+ useStoreKit2 = false,
65
+ }: {
66
+ sdkKey: string;
67
+ userId: string;
68
+ useStoreKit2?: boolean;
69
+ }): Promise<{
70
+ organizationId: number;
71
+ platform: 'react_native';
72
+ version: string;
73
+ userId: string;
74
+ group: ABTestGroup | null;
75
+ }> {
76
+ if (!userId) {
77
+ throw new Error('userId is required');
78
+ }
79
+ if (!sdkKey) {
80
+ throw new Error('sdkKey is required');
81
+ }
82
+
83
+ // SDK 키를 네이티브에 저장
84
+ await MonetaiReactNative.setSdkKey(sdkKey);
85
+
86
+ // userId 설정
87
+ await MonetaiReactNative.setUserId(userId);
88
+
89
+ // 트랜잭션 관찰 시작
90
+ await MonetaiReactNative.startObserving(useStoreKit2);
91
+
92
+ // iOS 영수증 전송 (ios 플랫폼에서만 호출)
93
+ if (Platform.OS === 'ios') {
94
+ MonetaiReactNative.sendReceipt().catch((err: any) => {
95
+ console.error('Failed to send receipt', err);
96
+ });
97
+ }
98
+
99
+ const result = await ApiRequest.initialize({ sdkKey, userId });
100
+ this.exposureTimeSec = result.campaign?.exposure_time_sec ?? null;
101
+ this.initialized = true;
102
+
103
+ // SDK 초기화 후, 초기화 전에 저장된 pendingEvents를 전송
104
+ const pendingPromises = this.pendingEvents.map(
105
+ async ({ eventName, value, createdAt }) => {
106
+ await ApiRequest.createEvent({
107
+ sdkKey,
108
+ userId,
109
+ eventName,
110
+ value,
111
+ createdAt,
112
+ });
113
+ console.log('[MonetaiSDK] Logged pending event', eventName, value);
114
+ }
115
+ );
116
+
117
+ Promise.all(pendingPromises).then(() => {
118
+ this.pendingEvents = [];
119
+ });
120
+
121
+ this.emitter.emit('LOAD_DISCOUNT_INFO');
122
+
123
+ return {
124
+ organizationId: result.organizationId,
125
+ platform: 'react_native',
126
+ version: result.version,
127
+ userId: userId,
128
+ group: result.group,
129
+ };
130
+ }
131
+
132
+ public async logEvent({ eventName }: { eventName: string }) {
133
+ const sdkKey = await this.getSdkKey();
134
+ const userId = await this.getUserId();
135
+
136
+ // SDK가 아직 초기화되지 않은 경우, 이벤트를 pendingEvents에 저장
137
+ if (!sdkKey || !userId) {
138
+ console.log('[MonetaiSDK] Pending event', eventName);
139
+ this.pendingEvents.push({ eventName, createdAt: new Date() });
140
+ return;
141
+ }
142
+ await ApiRequest.createEvent({
143
+ sdkKey,
144
+ userId,
145
+ eventName,
146
+ });
147
+ console.log('[MonetaiSDK] Logged event', eventName);
148
+ }
149
+
150
+ // Non-Purchaser 예측
151
+ public async predict(): Promise<{
152
+ prediction: PredictResult;
153
+ testGroup: ABTestGroup;
154
+ }> {
155
+ const sdkKey = await this.getSdkKey();
156
+ const userId = await this.getUserId();
157
+
158
+ if (!sdkKey || !userId || !this.exposureTimeSec) {
159
+ throw new Error(
160
+ 'MonetaiSDK is not initialized. Call initialize() first.'
161
+ );
162
+ }
163
+ const result = await ApiRequest.predict({ userId, sdkKey });
164
+
165
+ if (result.prediction === 'non-purchaser') {
166
+ const discountInfo = await ApiRequest.getAppUserDiscount({
167
+ sdkKey,
168
+ userId,
169
+ });
170
+ const hasActiveDiscount =
171
+ discountInfo != null &&
172
+ discountInfo.app_user_id === userId &&
173
+ dayjs(discountInfo.ended_at).isAfter(dayjs());
174
+
175
+ console.log('hasActiveDiscount', hasActiveDiscount);
176
+
177
+ if (!hasActiveDiscount) {
178
+ const startedAt = dayjs();
179
+ const endedAt = startedAt.clone().add(this.exposureTimeSec, 'second');
180
+ await createAppUserDiscount({
181
+ startedAt: startedAt.toDate(),
182
+ endedAt: endedAt.toDate(),
183
+ userId,
184
+ sdkKey,
185
+ });
186
+ this.emitter.emit('LOAD_DISCOUNT_INFO');
187
+ }
188
+ }
189
+
190
+ return result;
191
+ }
192
+
193
+ public getInitialized() {
194
+ return this.initialized;
195
+ }
196
+
197
+ public async getUserId(): Promise<string | null> {
198
+ try {
199
+ const userId = await MonetaiReactNative.getUserId();
200
+ return userId;
201
+ } catch (error) {
202
+ console.error('[MonetaiSDK] getUserId error', error);
203
+ return null;
204
+ }
205
+ }
206
+
207
+ public async getSdkKey(): Promise<string | null> {
208
+ try {
209
+ const sdkKey = await MonetaiReactNative.getSdkKey();
210
+ return sdkKey;
211
+ } catch (error) {
212
+ console.error('[MonetaiSDK] getSdkKey error', error);
213
+ return null;
214
+ }
215
+ }
216
+
217
+ public async reset() {
218
+ await MonetaiReactNative.reset();
219
+ this.initialized = false;
220
+ }
221
+ }
222
+
223
+ // 싱글턴으로 export
224
+ const monetaiSDK = new MonetaiSDK();
225
+ export default monetaiSDK;
package/src/index.tsx ADDED
@@ -0,0 +1,5 @@
1
+ import MonetaiSDK from './MonetaiSDK';
2
+ import MonetaiProvider from './MonetaiProvider';
3
+
4
+ export default MonetaiSDK;
5
+ export { MonetaiProvider };
package/src/lib.ts ADDED
@@ -0,0 +1,17 @@
1
+ export function getSDKVersion(): string {
2
+ // __SDK_VERSION__이 정의되어 있으면 해당 값을 사용
3
+ if (typeof __SDK_VERSION__ === 'string') {
4
+ return __SDK_VERSION__;
5
+ }
6
+
7
+ // 그렇지 않으면, package.json을 lazy하게 불러와 version을 반환
8
+ try {
9
+ // 주의: require 사용 시 번들링 및 환경에 따라 결과가 달라질 수 있음
10
+ // 라이브러리라면 외부에 의존하지 않도록 조심해야 합니다.
11
+ const pkg = require('../package.json');
12
+ return pkg.version;
13
+ } catch (error) {
14
+ console.warn('package.json에서 version을 불러올 수 없습니다.', error);
15
+ return 'unknown';
16
+ }
17
+ }
@@ -0,0 +1,2 @@
1
+ // global.d.ts
2
+ declare const __SDK_VERSION__: string;
package/src/types.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type ABTestGroup = 'baseline' | 'monetai' | 'unknown';
2
+ export type PredictResult = 'non-purchaser' | 'purchaser' | null;
3
+ export interface DiscountInfo {
4
+ startedAt: Date;
5
+ endedAt: Date;
6
+ userId: string;
7
+ sdkKey: string;
8
+ }
9
+
10
+ export interface AppUserDiscountResponse {
11
+ id: number;
12
+ started_at: Date;
13
+ ended_at: Date;
14
+ app_user_id: string;
15
+ sdk_key: string;
16
+ created_at: Date;
17
+ }