@1selfworld/adchain-sdk-react-native 1.0.1
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.
- package/LICENSE +20 -0
- package/README.md +211 -0
- package/adchain-sdk-react-native.podspec +25 -0
- package/android/build.gradle +38 -0
- package/android/src/main/java/com/adchain/exposdk/AdchainOfferwallViewManager.kt +288 -0
- package/android/src/main/java/com/adchain/exposdk/AdchainSdkModule.kt +742 -0
- package/android/src/main/java/com/adchain/exposdk/AdchainSdkPackage.kt +14 -0
- package/app.plugin.js +17 -0
- package/ios/AdchainOfferwallViewManager.m +17 -0
- package/ios/AdchainOfferwallViewManager.swift +242 -0
- package/ios/AdchainSdk.m +87 -0
- package/ios/AdchainSdk.swift +792 -0
- package/lib/module/index.js +162 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +88 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +153 -0
- package/plugin/src/withAdchainAndroid.js +56 -0
- package/plugin/src/withAdchainConfig.js +22 -0
- package/plugin/src/withAdchainIOS.js +60 -0
- package/react-native.config.js +8 -0
- package/src/index.tsx +274 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
import AdchainSDK
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
@objc(AdchainSdk)
|
|
7
|
+
class AdchainSdkModule: RCTEventEmitter {
|
|
8
|
+
|
|
9
|
+
// Helper function to get the top view controller
|
|
10
|
+
private func getTopViewController() -> UIViewController? {
|
|
11
|
+
guard let windowScene = UIApplication.shared.connectedScenes
|
|
12
|
+
.compactMap({ $0 as? UIWindowScene })
|
|
13
|
+
.first,
|
|
14
|
+
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
|
|
15
|
+
let rootViewController = window.rootViewController else {
|
|
16
|
+
return nil
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
var topController = rootViewController
|
|
20
|
+
while let presentedViewController = topController.presentedViewController {
|
|
21
|
+
topController = presentedViewController
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return topController
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// MARK: - Properties
|
|
28
|
+
|
|
29
|
+
// 내부 인스턴스 관리 (자동 생성/캐싱)
|
|
30
|
+
private var quizInstances: [String: AdchainQuiz] = [:]
|
|
31
|
+
private var missionInstances: [String: AdchainMission] = [:]
|
|
32
|
+
|
|
33
|
+
// MARK: - RCTEventEmitter
|
|
34
|
+
|
|
35
|
+
@objc override static func requiresMainQueueSetup() -> Bool { true }
|
|
36
|
+
|
|
37
|
+
override func supportedEvents() -> [String]! {
|
|
38
|
+
return ["onQuizCompleted", "onMissionCompleted", "onMissionProgressed", "onMissionRefreshed"] // Quiz, Mission 이벤트 지원
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// MARK: - 1. SDK 초기화
|
|
42
|
+
|
|
43
|
+
@objc func initialize(_ appKey: NSString,
|
|
44
|
+
appSecret: NSString,
|
|
45
|
+
options: NSDictionary?,
|
|
46
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
47
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
48
|
+
|
|
49
|
+
// options에서 environment와 timeout 추출
|
|
50
|
+
let environment = options?["environment"] as? String
|
|
51
|
+
let timeout = options?["timeout"] as? NSNumber
|
|
52
|
+
|
|
53
|
+
let env: AdchainSdkConfig.Environment
|
|
54
|
+
if let envString = environment {
|
|
55
|
+
switch envString.uppercased() {
|
|
56
|
+
case "STAGING":
|
|
57
|
+
env = .staging
|
|
58
|
+
case "DEVELOPMENT":
|
|
59
|
+
env = .development
|
|
60
|
+
default:
|
|
61
|
+
env = .production
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
env = .production
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var configBuilder = AdchainSdkConfig.Builder(appKey: appKey as String, appSecret: appSecret as String)
|
|
68
|
+
.setEnvironment(env)
|
|
69
|
+
|
|
70
|
+
if let timeoutValue = timeout {
|
|
71
|
+
configBuilder = configBuilder.setTimeout(TimeInterval(timeoutValue.doubleValue))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let config = configBuilder.build()
|
|
75
|
+
|
|
76
|
+
// UI 관련 작업을 메인 스레드에서 실행
|
|
77
|
+
DispatchQueue.main.async {
|
|
78
|
+
if UIApplication.shared.delegate?.window??.rootViewController?.view.window?.windowScene?.delegate is UIWindowSceneDelegate {
|
|
79
|
+
AdchainSdk.shared.initialize(application: UIApplication.shared, sdkConfig: config)
|
|
80
|
+
resolver([
|
|
81
|
+
"success": true,
|
|
82
|
+
"message": "SDK initialized successfully"
|
|
83
|
+
])
|
|
84
|
+
} else {
|
|
85
|
+
// Fallback
|
|
86
|
+
AdchainSdk.shared.initialize(application: UIApplication.shared, sdkConfig: config)
|
|
87
|
+
resolver([
|
|
88
|
+
"success": true,
|
|
89
|
+
"message": "SDK initialized successfully"
|
|
90
|
+
])
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// MARK: - 2. 인증 관련 (4개)
|
|
96
|
+
|
|
97
|
+
@objc func login(_ userId: NSString,
|
|
98
|
+
userInfo: NSDictionary?,
|
|
99
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
100
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
101
|
+
|
|
102
|
+
// userInfo에서 값 추출
|
|
103
|
+
let gender = userInfo?["gender"] as? String
|
|
104
|
+
let birthYear = userInfo?["birthYear"] as? NSNumber
|
|
105
|
+
let customProperties = userInfo?["customProperties"] as? NSDictionary
|
|
106
|
+
|
|
107
|
+
var userGender: AdchainSdkUser.Gender = .other
|
|
108
|
+
if let genderString = gender {
|
|
109
|
+
switch genderString.uppercased() {
|
|
110
|
+
case "MALE", "M":
|
|
111
|
+
userGender = .male
|
|
112
|
+
case "FEMALE", "F":
|
|
113
|
+
userGender = .female
|
|
114
|
+
default:
|
|
115
|
+
userGender = .other
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let user = AdchainSdkUser(
|
|
120
|
+
userId: userId as String,
|
|
121
|
+
gender: userGender,
|
|
122
|
+
birthYear: birthYear?.intValue
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// Custom properties 처리 (iOS SDK가 지원한다면)
|
|
126
|
+
// 현재는 생략
|
|
127
|
+
|
|
128
|
+
class LoginListenerImpl: NSObject, AdchainSdkLoginListener {
|
|
129
|
+
let resolver: RCTPromiseResolveBlock
|
|
130
|
+
let rejecter: RCTPromiseRejectBlock
|
|
131
|
+
|
|
132
|
+
init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
|
133
|
+
self.resolver = resolver
|
|
134
|
+
self.rejecter = rejecter
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func onSuccess() {
|
|
138
|
+
resolver([
|
|
139
|
+
"success": true,
|
|
140
|
+
"message": "Login successful"
|
|
141
|
+
])
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func onFailure(_ error: AdchainLoginError) {
|
|
145
|
+
rejecter("LOGIN_ERROR", error.description, nil)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
AdchainSdk.shared.login(adchainSdkUser: user, listener: LoginListenerImpl(resolver: resolver, rejecter: rejecter))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@objc func logout(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
153
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
154
|
+
AdchainSdk.shared.logout()
|
|
155
|
+
resolver([
|
|
156
|
+
"success": true,
|
|
157
|
+
"message": "Logout successful"
|
|
158
|
+
])
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@objc func isLoggedIn(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
162
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
163
|
+
resolver(AdchainSdk.shared.isLoggedIn)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@objc func getCurrentUser(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
167
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
168
|
+
if let user = AdchainSdk.shared.getCurrentUser() {
|
|
169
|
+
var userDict: [String: Any] = ["userId": user.userId]
|
|
170
|
+
|
|
171
|
+
if let gender = user.gender {
|
|
172
|
+
userDict["gender"] = gender.rawValue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if let birthYear = user.birthYear {
|
|
176
|
+
userDict["birthYear"] = birthYear
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
resolver(userDict)
|
|
180
|
+
} else {
|
|
181
|
+
resolver(nil)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// MARK: - 3. Quiz 관련 (2개)
|
|
186
|
+
|
|
187
|
+
@objc func loadQuizList(_ unitId: NSString,
|
|
188
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
189
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
190
|
+
// 인스턴스 자동 생성/재사용
|
|
191
|
+
let quiz: AdchainQuiz = quizInstances[unitId as String] ?? {
|
|
192
|
+
let newQuiz = AdchainQuiz()
|
|
193
|
+
quizInstances[unitId as String] = newQuiz
|
|
194
|
+
return newQuiz
|
|
195
|
+
}()
|
|
196
|
+
|
|
197
|
+
// Android와 동일하게 직접 load 호출
|
|
198
|
+
// shouldStoreCallbacks: false로 설정하여 refreshAfterCompletion에서 재호출되지 않도록 함
|
|
199
|
+
quiz.load(
|
|
200
|
+
onSuccess: { quizResponse in
|
|
201
|
+
var responseDict: [String: Any] = [
|
|
202
|
+
"success": quizResponse.success ?? true
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
if let titleText = quizResponse.titleText {
|
|
206
|
+
responseDict["titleText"] = titleText
|
|
207
|
+
}
|
|
208
|
+
if let completedImageUrl = quizResponse.completedImageUrl {
|
|
209
|
+
responseDict["completedImageUrl"] = completedImageUrl
|
|
210
|
+
}
|
|
211
|
+
if let completedImageWidth = quizResponse.completedImageWidth {
|
|
212
|
+
responseDict["completedImageWidth"] = completedImageWidth
|
|
213
|
+
}
|
|
214
|
+
if let completedImageHeight = quizResponse.completedImageHeight {
|
|
215
|
+
responseDict["completedImageHeight"] = completedImageHeight
|
|
216
|
+
}
|
|
217
|
+
if let message = quizResponse.message {
|
|
218
|
+
responseDict["message"] = message
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// events 배열 처리
|
|
222
|
+
let eventsArray = quizResponse.events.map { quizEvent in
|
|
223
|
+
return [
|
|
224
|
+
"id": quizEvent.id,
|
|
225
|
+
"title": quizEvent.title,
|
|
226
|
+
"description": quizEvent.description ?? "",
|
|
227
|
+
"imageUrl": quizEvent.imageUrl,
|
|
228
|
+
"point": quizEvent.point,
|
|
229
|
+
"isCompleted": quizEvent.completed ?? false
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
responseDict["events"] = eventsArray
|
|
233
|
+
|
|
234
|
+
resolver(responseDict)
|
|
235
|
+
},
|
|
236
|
+
onFailure: { error in
|
|
237
|
+
rejecter("QUIZ_LOAD_ERROR", error.localizedDescription, nil)
|
|
238
|
+
},
|
|
239
|
+
shouldStoreCallbacks: false
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@objc func clickQuiz(_ unitId: NSString,
|
|
244
|
+
quizId: NSString,
|
|
245
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
246
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
247
|
+
// 인스턴스 자동 생성/재사용
|
|
248
|
+
let quiz: AdchainQuiz = quizInstances[unitId as String] ?? {
|
|
249
|
+
let newQuiz = AdchainQuiz()
|
|
250
|
+
quizInstances[unitId as String] = newQuiz
|
|
251
|
+
return newQuiz
|
|
252
|
+
}()
|
|
253
|
+
|
|
254
|
+
// QuizEventsListener 설정
|
|
255
|
+
class QuizEventListenerImpl: NSObject, AdchainQuizEventsListener {
|
|
256
|
+
weak var module: AdchainSdkModule?
|
|
257
|
+
let unitId: String
|
|
258
|
+
let quizId: String
|
|
259
|
+
|
|
260
|
+
init(module: AdchainSdkModule?, unitId: String, quizId: String) {
|
|
261
|
+
self.module = module
|
|
262
|
+
self.unitId = unitId
|
|
263
|
+
self.quizId = quizId
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
func onImpressed(_ quizEvent: QuizEvent) {
|
|
267
|
+
// 필요시 처리
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
func onClicked(_ quizEvent: QuizEvent) {
|
|
271
|
+
// 필요시 처리
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func onQuizCompleted(_ quizEvent: QuizEvent, rewardAmount: Int) {
|
|
275
|
+
// React Native로 이벤트 전송
|
|
276
|
+
module?.sendEvent(withName: "onQuizCompleted", body: [
|
|
277
|
+
"unitId": unitId,
|
|
278
|
+
"quizId": quizId,
|
|
279
|
+
"rewardAmount": rewardAmount,
|
|
280
|
+
"timestamp": Date().timeIntervalSince1970
|
|
281
|
+
])
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let listener = QuizEventListenerImpl(module: self, unitId: unitId as String, quizId: quizId as String)
|
|
286
|
+
quiz.setQuizEventsListener(listener)
|
|
287
|
+
|
|
288
|
+
// iOS SDK가 ID 기반 클릭을 지원한다고 가정
|
|
289
|
+
DispatchQueue.main.async { [weak self] in
|
|
290
|
+
if let topVC = self?.getTopViewController() {
|
|
291
|
+
// iOS SDK의 네이티브 ID 기반 메서드 사용 (Android와 동일)
|
|
292
|
+
// 만약 이 메서드가 없다면 컴파일 오류가 발생할 것
|
|
293
|
+
quiz.clickQuiz(quizId as String, from: topVC)
|
|
294
|
+
resolver([
|
|
295
|
+
"success": true,
|
|
296
|
+
"message": "Quiz clicked"
|
|
297
|
+
])
|
|
298
|
+
} else {
|
|
299
|
+
rejecter("QUIZ_ERROR", "No view controller available", nil)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// MARK: - 4. Mission 관련 (3개)
|
|
305
|
+
|
|
306
|
+
@objc func loadMissionList(_ unitId: NSString,
|
|
307
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
308
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
309
|
+
// 인스턴스 자동 생성/재사용
|
|
310
|
+
let mission: AdchainMission = missionInstances[unitId as String] ?? {
|
|
311
|
+
let newMission = AdchainMission()
|
|
312
|
+
missionInstances[unitId as String] = newMission
|
|
313
|
+
return newMission
|
|
314
|
+
}()
|
|
315
|
+
|
|
316
|
+
// MissionEventsListener 설정 (missionRefreshed 이벤트를 받기 위해)
|
|
317
|
+
// eventsListener가 없으면 설정
|
|
318
|
+
if mission.eventsListener == nil {
|
|
319
|
+
class LoadMissionEventListenerImpl: NSObject, AdchainMissionEventsListener {
|
|
320
|
+
weak var module: AdchainSdkModule?
|
|
321
|
+
let unitId: String
|
|
322
|
+
|
|
323
|
+
init(module: AdchainSdkModule?, unitId: String) {
|
|
324
|
+
self.module = module
|
|
325
|
+
self.unitId = unitId
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func onImpressed(_ mission: Mission) {
|
|
329
|
+
// 필요시 처리
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
func onClicked(_ mission: Mission) {
|
|
333
|
+
// 필요시 처리
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
func onCompleted(_ mission: Mission) {
|
|
337
|
+
// React Native로 이벤트 전송
|
|
338
|
+
module?.sendEvent(withName: "onMissionCompleted", body: [
|
|
339
|
+
"unitId": unitId,
|
|
340
|
+
"missionId": mission.id,
|
|
341
|
+
"timestamp": Date().timeIntervalSince1970
|
|
342
|
+
])
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func onProgressed(_ mission: Mission) {
|
|
346
|
+
// React Native로 이벤트 전송
|
|
347
|
+
module?.sendEvent(withName: "onMissionProgressed", body: [
|
|
348
|
+
"unitId": unitId,
|
|
349
|
+
"missionId": mission.id,
|
|
350
|
+
"timestamp": Date().timeIntervalSince1970
|
|
351
|
+
])
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
func onRefreshed(unitId: String?) {
|
|
355
|
+
// React Native로 이벤트 전송
|
|
356
|
+
module?.sendEvent(withName: "onMissionRefreshed", body: [
|
|
357
|
+
"unitId": unitId ?? self.unitId,
|
|
358
|
+
"timestamp": Date().timeIntervalSince1970
|
|
359
|
+
])
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let listener = LoadMissionEventListenerImpl(module: self, unitId: unitId as String)
|
|
364
|
+
mission.setEventsListener(listener)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Android와 동일하게 직접 load 호출
|
|
368
|
+
// shouldStoreCallbacks: false로 설정하여 refreshAfterCompletion에서 재호출되지 않도록 함
|
|
369
|
+
mission.load(
|
|
370
|
+
onSuccess: { (missionList, progress) in
|
|
371
|
+
let missionsArray = missionList.map { m in
|
|
372
|
+
return [
|
|
373
|
+
"id": m.id,
|
|
374
|
+
"title": m.title,
|
|
375
|
+
"description": m.description,
|
|
376
|
+
"imageUrl": m.imageUrl,
|
|
377
|
+
"point": m.point,
|
|
378
|
+
"isCompleted": m.status == "completed",
|
|
379
|
+
"type": m.type?.rawValue ?? "normal",
|
|
380
|
+
"actionUrl": m.landingUrl
|
|
381
|
+
]
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let completedCount = progress.current
|
|
385
|
+
let totalCount = progress.total
|
|
386
|
+
|
|
387
|
+
let result: [String: Any] = [
|
|
388
|
+
"missions": missionsArray,
|
|
389
|
+
"completedCount": completedCount,
|
|
390
|
+
"totalCount": totalCount,
|
|
391
|
+
"canClaimReward": progress.isCompleted && totalCount > 0,
|
|
392
|
+
|
|
393
|
+
// 신규 필드 추가 (MissionResponse에서 가져오기)
|
|
394
|
+
"titleText": mission.missionResponse?.titleText ?? "무료 포인트 모으기!",
|
|
395
|
+
"descriptionText": mission.missionResponse?.descriptionText ?? "간단 광고 참여하고 100 포인트 받기",
|
|
396
|
+
"bottomText": mission.missionResponse?.bottomText ?? "800만 포인트 받으러 가기",
|
|
397
|
+
"rewardIconUrl": mission.missionResponse?.rewardIconUrl ?? "https://adchain-assets.1self.world/img_reward_coin.png",
|
|
398
|
+
"bottomIconUrl": mission.missionResponse?.bottomIconUrl ?? "https://adchain-assets.1self.world/img_offerwall_coin.png"
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
resolver(result)
|
|
402
|
+
},
|
|
403
|
+
onFailure: { error in
|
|
404
|
+
rejecter("MISSION_LOAD_ERROR", error.localizedDescription, nil)
|
|
405
|
+
},
|
|
406
|
+
shouldStoreCallbacks: false
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@objc func clickMission(_ unitId: NSString,
|
|
411
|
+
missionId: NSString,
|
|
412
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
413
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
414
|
+
// 인스턴스 자동 생성/재사용
|
|
415
|
+
let mission: AdchainMission = missionInstances[unitId as String] ?? {
|
|
416
|
+
let newMission = AdchainMission()
|
|
417
|
+
missionInstances[unitId as String] = newMission
|
|
418
|
+
return newMission
|
|
419
|
+
}()
|
|
420
|
+
|
|
421
|
+
// MissionEventsListener 설정
|
|
422
|
+
class MissionEventListenerImpl: NSObject, AdchainMissionEventsListener {
|
|
423
|
+
weak var module: AdchainSdkModule?
|
|
424
|
+
let unitId: String
|
|
425
|
+
let missionId: String
|
|
426
|
+
|
|
427
|
+
init(module: AdchainSdkModule?, unitId: String, missionId: String) {
|
|
428
|
+
self.module = module
|
|
429
|
+
self.unitId = unitId
|
|
430
|
+
self.missionId = missionId
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
func onImpressed(_ mission: Mission) {
|
|
434
|
+
// 필요시 처리
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
func onClicked(_ mission: Mission) {
|
|
438
|
+
// 필요시 처리
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
func onCompleted(_ mission: Mission) {
|
|
442
|
+
// React Native로 이벤트 전송
|
|
443
|
+
module?.sendEvent(withName: "onMissionCompleted", body: [
|
|
444
|
+
"unitId": unitId,
|
|
445
|
+
"missionId": missionId,
|
|
446
|
+
"timestamp": Date().timeIntervalSince1970
|
|
447
|
+
])
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
func onProgressed(_ mission: Mission) {
|
|
451
|
+
// React Native로 이벤트 전송 (missionCompleted와 동일한 구조)
|
|
452
|
+
module?.sendEvent(withName: "onMissionProgressed", body: [
|
|
453
|
+
"unitId": unitId,
|
|
454
|
+
"missionId": missionId,
|
|
455
|
+
"timestamp": Date().timeIntervalSince1970
|
|
456
|
+
])
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
func onRefreshed(unitId: String?) {
|
|
460
|
+
// React Native로 이벤트 전송
|
|
461
|
+
module?.sendEvent(withName: "onMissionRefreshed", body: [
|
|
462
|
+
"unitId": unitId ?? self.unitId,
|
|
463
|
+
"timestamp": Date().timeIntervalSince1970
|
|
464
|
+
])
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let listener = MissionEventListenerImpl(module: self, unitId: unitId as String, missionId: missionId as String)
|
|
469
|
+
mission.setEventsListener(listener)
|
|
470
|
+
|
|
471
|
+
// iOS SDK가 ID 기반 클릭을 지원한다고 가정
|
|
472
|
+
DispatchQueue.main.async { [weak self] in
|
|
473
|
+
if let topVC = self?.getTopViewController() {
|
|
474
|
+
// iOS SDK의 네이티브 ID 기반 메서드 사용 (Android와 동일)
|
|
475
|
+
// 만약 이 메서드가 없다면 컴파일 오류가 발생할 것
|
|
476
|
+
mission.clickMission(missionId as String, from: topVC)
|
|
477
|
+
resolver([
|
|
478
|
+
"success": true,
|
|
479
|
+
"message": "Mission clicked"
|
|
480
|
+
])
|
|
481
|
+
} else {
|
|
482
|
+
rejecter("MISSION_ERROR", "No view controller available", nil)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@objc func claimReward(_ unitId: NSString,
|
|
488
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
489
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
490
|
+
// 인스턴스 자동 생성/재사용
|
|
491
|
+
let mission: AdchainMission = missionInstances[unitId as String] ?? {
|
|
492
|
+
let newMission = AdchainMission()
|
|
493
|
+
missionInstances[unitId as String] = newMission
|
|
494
|
+
return newMission
|
|
495
|
+
}()
|
|
496
|
+
|
|
497
|
+
DispatchQueue.main.async { [weak self] in
|
|
498
|
+
if let topVC = self?.getTopViewController() {
|
|
499
|
+
mission.onRewardButtonClicked(from: topVC)
|
|
500
|
+
resolver([
|
|
501
|
+
"success": true,
|
|
502
|
+
"message": "Reward claimed"
|
|
503
|
+
])
|
|
504
|
+
} else {
|
|
505
|
+
rejecter("MISSION_ERROR", "No view controller available", nil)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// MARK: - 5. Debug/Utility Methods (3개)
|
|
511
|
+
|
|
512
|
+
@objc func isInitialized(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
513
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
514
|
+
resolver(AdchainSdk.shared.isInitialized())
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
@objc func getUserId(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
518
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
519
|
+
let userId = AdchainSdk.shared.getCurrentUser()?.userId ?? ""
|
|
520
|
+
resolver(userId)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
@objc func getIFA(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
524
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
525
|
+
// SDK의 getAdvertisingId 메서드를 사용하여 IFA 가져오기
|
|
526
|
+
// SDK가 캐싱, 권한 처리, 에러 처리 등을 모두 관리함
|
|
527
|
+
AdchainSdk.shared.getAdvertisingId { advertisingId in
|
|
528
|
+
resolver(advertisingId)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// MARK: - 6. Banner (1개)
|
|
533
|
+
|
|
534
|
+
@objc func getBannerInfo(_ placementId: String,
|
|
535
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
536
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
537
|
+
AdchainSdk.shared.getBannerInfo(placementId: placementId) { result in
|
|
538
|
+
switch result {
|
|
539
|
+
case .success(let response):
|
|
540
|
+
resolver([
|
|
541
|
+
"success": response.success,
|
|
542
|
+
"imageUrl": response.imageUrl as Any,
|
|
543
|
+
"imageWidth": response.imageWidth as Any,
|
|
544
|
+
"imageHeight": response.imageHeight as Any,
|
|
545
|
+
"titleText": response.titleText as Any,
|
|
546
|
+
"linkType": response.linkType as Any,
|
|
547
|
+
"internalLinkUrl": response.internalLinkUrl as Any,
|
|
548
|
+
"externalLinkUrl": response.externalLinkUrl as Any
|
|
549
|
+
])
|
|
550
|
+
case .failure(let error):
|
|
551
|
+
rejecter("BANNER_ERROR", error.localizedDescription, error)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// MARK: - 7. Offerwall (3개)
|
|
557
|
+
|
|
558
|
+
@objc func openOfferwall(_ placementId: NSString?,
|
|
559
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
560
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
561
|
+
class OfferwallCallbackImpl: NSObject, OfferwallCallback {
|
|
562
|
+
let resolver: RCTPromiseResolveBlock
|
|
563
|
+
var hasResolved = false
|
|
564
|
+
|
|
565
|
+
init(resolver: @escaping RCTPromiseResolveBlock) {
|
|
566
|
+
self.resolver = resolver
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
func onOpened() {
|
|
570
|
+
if !hasResolved {
|
|
571
|
+
hasResolved = true
|
|
572
|
+
resolver([
|
|
573
|
+
"success": true,
|
|
574
|
+
"message": "Offerwall opened"
|
|
575
|
+
])
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
func onClosed() {
|
|
580
|
+
// 이미 resolve 되었으므로 무시
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
func onError(_ message: String) {
|
|
584
|
+
// 이미 resolve 되었으므로 무시
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
func onRewardEarned(_ amount: Int) {
|
|
588
|
+
// 이미 resolve 되었으므로 무시
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
class OfferwallEventCallbackImpl: NSObject, OfferwallEventCallback {
|
|
593
|
+
func onCustomEvent(eventType: String, payload: [String : Any]) {
|
|
594
|
+
print("[AdchainSdk] [WebView → App] Custom Event: type=\(eventType), payload=\(payload)")
|
|
595
|
+
// Sample 앱에서 처리하도록 이벤트만 전달 (Alert/Toast 표시 안 함)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
func onDataRequest(requestId: String, requestType: String, params: [String : Any]) -> [String : Any]? {
|
|
599
|
+
print("[AdchainSdk] [WebView → App] Data Request: type=\(requestType), params=\(params)")
|
|
600
|
+
|
|
601
|
+
// Return mock data based on request type
|
|
602
|
+
let response: [String: Any]?
|
|
603
|
+
switch requestType {
|
|
604
|
+
case "user_points":
|
|
605
|
+
response = ["points": 12345, "currency": "KRW"]
|
|
606
|
+
case "user_profile":
|
|
607
|
+
response = ["userId": "test_123", "nickname": "TestPlayer", "level": 42]
|
|
608
|
+
case "app_version":
|
|
609
|
+
response = ["version": "1.0.0", "buildNumber": 100]
|
|
610
|
+
default:
|
|
611
|
+
response = nil
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
print("[AdchainSdk] [App → WebView] Data Response: \(response ?? [:])")
|
|
615
|
+
return response
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
DispatchQueue.main.async { [weak self] in
|
|
620
|
+
if let topVC = self?.getTopViewController() {
|
|
621
|
+
let callback = OfferwallCallbackImpl(resolver: resolver)
|
|
622
|
+
let eventCallback = OfferwallEventCallbackImpl()
|
|
623
|
+
let finalPlacementId = (placementId as String?) ?? ""
|
|
624
|
+
AdchainSdk.shared.openOfferwall(
|
|
625
|
+
presentingViewController: topVC,
|
|
626
|
+
placementId: finalPlacementId,
|
|
627
|
+
callback: callback,
|
|
628
|
+
eventCallback: eventCallback
|
|
629
|
+
)
|
|
630
|
+
} else {
|
|
631
|
+
rejecter("OFFERWALL_ERROR", "No view controller available", nil)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
@objc func openOfferwallWithUrl(_ url: NSString,
|
|
637
|
+
placementId: NSString?,
|
|
638
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
639
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
640
|
+
class OfferwallCallbackImpl: NSObject, OfferwallCallback {
|
|
641
|
+
let resolver: RCTPromiseResolveBlock
|
|
642
|
+
var hasResolved = false
|
|
643
|
+
|
|
644
|
+
init(resolver: @escaping RCTPromiseResolveBlock) {
|
|
645
|
+
self.resolver = resolver
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
func onOpened() {
|
|
649
|
+
if !hasResolved {
|
|
650
|
+
hasResolved = true
|
|
651
|
+
resolver([
|
|
652
|
+
"success": true,
|
|
653
|
+
"message": "Offerwall opened with URL"
|
|
654
|
+
])
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
func onClosed() {
|
|
659
|
+
// 이미 resolve 되었으므로 무시
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
func onError(_ message: String) {
|
|
663
|
+
// 이미 resolve 되었으므로 무시
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
func onRewardEarned(_ amount: Int) {
|
|
667
|
+
// 이미 resolve 되었으므로 무시
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
class OfferwallEventCallbackImpl: NSObject, OfferwallEventCallback {
|
|
672
|
+
func onCustomEvent(eventType: String, payload: [String : Any]) {
|
|
673
|
+
print("[AdchainSdk] [WebView → App] Custom Event: type=\(eventType), payload=\(payload)")
|
|
674
|
+
// Sample 앱에서 처리하도록 이벤트만 전달 (Alert/Toast 표시 안 함)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
func onDataRequest(requestId: String, requestType: String, params: [String : Any]) -> [String : Any]? {
|
|
678
|
+
print("[AdchainSdk] [WebView → App] Data Request: type=\(requestType), params=\(params)")
|
|
679
|
+
|
|
680
|
+
// Return mock data based on request type
|
|
681
|
+
let response: [String: Any]?
|
|
682
|
+
switch requestType {
|
|
683
|
+
case "user_points":
|
|
684
|
+
response = ["points": 12345, "currency": "KRW"]
|
|
685
|
+
case "user_profile":
|
|
686
|
+
response = ["userId": "test_123", "nickname": "TestPlayer", "level": 42]
|
|
687
|
+
case "app_version":
|
|
688
|
+
response = ["version": "1.0.0", "buildNumber": 100]
|
|
689
|
+
default:
|
|
690
|
+
response = nil
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
print("[AdchainSdk] [App → WebView] Data Response: \(response ?? [:])")
|
|
694
|
+
return response
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
DispatchQueue.main.async { [weak self] in
|
|
699
|
+
if let topVC = self?.getTopViewController() {
|
|
700
|
+
let callback = OfferwallCallbackImpl(resolver: resolver)
|
|
701
|
+
let eventCallback = OfferwallEventCallbackImpl()
|
|
702
|
+
let finalPlacementId = (placementId as String?) ?? ""
|
|
703
|
+
AdchainSdk.shared.openOfferwallWithUrl(
|
|
704
|
+
url as String,
|
|
705
|
+
placementId: finalPlacementId,
|
|
706
|
+
presentingViewController: topVC,
|
|
707
|
+
callback: callback,
|
|
708
|
+
eventCallback: eventCallback
|
|
709
|
+
)
|
|
710
|
+
} else {
|
|
711
|
+
rejecter("OFFERWALL_ERROR", "No view controller available", nil)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
@objc func openExternalBrowser(_ url: NSString,
|
|
717
|
+
placementId: NSString?,
|
|
718
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
719
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
720
|
+
let finalPlacementId = (placementId as String?) ?? ""
|
|
721
|
+
let success = AdchainSdk.shared.openExternalBrowser(
|
|
722
|
+
url as String,
|
|
723
|
+
placementId: finalPlacementId
|
|
724
|
+
)
|
|
725
|
+
if success {
|
|
726
|
+
resolver([
|
|
727
|
+
"success": true,
|
|
728
|
+
"message": "External browser opened"
|
|
729
|
+
])
|
|
730
|
+
} else {
|
|
731
|
+
rejecter("BROWSER_ERROR", "Failed to open external browser", nil)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
@objc func openAdjoeOfferwall(_ placementId: NSString?,
|
|
736
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
737
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
738
|
+
class OfferwallCallbackImpl: NSObject, OfferwallCallback {
|
|
739
|
+
let resolver: RCTPromiseResolveBlock
|
|
740
|
+
var hasResolved = false
|
|
741
|
+
|
|
742
|
+
init(resolver: @escaping RCTPromiseResolveBlock) {
|
|
743
|
+
self.resolver = resolver
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
func onOpened() {
|
|
747
|
+
if !hasResolved {
|
|
748
|
+
hasResolved = true
|
|
749
|
+
resolver([
|
|
750
|
+
"success": true,
|
|
751
|
+
"message": "Adjoe Offerwall opened"
|
|
752
|
+
])
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
func onClosed() {
|
|
757
|
+
// 이미 resolve 되었으므로 무시
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
func onError(_ message: String) {
|
|
761
|
+
// 이미 resolve 되었으므로 무시
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
func onRewardEarned(_ amount: Int) {
|
|
765
|
+
// 이미 resolve 되었으므로 무시
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
DispatchQueue.main.async { [weak self] in
|
|
770
|
+
if let topVC = self?.getTopViewController() {
|
|
771
|
+
let callback = OfferwallCallbackImpl(resolver: resolver)
|
|
772
|
+
let finalPlacementId = (placementId as String?) ?? ""
|
|
773
|
+
AdchainSdk.shared.openAdjoeOfferwall(
|
|
774
|
+
presentingViewController: topVC,
|
|
775
|
+
placementId: finalPlacementId,
|
|
776
|
+
callback: callback
|
|
777
|
+
)
|
|
778
|
+
} else {
|
|
779
|
+
rejecter("ADJOE_ERROR", "No view controller available", nil)
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// MARK: - Objective-C Export
|
|
786
|
+
|
|
787
|
+
@objc(AdchainSdk)
|
|
788
|
+
extension AdchainSdkModule {
|
|
789
|
+
@objc override static func moduleName() -> String! {
|
|
790
|
+
return "AdchainSdk"
|
|
791
|
+
}
|
|
792
|
+
}
|