@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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daehoon Kim
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "MonetaiReactNative"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://monetai.io.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+
18
+ # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
19
+ # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
20
+ if respond_to?(:install_modules_dependencies, true)
21
+ install_modules_dependencies(s)
22
+ else
23
+ s.dependency "React-Core"
24
+ end
25
+ end
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @hayanmind/monetai-react-native
2
+
3
+ .
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install @hayanmind/monetai-react-native
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import { multiply } from '@hayanmind/monetai-react-native';
15
+
16
+ // ...
17
+
18
+ const result = await multiply(3, 7);
19
+ ```
20
+
21
+ ## Contributing
22
+
23
+ See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
24
+
25
+ ## License
26
+
27
+ MIT
28
+
29
+ ---
30
+
31
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,81 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['MonetaiReactNative_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+
19
+ apply plugin: "com.android.library"
20
+ apply plugin: "kotlin-android"
21
+
22
+
23
+ def getExtOrIntegerDefault(name) {
24
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["MonetaiReactNative_" + name]).toInteger()
25
+ }
26
+
27
+ def supportsNamespace() {
28
+ def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
29
+ def major = parsed[0].toInteger()
30
+ def minor = parsed[1].toInteger()
31
+
32
+ // Namespace support was added in 7.3.0
33
+ return (major == 7 && minor >= 3) || major >= 8
34
+ }
35
+
36
+ android {
37
+ if (supportsNamespace()) {
38
+ namespace "com.hayanmind.monetaireactnative"
39
+
40
+ sourceSets {
41
+ main {
42
+ manifest.srcFile "src/main/AndroidManifestNew.xml"
43
+ }
44
+ }
45
+ }
46
+
47
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
48
+
49
+ defaultConfig {
50
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
51
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
52
+ }
53
+
54
+ buildTypes {
55
+ release {
56
+ minifyEnabled false
57
+ }
58
+ }
59
+
60
+ lintOptions {
61
+ disable "GradleCompatible"
62
+ }
63
+
64
+ compileOptions {
65
+ sourceCompatibility JavaVersion.VERSION_1_8
66
+ targetCompatibility JavaVersion.VERSION_1_8
67
+ }
68
+ }
69
+
70
+ repositories {
71
+ mavenCentral()
72
+ google()
73
+ }
74
+
75
+ def kotlin_version = getExtOrDefault("kotlinVersion")
76
+
77
+ dependencies {
78
+ implementation "com.facebook.react:react-android"
79
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
80
+ }
81
+
@@ -0,0 +1,5 @@
1
+ MonetaiReactNative_kotlinVersion=2.0.21
2
+ MonetaiReactNative_minSdkVersion=24
3
+ MonetaiReactNative_targetSdkVersion=34
4
+ MonetaiReactNative_compileSdkVersion=35
5
+ MonetaiReactNative_ndkVersion=27.1.12297006
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.hayanmind.monetaireactnative">
3
+ </manifest>
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,110 @@
1
+ package com.hayanmind.monetaireactnative
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.util.Log
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
8
+ import com.facebook.react.bridge.ReactMethod
9
+ import com.facebook.react.bridge.Promise
10
+
11
+ class MonetaiReactNativeModule(reactContext: ReactApplicationContext) :
12
+ ReactContextBaseJavaModule(reactContext) {
13
+
14
+ private val TAG = "MonetaiSDK"
15
+ private val PREFS_NAME = "MonetaiPrefs"
16
+ private val USER_ID_KEY = "MonetaiAppAccountToken"
17
+ private val SDK_KEY = "MonetaiSdkKey"
18
+
19
+ private val sharedPreferences: SharedPreferences by lazy {
20
+ reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
21
+ }
22
+
23
+ override fun getName(): String {
24
+ return NAME
25
+ }
26
+
27
+ @ReactMethod
28
+ fun setUserId(userId: String, promise: Promise) {
29
+ try {
30
+ sharedPreferences.edit().putString(USER_ID_KEY, userId).apply()
31
+ Log.d(TAG, "userId 저장 완료: $userId")
32
+ promise.resolve(null)
33
+ } catch (e: Exception) {
34
+ promise.reject("ERROR_SETTING_USER_ID", "userId 저장 실패", e)
35
+ }
36
+ }
37
+
38
+ @ReactMethod
39
+ fun getUserId(promise: Promise) {
40
+ try {
41
+ val userId = sharedPreferences.getString(USER_ID_KEY, null)
42
+ if (userId != null) {
43
+ Log.d(TAG, "getUserId: $userId")
44
+ promise.resolve(userId)
45
+ } else {
46
+ Log.d(TAG, "getUserId: 값이 없음, null 반환")
47
+ promise.resolve(null)
48
+ }
49
+ } catch (e: Exception) {
50
+ promise.reject("ERROR_GETTING_USER_ID", "userId 조회 실패", e)
51
+ }
52
+ }
53
+
54
+ @ReactMethod
55
+ fun setSdkKey(sdkKey: String, promise: Promise) {
56
+ try {
57
+ sharedPreferences.edit().putString(SDK_KEY, sdkKey).apply()
58
+ Log.d(TAG, "sdkKey 저장 완료")
59
+ promise.resolve(null)
60
+ } catch (e: Exception) {
61
+ promise.reject("ERROR_SETTING_SDK_KEY", "sdkKey 저장 실패", e)
62
+ }
63
+ }
64
+
65
+ @ReactMethod
66
+ fun getSdkKey(promise: Promise) {
67
+ try {
68
+ val sdkKey = sharedPreferences.getString(SDK_KEY, null)
69
+ if (sdkKey != null) {
70
+ Log.d(TAG, "getSdkKey 성공")
71
+ promise.resolve(sdkKey)
72
+ } else {
73
+ Log.d(TAG, "getSdkKey: 값이 없음, null 반환")
74
+ promise.resolve(null)
75
+ }
76
+ } catch (e: Exception) {
77
+ promise.reject("ERROR_GETTING_SDK_KEY", "sdkKey 조회 실패", e)
78
+ }
79
+ }
80
+
81
+ @ReactMethod
82
+ fun startObserving(useStoreKit2: Boolean, promise: Promise) {
83
+ Log.w(TAG, "startObserving: Android 플랫폼에서는 지원하지 않는 기능입니다")
84
+ promise.resolve(null)
85
+ }
86
+
87
+ @ReactMethod
88
+ fun sendReceipt(promise: Promise) {
89
+ Log.w(TAG, "sendReceipt: Android 플랫폼에서는 지원하지 않는 기능입니다")
90
+ promise.resolve(null)
91
+ }
92
+
93
+ @ReactMethod
94
+ fun reset(promise: Promise) {
95
+ try {
96
+ sharedPreferences.edit()
97
+ .remove(USER_ID_KEY)
98
+ .remove(SDK_KEY)
99
+ .apply()
100
+ Log.d(TAG, "userId와 sdkKey가 초기화되었습니다")
101
+ promise.resolve(null)
102
+ } catch (e: Exception) {
103
+ promise.reject("ERROR_RESETTING", "데이터 초기화 실패", e)
104
+ }
105
+ }
106
+
107
+ companion object {
108
+ const val NAME = "MonetaiReactNative"
109
+ }
110
+ }
@@ -0,0 +1,17 @@
1
+ package com.hayanmind.monetaireactnative
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+
9
+ class MonetaiReactNativePackage : ReactPackage {
10
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
11
+ return listOf(MonetaiReactNativeModule(reactContext))
12
+ }
13
+
14
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
15
+ return emptyList()
16
+ }
17
+ }
@@ -0,0 +1,2 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTViewManager.h>
@@ -0,0 +1,38 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(MonetaiReactNative, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(multiply:(float)a withB:(float)b
6
+ withResolver:(RCTPromiseResolveBlock)resolve
7
+ withRejecter:(RCTPromiseRejectBlock)reject)
8
+
9
+ RCT_EXTERN_METHOD(startObserving:(BOOL)useStoreKit2
10
+ withResolver:(RCTPromiseResolveBlock)resolve
11
+ withRejecter:(RCTPromiseRejectBlock)reject)
12
+
13
+ RCT_EXTERN_METHOD(setUserId:(NSString *)userId
14
+ withResolver:(RCTPromiseResolveBlock)resolve
15
+ withRejecter:(RCTPromiseRejectBlock)reject)
16
+
17
+ RCT_EXTERN_METHOD(getUserId:(RCTPromiseResolveBlock)resolve
18
+ withRejecter:(RCTPromiseRejectBlock)reject)
19
+
20
+ RCT_EXTERN_METHOD(setSdkKey:(NSString *)sdkKey
21
+ withResolver:(RCTPromiseResolveBlock)resolve
22
+ withRejecter:(RCTPromiseRejectBlock)reject)
23
+
24
+ RCT_EXTERN_METHOD(getSdkKey:(RCTPromiseResolveBlock)resolve
25
+ withRejecter:(RCTPromiseRejectBlock)reject)
26
+
27
+ RCT_EXTERN_METHOD(sendReceipt:(RCTPromiseResolveBlock)resolve
28
+ withRejecter:(RCTPromiseRejectBlock)reject)
29
+
30
+ RCT_EXTERN_METHOD(reset:(RCTPromiseResolveBlock)resolve
31
+ withRejecter:(RCTPromiseRejectBlock)reject)
32
+
33
+ + (BOOL)requiresMainQueueSetup
34
+ {
35
+ return NO;
36
+ }
37
+
38
+ @end
@@ -0,0 +1,336 @@
1
+ import Foundation
2
+ import StoreKit
3
+
4
+ // Hardcoded endpoint URL for mapping
5
+ private let mappingURL = URL(string: "https://monetai-api-414410537412.us-central1.run.app/sdk/transaction-id-to-user-id/ios")!
6
+ // 영수증 검증용 엔드포인트
7
+ private let receiptValidationURL = URL(string: "https://monetai-api-414410537412.us-central1.run.app/sdk/transaction-id-to-user-id/ios/receipt")!
8
+
9
+ // MARK: - 영수증 검증 및 전송 로직
10
+ class ReceiptValidator {
11
+ static let shared = ReceiptValidator()
12
+
13
+ // 영수증 전송만 담당하는 단일 메소드
14
+ func sendReceipt() {
15
+ guard let receiptURL = Bundle.main.appStoreReceiptURL,
16
+ let receiptData = try? Data(contentsOf: receiptURL),
17
+ let userId = KeychainManager.shared.getToken(),
18
+ let bundleId = Bundle.main.bundleIdentifier,
19
+ let sdkKey = KeychainManager.shared.getSdkKey() else {
20
+ print("[Monetai][Error] 필요한 정보가 누락되었습니다")
21
+ return
22
+ }
23
+
24
+ let base64Receipt = receiptData.base64EncodedString()
25
+
26
+ let payload: [String: Any] = [
27
+ "receiptData": base64Receipt,
28
+ "bundleId": bundleId,
29
+ "userId": userId,
30
+ "sdkKey": sdkKey
31
+ ]
32
+
33
+ var request = URLRequest(url: receiptValidationURL)
34
+ request.httpMethod = "POST"
35
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
36
+ request.httpBody = try? JSONSerialization.data(withJSONObject: payload)
37
+
38
+ URLSession.shared.dataTask(with: request) { data, response, error in
39
+ if let error = error {
40
+ print("[Monetai][Error] 영수증 전송 실패: \(error)")
41
+ return
42
+ }
43
+
44
+ if let httpResponse = response as? HTTPURLResponse {
45
+ print("[Monetai][Debug] 영수증 전송 응답: \(httpResponse.statusCode)")
46
+ }
47
+ }.resume()
48
+ }
49
+ }
50
+
51
+ // MARK: - Keychain Helper
52
+ class KeychainManager {
53
+ static let shared = KeychainManager()
54
+ private let tokenKey = "MonetaiAppAccountToken"
55
+ private let sdkKey = "MonetaiSdkKey"
56
+
57
+ func setToken(_ token: String) {
58
+ guard let data = token.data(using: .utf8) else { return }
59
+ let query: [String: Any] = [
60
+ kSecClass as String: kSecClassGenericPassword,
61
+ kSecAttrAccount as String: tokenKey,
62
+ kSecValueData as String: data
63
+ ]
64
+ SecItemDelete(query as CFDictionary)
65
+ let status = SecItemAdd(query as CFDictionary, nil)
66
+ print("[Monetai][Debug] setUserId status: \(status)")
67
+ }
68
+
69
+ func getToken() -> String? {
70
+ let query: [String: Any] = [
71
+ kSecClass as String: kSecClassGenericPassword,
72
+ kSecAttrAccount as String: tokenKey,
73
+ kSecReturnData as String: kCFBooleanTrue!,
74
+ kSecMatchLimit as String: kSecMatchLimitOne
75
+ ]
76
+ var item: AnyObject?
77
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
78
+ guard status == errSecSuccess,
79
+ let data = item as? Data,
80
+ let token = String(data: data, encoding: .utf8) else {
81
+ print("[Monetai][Error] getUserId status: \(status)")
82
+ return nil
83
+ }
84
+ print("[Monetai][Debug] getUserId: \(token)")
85
+ return token
86
+ }
87
+
88
+ func setSdkKey(_ key: String) {
89
+ guard let data = key.data(using: .utf8) else { return }
90
+ let query: [String: Any] = [
91
+ kSecClass as String: kSecClassGenericPassword,
92
+ kSecAttrAccount as String: sdkKey,
93
+ kSecValueData as String: data
94
+ ]
95
+ SecItemDelete(query as CFDictionary)
96
+ let status = SecItemAdd(query as CFDictionary, nil)
97
+ print("[Monetai][Debug] setSdkKey status: \(status)")
98
+ }
99
+
100
+ func getSdkKey() -> String? {
101
+ let query: [String: Any] = [
102
+ kSecClass as String: kSecClassGenericPassword,
103
+ kSecAttrAccount as String: sdkKey,
104
+ kSecReturnData as String: kCFBooleanTrue!,
105
+ kSecMatchLimit as String: kSecMatchLimitOne
106
+ ]
107
+ var item: AnyObject?
108
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
109
+ guard status == errSecSuccess,
110
+ let data = item as? Data,
111
+ let key = String(data: data, encoding: .utf8) else {
112
+ print("[Monetai][Error] getSdkKey status: \(status)")
113
+ return nil
114
+ }
115
+ print("[Monetai][Debug] getSdkKey: \(key)")
116
+ return key
117
+ }
118
+ }
119
+
120
+ // MARK: - StoreKit1 Observer
121
+ class PurchaseObserver: NSObject, SKPaymentTransactionObserver {
122
+ static let shared = PurchaseObserver()
123
+
124
+ private override init() {
125
+ super.init()
126
+ print("[Monetai][Debug] PurchaseObserver initialized")
127
+ SKPaymentQueue.default().add(self)
128
+ }
129
+
130
+ deinit {
131
+ SKPaymentQueue.default().remove(self)
132
+ print("[Monetai][Debug] PurchaseObserver deinitialized")
133
+ }
134
+
135
+ @objc static func start() {
136
+ print("[Monetai][Debug] PurchaseObserver.start() called")
137
+ _ = PurchaseObserver.shared
138
+ }
139
+
140
+ func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
141
+ print("[Monetai][Debug] paymentQueue count: \(transactions.count)")
142
+ for tx in transactions {
143
+ let stateDesc: String = {
144
+ switch tx.transactionState {
145
+ case .purchasing: return "purchasing"
146
+ case .purchased: return "purchased"
147
+ case .failed: return "failed"
148
+ case .restored: return "restored"
149
+ case .deferred: return "deferred"
150
+ @unknown default: return "unknown"
151
+ }
152
+ }()
153
+ print("[Monetai][Debug] State: \(stateDesc), id: \(tx.transactionIdentifier ?? "nil")")
154
+ guard let origId = tx.original?.transactionIdentifier ?? tx.transactionIdentifier else {
155
+ print("[Monetai][Error] No transactionIdentifier")
156
+ continue
157
+ }
158
+ handleTransaction(origId)
159
+ }
160
+ }
161
+
162
+ private func handleTransaction(_ originalTxId: String) {
163
+ print("[Monetai][Debug] handleTransaction: \(originalTxId)")
164
+ guard let userId = KeychainManager.shared.getToken() else {
165
+ print("[Monetai][Error] no userId set")
166
+ return
167
+ }
168
+ guard let bundleId = Bundle.main.bundleIdentifier else {
169
+ print("[Monetai][Error] Bundle identifier is nil")
170
+ return
171
+ }
172
+ guard let sdkKey = KeychainManager.shared.getSdkKey() else {
173
+ print("[Monetai][Error] no sdkKey set")
174
+ return
175
+ }
176
+ let payload: [String: Any] = [
177
+ "transactionId": originalTxId,
178
+ "bundleId": bundleId,
179
+ "userId": userId,
180
+ "sdkKey": sdkKey
181
+ ]
182
+ print("[Monetai][Debug] POST to \(mappingURL): \(payload)")
183
+ sendMapping(payload)
184
+ }
185
+
186
+ private func sendMapping(_ payload: [String: Any]) {
187
+ var req = URLRequest(url: mappingURL)
188
+ req.httpMethod = "POST"
189
+ req.setValue("application/json", forHTTPHeaderField: "Content-Type")
190
+ req.httpBody = try? JSONSerialization.data(withJSONObject: payload)
191
+ URLSession.shared.dataTask(with: req) { _, res, err in
192
+ if let err = err {
193
+ print("[Monetai][Error] Mapping POST failed: \(err)")
194
+ } else {
195
+ print("[Monetai][Debug] Mapping POST succeeded: \(String(describing: res))")
196
+ }
197
+ }.resume()
198
+ }
199
+ }
200
+
201
+ // MARK: - StoreKit2 Observer (iOS 15+)
202
+ @available(iOS 15.0, *)
203
+ class StoreKit2Observer {
204
+ static let shared = StoreKit2Observer()
205
+
206
+ private init() {
207
+ print("[Monetai][Debug] StoreKit2Observer initialized")
208
+ Task {
209
+ for await update in Transaction.updates {
210
+ print("[Monetai][Debug] SK2 update: \(update)")
211
+ if case .verified(let txn) = update {
212
+ let origId = String(txn.originalID)
213
+ print("[Monetai][Debug] SK2 origTxId: \(origId)")
214
+ handleTransaction(origId)
215
+ } else {
216
+ print("[Monetai][Error] SK2 unverified: \(update)")
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ private func handleTransaction(_ originalTxId: String) {
223
+ print("[Monetai][Debug] SK2 handle: \(originalTxId)")
224
+ guard let userId = KeychainManager.shared.getToken() else {
225
+ print("[Monetai][Error] SK2 no userId")
226
+ return
227
+ }
228
+ guard let bundleId = Bundle.main.bundleIdentifier else {
229
+ print("[Monetai][Error] Bundle identifier is nil")
230
+ return
231
+ }
232
+ guard let sdkKey = KeychainManager.shared.getSdkKey() else {
233
+ print("[Monetai][Error] no sdkKey set")
234
+ return
235
+ }
236
+ let payload: [String: Any] = [
237
+ "transactionId": originalTxId,
238
+ "bundleId": bundleId,
239
+ "userId": userId,
240
+ "sdkKey": sdkKey
241
+ ]
242
+ print("[Monetai][Debug] SK2 POST to \(mappingURL): \(payload)")
243
+ var req = URLRequest(url: mappingURL)
244
+ req.httpMethod = "POST"
245
+ req.setValue("application/json", forHTTPHeaderField: "Content-Type")
246
+ req.httpBody = try? JSONSerialization.data(withJSONObject: payload)
247
+ URLSession.shared.dataTask(with: req) { _, res, err in
248
+ if let err = err {
249
+ print("[Monetai][Error] SK2 POST failed: \(err)")
250
+ } else {
251
+ print("[Monetai][Debug] SK2 POST succeeded: \(String(describing: res))")
252
+ }
253
+ }.resume()
254
+ }
255
+ }
256
+
257
+ // MARK: - React Native Module
258
+ @objc(MonetaiReactNative)
259
+ class MonetaiReactNative: NSObject {
260
+
261
+ @objc(setUserId:withResolver:withRejecter:)
262
+ func setUserId(_ userId: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
263
+ KeychainManager.shared.setToken(userId)
264
+ resolve(nil)
265
+ }
266
+
267
+ @objc(getUserId:withRejecter:)
268
+ func getUserId(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
269
+ if let userId = KeychainManager.shared.getToken() {
270
+ resolve(userId)
271
+ } else {
272
+ resolve(nil)
273
+ }
274
+ }
275
+
276
+ @objc(setSdkKey:withResolver:withRejecter:)
277
+ func setSdkKey(_ sdkKey: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
278
+ KeychainManager.shared.setSdkKey(sdkKey)
279
+ resolve(nil)
280
+ }
281
+
282
+ @objc(getSdkKey:withRejecter:)
283
+ func getSdkKey(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
284
+ if let sdkKey = KeychainManager.shared.getSdkKey() {
285
+ resolve(sdkKey)
286
+ } else {
287
+ resolve(nil)
288
+ }
289
+ }
290
+
291
+ @objc(startObserving:withResolver:withRejecter:)
292
+ func startObserving(useStoreKit2: Bool, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
293
+ if useStoreKit2 {
294
+ if #available(iOS 15.0, *) {
295
+ _ = StoreKit2Observer.shared
296
+ resolve(nil)
297
+ } else {
298
+ reject("SK2_UNAVAILABLE", "StoreKit2 requires iOS 15.0 or newer", nil)
299
+ }
300
+ } else {
301
+ PurchaseObserver.start()
302
+ resolve(nil)
303
+ }
304
+ }
305
+
306
+ @objc(sendReceipt:withRejecter:)
307
+ func sendReceipt(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
308
+ ReceiptValidator.shared.sendReceipt()
309
+ resolve(nil)
310
+ }
311
+
312
+ @objc(reset:withRejecter:)
313
+ func reset(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
314
+ // userId와 sdkKey 모두 제거
315
+ let tokenQuery: [String: Any] = [
316
+ kSecClass as String: kSecClassGenericPassword,
317
+ kSecAttrAccount as String: "MonetaiAppAccountToken"
318
+ ]
319
+
320
+ let sdkKeyQuery: [String: Any] = [
321
+ kSecClass as String: kSecClassGenericPassword,
322
+ kSecAttrAccount as String: "MonetaiSdkKey"
323
+ ]
324
+
325
+ SecItemDelete(tokenQuery as CFDictionary)
326
+ SecItemDelete(sdkKeyQuery as CFDictionary)
327
+
328
+ print("[Monetai][Debug] userId와 sdkKey가 초기화되었습니다")
329
+ resolve(nil)
330
+ }
331
+
332
+ @objc
333
+ static func requiresMainQueueSetup() -> Bool {
334
+ return false
335
+ }
336
+ }