@ait-co/devtools 0.0.2 → 0.1.0

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.
@@ -1,511 +1,1087 @@
1
- import {
2
- Accuracy,
3
- Storage,
4
- aitState,
5
- createMockProxy,
6
- fetchAlbumPhotos,
7
- fetchContacts,
8
- generateHapticFeedback,
9
- getClipboardText,
10
- getCurrentLocation,
11
- getDefaultPlaceholderImages,
12
- getNetworkStatusByMode,
13
- getPermission,
14
- openCamera,
15
- openPermissionDialog,
16
- requestPermission,
17
- saveBase64Data,
18
- setClipboardText,
19
- startUpdateLocation
20
- } from "../chunk-6PPZTREF.js";
21
-
22
- // src/mock/auth/index.ts
1
+ //#region src/mock/proxy.ts
2
+ /**
3
+ * 미구현 API용 Proxy 트립와이어.
4
+ *
5
+ * 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
6
+ * 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
7
+ * mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
8
+ * 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
9
+ * 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
10
+ */
11
+ const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
12
+ function createMockProxy(moduleName, implementations) {
13
+ return new Proxy(implementations, { get(target, prop) {
14
+ if (typeof prop === "symbol") return void 0;
15
+ if (prop in target) return target[prop];
16
+ throw new Error(`[@ait-co/devtools] ${moduleName}.${prop} is not mocked. This API may exist in @apps-in-toss/web-framework, but devtools' mock does not cover it yet. Please file an issue: ${ISSUES_URL}`);
17
+ } });
18
+ }
19
+ //#endregion
20
+ //#region src/mock/state.ts
21
+ const DEFAULT_STATE = {
22
+ platform: "ios",
23
+ environment: "sandbox",
24
+ appVersion: "5.240.0",
25
+ locale: "ko-KR",
26
+ schemeUri: "/",
27
+ groupId: "mock-group-id",
28
+ deploymentId: "mock-deployment-id",
29
+ deviceId: "",
30
+ brand: {
31
+ displayName: "Mock App",
32
+ icon: "",
33
+ primaryColor: "#3182F6"
34
+ },
35
+ networkStatus: "WIFI",
36
+ permissions: {
37
+ clipboard: "allowed",
38
+ contacts: "allowed",
39
+ photos: "allowed",
40
+ geolocation: "allowed",
41
+ camera: "allowed",
42
+ microphone: "notDetermined"
43
+ },
44
+ location: {
45
+ coords: {
46
+ latitude: 37.5665,
47
+ longitude: 126.978,
48
+ altitude: 0,
49
+ accuracy: 10,
50
+ altitudeAccuracy: 0,
51
+ heading: 0
52
+ },
53
+ timestamp: Date.now(),
54
+ accessLocation: "FINE"
55
+ },
56
+ safeAreaInsets: {
57
+ top: 47,
58
+ bottom: 34,
59
+ left: 0,
60
+ right: 0
61
+ },
62
+ contacts: [{
63
+ name: "홍길동",
64
+ phoneNumber: "010-1234-5678"
65
+ }, {
66
+ name: "김토스",
67
+ phoneNumber: "010-9876-5432"
68
+ }],
69
+ iap: {
70
+ products: [{
71
+ sku: "mock-gem-100",
72
+ type: "CONSUMABLE",
73
+ displayName: "보석 100개",
74
+ displayAmount: "1,000원",
75
+ iconUrl: "",
76
+ description: "게임에서 사용할 수 있는 보석 100개"
77
+ }],
78
+ nextResult: "success",
79
+ pendingOrders: [],
80
+ completedOrders: []
81
+ },
82
+ payment: {
83
+ nextResult: "success",
84
+ failReason: ""
85
+ },
86
+ auth: {
87
+ isLoggedIn: true,
88
+ isTossLoginIntegrated: true,
89
+ userKeyHash: "mock-user-hash-abc123"
90
+ },
91
+ ads: {
92
+ isLoaded: false,
93
+ nextEvent: "loaded"
94
+ },
95
+ game: {
96
+ profile: {
97
+ nickname: "MockPlayer",
98
+ profileImageUri: ""
99
+ },
100
+ leaderboardScores: []
101
+ },
102
+ analyticsLog: [],
103
+ deviceModes: {
104
+ camera: "mock",
105
+ photos: "mock",
106
+ location: "mock",
107
+ network: "mock",
108
+ clipboard: "web"
109
+ },
110
+ mockData: {
111
+ images: [],
112
+ clipboardText: ""
113
+ },
114
+ panelEditable: true
115
+ };
116
+ function generateDeviceId() {
117
+ const stored = localStorage.getItem("__ait_device_id");
118
+ if (stored) return stored;
119
+ const id = crypto.randomUUID();
120
+ localStorage.setItem("__ait_device_id", id);
121
+ return id;
122
+ }
123
+ var AitStateManager = class {
124
+ _state;
125
+ _listeners = /* @__PURE__ */ new Set();
126
+ constructor() {
127
+ this._state = structuredClone(DEFAULT_STATE);
128
+ try {
129
+ this._state.deviceId = generateDeviceId();
130
+ } catch {
131
+ this._state.deviceId = `mock-device-${Math.random().toString(36).slice(2)}`;
132
+ }
133
+ }
134
+ get state() {
135
+ return this._state;
136
+ }
137
+ update(partial) {
138
+ this._state = {
139
+ ...this._state,
140
+ ...partial
141
+ };
142
+ this._notify();
143
+ }
144
+ /** 중첩 객체 업데이트용 */
145
+ patch(key, partial) {
146
+ const current = this._state[key];
147
+ if (typeof current === "object" && current !== null && !Array.isArray(current)) this._state = {
148
+ ...this._state,
149
+ [key]: {
150
+ ...current,
151
+ ...partial
152
+ }
153
+ };
154
+ else this._state = {
155
+ ...this._state,
156
+ [key]: partial
157
+ };
158
+ this._notify();
159
+ }
160
+ subscribe(listener) {
161
+ this._listeners.add(listener);
162
+ return () => this._listeners.delete(listener);
163
+ }
164
+ /** 분석 로그 추가 */
165
+ logAnalytics(entry) {
166
+ this._state = {
167
+ ...this._state,
168
+ analyticsLog: [...this._state.analyticsLog, {
169
+ ...entry,
170
+ timestamp: Date.now()
171
+ }]
172
+ };
173
+ this._notify();
174
+ }
175
+ /** 이벤트 트리거 (backEvent, homeEvent 등) */
176
+ trigger(event) {
177
+ window.dispatchEvent(new CustomEvent(`__ait:${event}`));
178
+ }
179
+ reset() {
180
+ const deviceId = this._state.deviceId;
181
+ this._state = {
182
+ ...structuredClone(DEFAULT_STATE),
183
+ deviceId
184
+ };
185
+ this._notify();
186
+ }
187
+ _notify() {
188
+ for (const listener of this._listeners) listener();
189
+ }
190
+ };
191
+ const aitState = new AitStateManager();
192
+ if (typeof window !== "undefined") window.__ait = aitState;
193
+ //#endregion
194
+ //#region src/mock/ads/index.ts
195
+ /**
196
+ * 광고 mock (GoogleAdMob, TossAds, FullScreenAd)
197
+ */
198
+ function withIsSupported(fn) {
199
+ fn.isSupported = () => true;
200
+ return fn;
201
+ }
202
+ const GoogleAdMob = createMockProxy("GoogleAdMob", {
203
+ loadAppsInTossAdMob: withIsSupported((args) => {
204
+ setTimeout(() => {
205
+ aitState.patch("ads", { isLoaded: true });
206
+ args.onEvent({
207
+ type: "loaded",
208
+ data: { adGroupId: args.options?.adGroupId }
209
+ });
210
+ }, 200);
211
+ return () => {};
212
+ }),
213
+ showAppsInTossAdMob: withIsSupported((args) => {
214
+ if (!aitState.state.ads.isLoaded) {
215
+ args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
216
+ return () => {};
217
+ }
218
+ setTimeout(() => args.onEvent({ type: "requested" }), 50);
219
+ setTimeout(() => args.onEvent({ type: "show" }), 100);
220
+ setTimeout(() => args.onEvent({ type: "impression" }), 150);
221
+ setTimeout(() => {
222
+ args.onEvent({
223
+ type: "userEarnedReward",
224
+ data: {
225
+ unitType: "coins",
226
+ unitAmount: 10
227
+ }
228
+ });
229
+ }, 1e3);
230
+ setTimeout(() => {
231
+ args.onEvent({ type: "dismissed" });
232
+ aitState.patch("ads", { isLoaded: false });
233
+ }, 1500);
234
+ return () => {};
235
+ }),
236
+ isAppsInTossAdMobLoaded: withIsSupported(async (_options) => aitState.state.ads.isLoaded)
237
+ });
238
+ const TossAds = createMockProxy("TossAds", {
239
+ initialize: withIsSupported((_options) => {
240
+ console.log("[@ait-co/devtools] TossAds.initialize (mock)");
241
+ }),
242
+ attach: withIsSupported((_adGroupId, target, _options) => {
243
+ const el = typeof target === "string" ? document.querySelector(target) : target;
244
+ if (el) {
245
+ const placeholder = document.createElement("div");
246
+ placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:16px;text-align:center;color:#666;font-size:14px;";
247
+ placeholder.textContent = "[@ait-co/devtools] TossAds Placeholder";
248
+ el.appendChild(placeholder);
249
+ }
250
+ }),
251
+ attachBanner: withIsSupported((_adGroupId, target, _options) => {
252
+ const el = typeof target === "string" ? document.querySelector(target) : target;
253
+ if (el) {
254
+ const placeholder = document.createElement("div");
255
+ placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:12px;text-align:center;color:#666;font-size:12px;";
256
+ placeholder.textContent = "[@ait-co/devtools] Banner Ad Placeholder";
257
+ el.appendChild(placeholder);
258
+ }
259
+ return { destroy: () => {} };
260
+ }),
261
+ destroy: withIsSupported((_slotId) => {}),
262
+ destroyAll: withIsSupported(() => {})
263
+ });
264
+ const loadFullScreenAd = withIsSupported((args) => {
265
+ setTimeout(() => {
266
+ aitState.patch("ads", { isLoaded: true });
267
+ args.onEvent({
268
+ type: "loaded",
269
+ data: { adGroupId: args.options?.adGroupId }
270
+ });
271
+ }, 200);
272
+ return () => {};
273
+ });
274
+ const showFullScreenAd = withIsSupported((args) => {
275
+ if (!aitState.state.ads.isLoaded) {
276
+ args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
277
+ return () => {};
278
+ }
279
+ setTimeout(() => args.onEvent({ type: "show" }), 100);
280
+ setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
281
+ return () => {};
282
+ });
283
+ //#endregion
284
+ //#region src/mock/analytics/index.ts
285
+ /**
286
+ * Analytics mock
287
+ */
288
+ const Analytics = {
289
+ screen: (params) => {
290
+ aitState.logAnalytics({
291
+ type: "screen",
292
+ params: params ?? {}
293
+ });
294
+ return Promise.resolve();
295
+ },
296
+ impression: (params) => {
297
+ aitState.logAnalytics({
298
+ type: "impression",
299
+ params: params ?? {}
300
+ });
301
+ return Promise.resolve();
302
+ },
303
+ click: (params) => {
304
+ aitState.logAnalytics({
305
+ type: "click",
306
+ params: params ?? {}
307
+ });
308
+ return Promise.resolve();
309
+ }
310
+ };
311
+ async function eventLog(params) {
312
+ aitState.logAnalytics({
313
+ type: params.log_type,
314
+ params: {
315
+ log_name: params.log_name,
316
+ ...params.params
317
+ }
318
+ });
319
+ }
320
+ //#endregion
321
+ //#region src/mock/auth/index.ts
322
+ /**
323
+ * 인증/로그인 mock
324
+ */
23
325
  async function appLogin() {
24
- return {
25
- authorizationCode: `mock-auth-${crypto.randomUUID()}`,
26
- referrer: aitState.state.environment === "toss" ? "DEFAULT" : "SANDBOX"
27
- };
326
+ return {
327
+ authorizationCode: `mock-auth-${crypto.randomUUID()}`,
328
+ referrer: aitState.state.environment === "toss" ? "DEFAULT" : "SANDBOX"
329
+ };
28
330
  }
29
331
  async function getIsTossLoginIntegratedService() {
30
- return aitState.state.auth.isTossLoginIntegrated;
332
+ return aitState.state.auth.isTossLoginIntegrated;
31
333
  }
32
334
  async function getUserKeyForGame() {
33
- if (!aitState.state.auth.userKeyHash) return void 0;
34
- return { hash: aitState.state.auth.userKeyHash, type: "HASH" };
335
+ if (!aitState.state.auth.userKeyHash) return void 0;
336
+ return {
337
+ hash: aitState.state.auth.userKeyHash,
338
+ type: "HASH"
339
+ };
35
340
  }
36
341
  async function appsInTossSignTossCert(_params) {
37
- console.log("[@ait-co/devtools] appsInTossSignTossCert called (no-op in mock)");
342
+ console.log("[@ait-co/devtools] appsInTossSignTossCert called (no-op in mock)");
38
343
  }
39
-
40
- // src/mock/navigation/index.ts
344
+ //#endregion
345
+ //#region src/mock/device/_helpers.ts
346
+ /**
347
+ * 디바이스 모듈 내부 공유 헬퍼
348
+ */
349
+ function generatePlaceholderImage(width, height, text, color) {
350
+ const canvas = document.createElement("canvas");
351
+ canvas.width = width;
352
+ canvas.height = height;
353
+ const ctx = canvas.getContext("2d");
354
+ if (!ctx) {
355
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect fill="${color}" width="${width}" height="${height}"/><text x="50%" y="50%" fill="white" font-size="16" text-anchor="middle" dominant-baseline="middle">${text}</text></svg>`;
356
+ return `data:image/svg+xml;base64,${btoa(svg)}`;
357
+ }
358
+ ctx.fillStyle = color;
359
+ ctx.fillRect(0, 0, width, height);
360
+ ctx.fillStyle = "white";
361
+ ctx.font = "16px sans-serif";
362
+ ctx.textAlign = "center";
363
+ ctx.textBaseline = "middle";
364
+ ctx.fillText(text, width / 2, height / 2);
365
+ return canvas.toDataURL("image/png");
366
+ }
367
+ const DEFAULT_PLACEHOLDERS = [
368
+ {
369
+ text: "Mock Photo 1",
370
+ color: "#3182F6"
371
+ },
372
+ {
373
+ text: "Mock Photo 2",
374
+ color: "#27ae60"
375
+ },
376
+ {
377
+ text: "Mock Photo 3",
378
+ color: "#e67e22"
379
+ }
380
+ ];
381
+ let cachedPlaceholders = null;
382
+ function getDefaultPlaceholderImages() {
383
+ if (!cachedPlaceholders) cachedPlaceholders = DEFAULT_PLACEHOLDERS.map((p) => generatePlaceholderImage(320, 240, p.text, p.color));
384
+ return [...cachedPlaceholders];
385
+ }
386
+ /** @internal device 모듈 내부 전용 */
387
+ function getMockImages() {
388
+ const images = aitState.state.mockData.images;
389
+ if (images.length > 0) return images;
390
+ return getDefaultPlaceholderImages();
391
+ }
392
+ const PROMPT_TIMEOUT_MS = 3e4;
393
+ /** @internal device 모듈 내부 전용 */
394
+ function waitForPromptResponse(type) {
395
+ return new Promise((resolve, reject) => {
396
+ const eventName = `__ait:prompt-response:${type}`;
397
+ const cancelName = "__ait:prompt-cancel";
398
+ function cleanup() {
399
+ clearTimeout(timer);
400
+ window.removeEventListener(eventName, handler);
401
+ window.removeEventListener(cancelName, cancelHandler);
402
+ }
403
+ const timer = setTimeout(() => {
404
+ cleanup();
405
+ const hint = !!document.querySelector(".ait-panel") ? "Please provide input via the DevTools panel." : "Is @ait-co/devtools/panel imported?";
406
+ reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] Prompt timeout for "${type}" after ${PROMPT_TIMEOUT_MS / 1e3}s. ${hint}`));
407
+ }, PROMPT_TIMEOUT_MS);
408
+ const handler = (e) => {
409
+ cleanup();
410
+ resolve(e.detail);
411
+ };
412
+ const cancelHandler = () => {
413
+ cleanup();
414
+ reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] Prompt cancelled for "${type}"`));
415
+ };
416
+ window.addEventListener(eventName, handler);
417
+ window.addEventListener(cancelName, cancelHandler);
418
+ window.dispatchEvent(new CustomEvent("__ait:prompt-request", { detail: { type } }));
419
+ });
420
+ }
421
+ //#endregion
422
+ //#region src/mock/permissions.ts
423
+ /**
424
+ * 권한 시스템 mock
425
+ * 각 디바이스 API (.getPermission, .openPermissionDialog)에 부착된다.
426
+ */
427
+ async function getPermission(name) {
428
+ return aitState.state.permissions[name];
429
+ }
430
+ async function openPermissionDialog(name) {
431
+ if (aitState.state.permissions[name] === "allowed") return "allowed";
432
+ aitState.patch("permissions", { [name]: "allowed" });
433
+ return "allowed";
434
+ }
435
+ async function requestPermission(permission) {
436
+ return openPermissionDialog(permission.name);
437
+ }
438
+ /** 권한이 필요한 함수에 .getPermission(), .openPermissionDialog()를 부착 */
439
+ function withPermission(fn, permissionName) {
440
+ const enhanced = fn;
441
+ enhanced.getPermission = () => getPermission(permissionName);
442
+ enhanced.openPermissionDialog = () => openPermissionDialog(permissionName);
443
+ return enhanced;
444
+ }
445
+ /** 권한 체크 후 denied면 에러 throw */
446
+ function checkPermission(name, fnName) {
447
+ if (aitState.state.permissions[name] === "denied") throw new Error(`[@ait-co/devtools] ${fnName}: Permission "${name}" is denied. Change it in the DevTools panel.`);
448
+ }
449
+ //#endregion
450
+ //#region src/mock/device/camera.ts
451
+ /**
452
+ * Camera & Album Photos mock
453
+ * mock/web/prompt 모드 지원
454
+ */
455
+ async function openCameraMock() {
456
+ const images = getMockImages();
457
+ return {
458
+ id: crypto.randomUUID(),
459
+ dataUri: images[0]
460
+ };
461
+ }
462
+ async function openCameraWeb() {
463
+ return new Promise((resolve, reject) => {
464
+ const input = document.createElement("input");
465
+ input.type = "file";
466
+ input.accept = "image/*";
467
+ input.capture = "environment";
468
+ let settled = false;
469
+ input.onchange = () => {
470
+ settled = true;
471
+ const file = input.files?.[0];
472
+ if (!file) {
473
+ reject(/* @__PURE__ */ new Error("No file selected"));
474
+ return;
475
+ }
476
+ const reader = new FileReader();
477
+ reader.onload = () => resolve({
478
+ id: crypto.randomUUID(),
479
+ dataUri: reader.result
480
+ });
481
+ reader.onerror = () => reject(/* @__PURE__ */ new Error("Failed to read file"));
482
+ reader.readAsDataURL(file);
483
+ };
484
+ const onFocus = () => {
485
+ setTimeout(() => {
486
+ if (!settled) reject(/* @__PURE__ */ new Error("File picker cancelled"));
487
+ window.removeEventListener("focus", onFocus);
488
+ }, 300);
489
+ };
490
+ window.addEventListener("focus", onFocus);
491
+ input.click();
492
+ });
493
+ }
494
+ async function openCameraPrompt() {
495
+ const dataUri = await waitForPromptResponse("camera");
496
+ return {
497
+ id: crypto.randomUUID(),
498
+ dataUri
499
+ };
500
+ }
501
+ const _openCamera = async (_options) => {
502
+ checkPermission("camera", "openCamera");
503
+ const mode = aitState.state.deviceModes.camera;
504
+ if (mode === "web") return openCameraWeb();
505
+ if (mode === "prompt") return openCameraPrompt();
506
+ return openCameraMock();
507
+ };
508
+ const openCamera = withPermission(_openCamera, "camera");
509
+ async function fetchAlbumPhotosMock(maxCount) {
510
+ return getMockImages().slice(0, maxCount).map((dataUri) => ({
511
+ id: crypto.randomUUID(),
512
+ dataUri
513
+ }));
514
+ }
515
+ async function fetchAlbumPhotosWeb(maxCount) {
516
+ return new Promise((resolve, reject) => {
517
+ const input = document.createElement("input");
518
+ input.type = "file";
519
+ input.accept = "image/*";
520
+ input.multiple = true;
521
+ let settled = false;
522
+ input.onchange = async () => {
523
+ settled = true;
524
+ const files = Array.from(input.files ?? []).slice(0, maxCount);
525
+ if (files.length === 0) {
526
+ reject(/* @__PURE__ */ new Error("No files selected"));
527
+ return;
528
+ }
529
+ resolve(await Promise.all(files.map((file) => new Promise((res, rej) => {
530
+ const reader = new FileReader();
531
+ reader.onload = () => res({
532
+ id: crypto.randomUUID(),
533
+ dataUri: reader.result
534
+ });
535
+ reader.onerror = () => rej(/* @__PURE__ */ new Error("Failed to read file"));
536
+ reader.readAsDataURL(file);
537
+ }))));
538
+ };
539
+ const onFocus = () => {
540
+ setTimeout(() => {
541
+ if (!settled) reject(/* @__PURE__ */ new Error("File picker cancelled"));
542
+ window.removeEventListener("focus", onFocus);
543
+ }, 300);
544
+ };
545
+ window.addEventListener("focus", onFocus);
546
+ input.click();
547
+ });
548
+ }
549
+ async function fetchAlbumPhotosPrompt(maxCount) {
550
+ return (await waitForPromptResponse("photos")).slice(0, maxCount).map((dataUri) => ({
551
+ id: crypto.randomUUID(),
552
+ dataUri
553
+ }));
554
+ }
555
+ const _fetchAlbumPhotos = async (options) => {
556
+ checkPermission("photos", "fetchAlbumPhotos");
557
+ const maxCount = options?.maxCount ?? 10;
558
+ const mode = aitState.state.deviceModes.photos;
559
+ if (mode === "web") return fetchAlbumPhotosWeb(maxCount);
560
+ if (mode === "prompt") return fetchAlbumPhotosPrompt(maxCount);
561
+ return fetchAlbumPhotosMock(maxCount);
562
+ };
563
+ const fetchAlbumPhotos = withPermission(_fetchAlbumPhotos, "photos");
564
+ //#endregion
565
+ //#region src/mock/device/clipboard.ts
566
+ /**
567
+ * Clipboard mock
568
+ * mock/web 모드 지원
569
+ */
570
+ const _getClipboardText = async () => {
571
+ checkPermission("clipboard", "getClipboardText");
572
+ if (aitState.state.deviceModes.clipboard === "mock") return aitState.state.mockData.clipboardText;
573
+ try {
574
+ return await navigator.clipboard.readText();
575
+ } catch {
576
+ return "";
577
+ }
578
+ };
579
+ const getClipboardText = withPermission(_getClipboardText, "clipboard");
580
+ const _setClipboardText = async (text) => {
581
+ checkPermission("clipboard", "setClipboardText");
582
+ if (aitState.state.deviceModes.clipboard === "mock") {
583
+ aitState.patch("mockData", { clipboardText: text });
584
+ return;
585
+ }
586
+ await navigator.clipboard.writeText(text);
587
+ };
588
+ const setClipboardText = withPermission(_setClipboardText, "clipboard");
589
+ //#endregion
590
+ //#region src/mock/device/contacts.ts
591
+ /**
592
+ * Contacts mock
593
+ */
594
+ const _fetchContacts = async (options) => {
595
+ checkPermission("contacts", "fetchContacts");
596
+ let contacts = aitState.state.contacts;
597
+ if (options.query?.contains) {
598
+ const q = options.query.contains.toLowerCase();
599
+ contacts = contacts.filter((c) => c.name.toLowerCase().includes(q) || c.phoneNumber.includes(q));
600
+ }
601
+ const sliced = contacts.slice(options.offset, options.offset + options.size);
602
+ const nextOffset = options.offset + options.size;
603
+ return {
604
+ result: sliced,
605
+ nextOffset: nextOffset < contacts.length ? nextOffset : null,
606
+ done: nextOffset >= contacts.length
607
+ };
608
+ };
609
+ const fetchContacts = withPermission(_fetchContacts, "contacts");
610
+ //#endregion
611
+ //#region src/mock/device/haptic.ts
612
+ /**
613
+ * Haptic Feedback & saveBase64Data mock
614
+ */
615
+ async function generateHapticFeedback(options) {
616
+ console.log(`[@ait-co/devtools] haptic: ${options.type}`);
617
+ aitState.logAnalytics({
618
+ type: "haptic",
619
+ params: { hapticType: options.type }
620
+ });
621
+ }
622
+ async function saveBase64Data(params) {
623
+ const a = document.createElement("a");
624
+ a.href = `data:${params.mimeType};base64,${params.data}`;
625
+ a.download = params.fileName;
626
+ a.click();
627
+ }
628
+ //#endregion
629
+ //#region src/mock/device/location.ts
630
+ /**
631
+ * Location mock (getCurrentLocation, startUpdateLocation)
632
+ * mock/web/prompt 모드 지원
633
+ */
634
+ var Accuracy = /* @__PURE__ */ function(Accuracy) {
635
+ Accuracy[Accuracy["Lowest"] = 1] = "Lowest";
636
+ Accuracy[Accuracy["Low"] = 2] = "Low";
637
+ Accuracy[Accuracy["Balanced"] = 3] = "Balanced";
638
+ Accuracy[Accuracy["High"] = 4] = "High";
639
+ Accuracy[Accuracy["Highest"] = 5] = "Highest";
640
+ Accuracy[Accuracy["BestForNavigation"] = 6] = "BestForNavigation";
641
+ return Accuracy;
642
+ }(Accuracy || {});
643
+ function buildLocation() {
644
+ return {
645
+ coords: { ...aitState.state.location.coords },
646
+ timestamp: Date.now(),
647
+ accessLocation: aitState.state.location.accessLocation
648
+ };
649
+ }
650
+ async function getCurrentLocationMock() {
651
+ return buildLocation();
652
+ }
653
+ async function getCurrentLocationWeb() {
654
+ return new Promise((resolve) => {
655
+ if (!navigator.geolocation) {
656
+ console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
657
+ resolve(buildLocation());
658
+ return;
659
+ }
660
+ navigator.geolocation.getCurrentPosition((pos) => {
661
+ resolve({
662
+ coords: {
663
+ latitude: pos.coords.latitude,
664
+ longitude: pos.coords.longitude,
665
+ altitude: pos.coords.altitude ?? 0,
666
+ accuracy: pos.coords.accuracy,
667
+ altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
668
+ heading: pos.coords.heading ?? 0
669
+ },
670
+ timestamp: pos.timestamp,
671
+ accessLocation: "FINE"
672
+ });
673
+ }, () => {
674
+ console.warn("[@ait-co/devtools] Geolocation failed, falling back to mock");
675
+ resolve(buildLocation());
676
+ });
677
+ });
678
+ }
679
+ async function getCurrentLocationPrompt() {
680
+ return waitForPromptResponse("location");
681
+ }
682
+ const _getCurrentLocation = async (_options) => {
683
+ checkPermission("geolocation", "getCurrentLocation");
684
+ const mode = aitState.state.deviceModes.location;
685
+ if (mode === "web") return getCurrentLocationWeb();
686
+ if (mode === "prompt") return getCurrentLocationPrompt();
687
+ return getCurrentLocationMock();
688
+ };
689
+ const getCurrentLocation = withPermission(_getCurrentLocation, "geolocation");
690
+ function startUpdateLocationMock(eventParams) {
691
+ const { onEvent, options } = eventParams;
692
+ const interval = Math.max(options.timeInterval, 500);
693
+ const id = setInterval(() => {
694
+ const loc = buildLocation();
695
+ loc.coords.latitude += (Math.random() - .5) * 1e-4;
696
+ loc.coords.longitude += (Math.random() - .5) * 1e-4;
697
+ onEvent(loc);
698
+ }, interval);
699
+ return () => clearInterval(id);
700
+ }
701
+ function startUpdateLocationWeb(eventParams) {
702
+ const { onEvent, onError } = eventParams;
703
+ if (!navigator.geolocation) {
704
+ console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
705
+ return startUpdateLocationMock(eventParams);
706
+ }
707
+ const watchId = navigator.geolocation.watchPosition((pos) => {
708
+ onEvent({
709
+ coords: {
710
+ latitude: pos.coords.latitude,
711
+ longitude: pos.coords.longitude,
712
+ altitude: pos.coords.altitude ?? 0,
713
+ accuracy: pos.coords.accuracy,
714
+ altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
715
+ heading: pos.coords.heading ?? 0
716
+ },
717
+ timestamp: pos.timestamp,
718
+ accessLocation: "FINE"
719
+ });
720
+ }, (err) => onError(err));
721
+ return () => navigator.geolocation.clearWatch(watchId);
722
+ }
723
+ function startUpdateLocationPrompt(eventParams) {
724
+ const { onEvent } = eventParams;
725
+ const handler = (e) => {
726
+ onEvent(e.detail);
727
+ };
728
+ window.addEventListener("__ait:prompt-response:location-update", handler);
729
+ window.dispatchEvent(new CustomEvent("__ait:prompt-request", { detail: { type: "location-update" } }));
730
+ return () => window.removeEventListener("__ait:prompt-response:location-update", handler);
731
+ }
732
+ const _startUpdateLocation = (eventParams) => {
733
+ const mode = aitState.state.deviceModes.location;
734
+ if (mode === "web") return startUpdateLocationWeb(eventParams);
735
+ if (mode === "prompt") return startUpdateLocationPrompt(eventParams);
736
+ return startUpdateLocationMock(eventParams);
737
+ };
738
+ const startUpdateLocation = withPermission(_startUpdateLocation, "geolocation");
739
+ //#endregion
740
+ //#region src/mock/device/network.ts
741
+ /**
742
+ * Network Status mock (mode-aware helper)
743
+ * navigation 모듈에서 사용. circular dep 방지를 위해 device에 위치.
744
+ */
745
+ /**
746
+ * Web mode: uses navigator.connection.effectiveType (4g/3g/2g) and navigator.onLine.
747
+ * Limitations: WIFI, 5G, WWAN cannot be detected via the Network Information API.
748
+ * Falls back to state-based value when effectiveType is unavailable.
749
+ */
750
+ function getNetworkStatusByMode() {
751
+ const mode = aitState.state.deviceModes.network;
752
+ if (mode === "mock") return null;
753
+ if (mode === "web") {
754
+ if (!navigator.onLine) return "OFFLINE";
755
+ const conn = navigator.connection;
756
+ if (conn?.effectiveType) return {
757
+ "4g": "4G",
758
+ "3g": "3G",
759
+ "2g": "2G",
760
+ "slow-2g": "2G"
761
+ }[conn.effectiveType] ?? "UNKNOWN";
762
+ return aitState.state.networkStatus;
763
+ }
764
+ return null;
765
+ }
766
+ //#endregion
767
+ //#region src/mock/device/storage.ts
768
+ /**
769
+ * Storage mock
770
+ * localStorage에 `__ait_storage:` prefix로 저장하여 앱 자체 localStorage와 분리
771
+ */
772
+ const Storage = createMockProxy("Storage", {
773
+ getItem: async (key) => {
774
+ return localStorage.getItem(`__ait_storage:${key}`);
775
+ },
776
+ setItem: async (key, value) => {
777
+ localStorage.setItem(`__ait_storage:${key}`, value);
778
+ },
779
+ removeItem: async (key) => {
780
+ localStorage.removeItem(`__ait_storage:${key}`);
781
+ },
782
+ clearItems: async () => {
783
+ const keys = Object.keys(localStorage).filter((k) => k.startsWith("__ait_storage:"));
784
+ for (const k of keys) localStorage.removeItem(k);
785
+ }
786
+ });
787
+ //#endregion
788
+ //#region src/mock/game/index.ts
789
+ /**
790
+ * 게임/프로모션 mock
791
+ */
792
+ async function grantPromotionReward(params) {
793
+ console.log("[@ait-co/devtools] grantPromotionReward:", params.params);
794
+ return { key: `mock-reward-${Date.now()}` };
795
+ }
796
+ async function grantPromotionRewardForGame(params) {
797
+ console.log("[@ait-co/devtools] grantPromotionRewardForGame:", params.params);
798
+ return { key: `mock-reward-${Date.now()}` };
799
+ }
800
+ async function submitGameCenterLeaderBoardScore(params) {
801
+ aitState.patch("game", { leaderboardScores: [...aitState.state.game.leaderboardScores, {
802
+ score: params.score,
803
+ timestamp: Date.now()
804
+ }] });
805
+ return { statusCode: "SUCCESS" };
806
+ }
807
+ async function getGameCenterGameProfile() {
808
+ const profile = aitState.state.game.profile;
809
+ if (!profile) return { statusCode: "PROFILE_NOT_FOUND" };
810
+ return {
811
+ statusCode: "SUCCESS",
812
+ nickname: profile.nickname,
813
+ profileImageUri: profile.profileImageUri
814
+ };
815
+ }
816
+ async function openGameCenterLeaderboard() {
817
+ console.log("[@ait-co/devtools] openGameCenterLeaderboard (no-op in browser)");
818
+ }
819
+ function contactsViral(params) {
820
+ setTimeout(() => {
821
+ params.onEvent({
822
+ type: "close",
823
+ data: {
824
+ closeReason: "noReward",
825
+ sentRewardsCount: 0
826
+ }
827
+ });
828
+ }, 500);
829
+ return () => {};
830
+ }
831
+ //#endregion
832
+ //#region src/mock/iap/index.ts
833
+ /**
834
+ * IAP (인앱결제) mock
835
+ */
836
+ let orderCounter = 0;
837
+ function generateOrderId() {
838
+ return `mock-order-${++orderCounter}-${Date.now()}`;
839
+ }
840
+ function buildOrderResult(sku) {
841
+ const product = aitState.state.iap.products.find((p) => p.sku === sku);
842
+ const amountStr = product?.displayAmount?.replace(/[^0-9]/g, "") ?? "1000";
843
+ return {
844
+ orderId: generateOrderId(),
845
+ displayName: product?.displayName ?? "Mock Product",
846
+ displayAmount: product?.displayAmount ?? "1,000원",
847
+ amount: parseInt(amountStr, 10) || 1e3,
848
+ currency: "KRW",
849
+ fraction: 0,
850
+ miniAppIconUrl: product?.iconUrl || null
851
+ };
852
+ }
853
+ async function handlePurchase(sku, processProductGrant, onEvent, onError) {
854
+ const nextResult = aitState.state.iap.nextResult;
855
+ await new Promise((r) => setTimeout(r, 300));
856
+ if (nextResult !== "success") {
857
+ onError({ code: nextResult });
858
+ return;
859
+ }
860
+ const result = buildOrderResult(sku);
861
+ try {
862
+ if (!await processProductGrant({ orderId: result.orderId })) {
863
+ onError({ code: "PRODUCT_NOT_GRANTED_BY_PARTNER" });
864
+ return;
865
+ }
866
+ } catch (e) {
867
+ onError(e);
868
+ return;
869
+ }
870
+ aitState.patch("iap", { completedOrders: [...aitState.state.iap.completedOrders, {
871
+ orderId: result.orderId,
872
+ sku,
873
+ status: "COMPLETED",
874
+ date: (/* @__PURE__ */ new Date()).toISOString()
875
+ }] });
876
+ await onEvent({
877
+ type: "success",
878
+ data: result
879
+ });
880
+ }
881
+ const IAP = createMockProxy("IAP", {
882
+ createOneTimePurchaseOrder(params) {
883
+ handlePurchase(params.options.sku ?? params.options.productId ?? "", params.options.processProductGrant, params.onEvent, params.onError).catch((e) => console.error("[@ait-co/devtools] IAP unexpected error:", e));
884
+ return () => {};
885
+ },
886
+ createSubscriptionPurchaseOrder(params) {
887
+ handlePurchase(params.options.sku, params.options.processProductGrant, params.onEvent, params.onError).catch((e) => console.error("[@ait-co/devtools] IAP unexpected error:", e));
888
+ return () => {};
889
+ },
890
+ async getProductItemList() {
891
+ return { products: aitState.state.iap.products.map((p) => ({
892
+ ...p,
893
+ ...p.type === "SUBSCRIPTION" ? { renewalCycle: p.renewalCycle ?? "MONTHLY" } : {}
894
+ })) };
895
+ },
896
+ async getPendingOrders() {
897
+ return { orders: [...aitState.state.iap.pendingOrders] };
898
+ },
899
+ async getCompletedOrRefundedOrders() {
900
+ return {
901
+ hasNext: false,
902
+ nextKey: null,
903
+ orders: [...aitState.state.iap.completedOrders]
904
+ };
905
+ },
906
+ async completeProductGrant(args) {
907
+ const idx = aitState.state.iap.pendingOrders.findIndex((o) => o.orderId === args.params.orderId);
908
+ if (idx !== -1) {
909
+ const order = aitState.state.iap.pendingOrders[idx];
910
+ const pendingOrders = aitState.state.iap.pendingOrders.filter((_, i) => i !== idx);
911
+ const completedOrders = [...aitState.state.iap.completedOrders, {
912
+ orderId: order.orderId,
913
+ sku: order.sku,
914
+ status: "COMPLETED",
915
+ date: (/* @__PURE__ */ new Date()).toISOString()
916
+ }];
917
+ aitState.patch("iap", {
918
+ pendingOrders,
919
+ completedOrders
920
+ });
921
+ }
922
+ return true;
923
+ },
924
+ async getSubscriptionInfo(_args) {
925
+ return { subscription: {
926
+ catalogId: 1,
927
+ status: "ACTIVE",
928
+ expiresAt: new Date(Date.now() + 720 * 60 * 60 * 1e3).toISOString(),
929
+ isAutoRenew: true,
930
+ gracePeriodExpiresAt: null,
931
+ isAccessible: true
932
+ } };
933
+ }
934
+ });
935
+ async function checkoutPayment(options) {
936
+ const { nextResult, failReason } = aitState.state.payment;
937
+ console.log("[@ait-co/devtools] checkoutPayment:", options.params.payToken);
938
+ await new Promise((r) => setTimeout(r, 300));
939
+ if (nextResult === "success") return { success: true };
940
+ return {
941
+ success: false,
942
+ reason: failReason || "Mock payment failed"
943
+ };
944
+ }
945
+ //#endregion
946
+ //#region src/mock/navigation/index.ts
947
+ /**
948
+ * 화면/네비게이션/이벤트 mock
949
+ */
41
950
  async function closeView() {
42
- console.log("[@ait-co/devtools] closeView called");
43
- window.history.back();
951
+ console.log("[@ait-co/devtools] closeView called");
952
+ window.history.back();
44
953
  }
45
954
  async function openURL(url) {
46
- console.log("[@ait-co/devtools] openURL:", url);
47
- window.open(url, "_blank");
955
+ console.log("[@ait-co/devtools] openURL:", url);
956
+ window.open(url, "_blank");
48
957
  }
49
958
  async function share(message) {
50
- if (navigator.share) {
51
- await navigator.share({ text: message.message });
52
- return;
53
- }
54
- console.log("[@ait-co/devtools] share:", message.message);
959
+ if (navigator.share) {
960
+ await navigator.share({ text: message.message });
961
+ return;
962
+ }
963
+ console.log("[@ait-co/devtools] share:", message.message);
55
964
  }
56
965
  async function getTossShareLink(path, _ogImageUrl) {
57
- return `https://toss.im/share/mock${path}`;
966
+ return `https://toss.im/share/mock${path}`;
58
967
  }
59
968
  async function setIosSwipeGestureEnabled(_options) {
60
- console.log("[@ait-co/devtools] setIosSwipeGestureEnabled:", _options.isEnabled);
969
+ console.log("[@ait-co/devtools] setIosSwipeGestureEnabled:", _options.isEnabled);
61
970
  }
62
971
  async function setDeviceOrientation(_options) {
63
- console.log("[@ait-co/devtools] setDeviceOrientation:", _options.type);
972
+ console.log("[@ait-co/devtools] setDeviceOrientation:", _options.type);
64
973
  }
65
974
  async function setScreenAwakeMode(options) {
66
- console.log("[@ait-co/devtools] setScreenAwakeMode:", options.enabled);
67
- return { enabled: options.enabled };
975
+ console.log("[@ait-co/devtools] setScreenAwakeMode:", options.enabled);
976
+ return { enabled: options.enabled };
68
977
  }
69
978
  async function setSecureScreen(options) {
70
- console.log("[@ait-co/devtools] setSecureScreen:", options.enabled);
71
- return { enabled: options.enabled };
979
+ console.log("[@ait-co/devtools] setSecureScreen:", options.enabled);
980
+ return { enabled: options.enabled };
72
981
  }
73
982
  async function requestReview() {
74
- console.log("[@ait-co/devtools] requestReview called");
983
+ console.log("[@ait-co/devtools] requestReview called");
75
984
  }
76
985
  requestReview.isSupported = () => true;
77
986
  function getPlatformOS() {
78
- return aitState.state.platform;
987
+ return aitState.state.platform;
79
988
  }
80
989
  function getOperationalEnvironment() {
81
- return aitState.state.environment;
990
+ return aitState.state.environment;
82
991
  }
83
992
  function getTossAppVersion() {
84
- return aitState.state.appVersion;
993
+ return aitState.state.appVersion;
85
994
  }
86
995
  function isMinVersionSupported(minVersions) {
87
- const platform = aitState.state.platform;
88
- const required = platform === "ios" ? minVersions.ios : minVersions.android;
89
- if (required === "always") return true;
90
- if (required === "never") return false;
91
- const current = aitState.state.appVersion.split(".").map(Number);
92
- const min = required.split(".").map(Number);
93
- for (let i = 0; i < 3; i++) {
94
- if ((current[i] ?? 0) > (min[i] ?? 0)) return true;
95
- if ((current[i] ?? 0) < (min[i] ?? 0)) return false;
96
- }
97
- return true;
996
+ const required = aitState.state.platform === "ios" ? minVersions.ios : minVersions.android;
997
+ if (required === "always") return true;
998
+ if (required === "never") return false;
999
+ const current = aitState.state.appVersion.split(".").map(Number);
1000
+ const min = required.split(".").map(Number);
1001
+ for (let i = 0; i < 3; i++) {
1002
+ if ((current[i] ?? 0) > (min[i] ?? 0)) return true;
1003
+ if ((current[i] ?? 0) < (min[i] ?? 0)) return false;
1004
+ }
1005
+ return true;
98
1006
  }
99
1007
  function getSchemeUri() {
100
- return aitState.state.schemeUri || window.location.pathname;
1008
+ return aitState.state.schemeUri || window.location.pathname;
101
1009
  }
102
1010
  function getLocale() {
103
- return aitState.state.locale;
1011
+ return aitState.state.locale;
104
1012
  }
105
1013
  function getDeviceId() {
106
- return aitState.state.deviceId;
1014
+ return aitState.state.deviceId;
107
1015
  }
108
1016
  function getGroupId() {
109
- return aitState.state.groupId;
1017
+ return aitState.state.groupId;
110
1018
  }
111
1019
  async function getNetworkStatus() {
112
- const modeResult = getNetworkStatusByMode();
113
- if (modeResult) return modeResult;
114
- return aitState.state.networkStatus;
1020
+ const modeResult = getNetworkStatusByMode();
1021
+ if (modeResult) return modeResult;
1022
+ return aitState.state.networkStatus;
115
1023
  }
116
1024
  async function getServerTime() {
117
- return Date.now();
1025
+ return Date.now();
118
1026
  }
119
1027
  getServerTime.isSupported = () => true;
120
- var graniteEvent = {
121
- addEventListener(event, { onEvent, onError }) {
122
- const handler = () => {
123
- try {
124
- onEvent();
125
- } catch (e) {
126
- onError?.(e instanceof Error ? e : new Error(String(e)));
127
- }
128
- };
129
- window.addEventListener(`__ait:${event}`, handler);
130
- return () => window.removeEventListener(`__ait:${event}`, handler);
131
- }
132
- };
133
- var appsInTossEvent = {
134
- addEventListener(_event, _handlers) {
135
- return () => {
136
- };
137
- }
138
- };
139
- var tdsEvent = {
140
- addEventListener(event, { onEvent }) {
141
- const handler = (e) => {
142
- const detail = e.detail;
143
- onEvent(detail);
144
- };
145
- window.addEventListener(`__ait:${event}`, handler);
146
- return () => window.removeEventListener(`__ait:${event}`, handler);
147
- }
148
- };
1028
+ const graniteEvent = { addEventListener(event, { onEvent, onError }) {
1029
+ const handler = () => {
1030
+ try {
1031
+ onEvent();
1032
+ } catch (e) {
1033
+ onError?.(e instanceof Error ? e : new Error(String(e)));
1034
+ }
1035
+ };
1036
+ window.addEventListener(`__ait:${event}`, handler);
1037
+ return () => window.removeEventListener(`__ait:${event}`, handler);
1038
+ } };
1039
+ const appsInTossEvent = { addEventListener(_event, _handlers) {
1040
+ return () => {};
1041
+ } };
1042
+ const tdsEvent = { addEventListener(event, { onEvent }) {
1043
+ const handler = (e) => {
1044
+ const detail = e.detail;
1045
+ onEvent(detail);
1046
+ };
1047
+ window.addEventListener(`__ait:${event}`, handler);
1048
+ return () => window.removeEventListener(`__ait:${event}`, handler);
1049
+ } };
149
1050
  function onVisibilityChangedByTransparentServiceWeb(eventParams) {
150
- const handler = () => eventParams.onEvent(!document.hidden);
151
- document.addEventListener("visibilitychange", handler);
152
- return () => document.removeEventListener("visibilitychange", handler);
1051
+ const handler = () => eventParams.onEvent(!document.hidden);
1052
+ document.addEventListener("visibilitychange", handler);
1053
+ return () => document.removeEventListener("visibilitychange", handler);
153
1054
  }
154
- var env = {
155
- getDeploymentId: () => aitState.state.deploymentId
156
- };
1055
+ const env = { getDeploymentId: () => aitState.state.deploymentId };
157
1056
  function getAppsInTossGlobals() {
158
- return {
159
- deploymentId: aitState.state.deploymentId,
160
- brandDisplayName: aitState.state.brand.displayName,
161
- brandIcon: aitState.state.brand.icon,
162
- brandPrimaryColor: aitState.state.brand.primaryColor
163
- };
164
- }
165
- var SafeAreaInsets = {
166
- get: () => ({ ...aitState.state.safeAreaInsets }),
167
- // NOTE: aitState.subscribe에 위임하므로 safeAreaInsets 상태 변경에도 콜백이 호출된다.
168
- // 실제 SDK는 insets 변경 시에만 호출되지만, mock에서는 간소화를 위해 필터링하지 않는다.
169
- subscribe: ({ onEvent }) => {
170
- return aitState.subscribe(() => onEvent({ ...aitState.state.safeAreaInsets }));
171
- }
1057
+ return {
1058
+ deploymentId: aitState.state.deploymentId,
1059
+ brandDisplayName: aitState.state.brand.displayName,
1060
+ brandIcon: aitState.state.brand.icon,
1061
+ brandPrimaryColor: aitState.state.brand.primaryColor
1062
+ };
1063
+ }
1064
+ const SafeAreaInsets = {
1065
+ get: () => ({ ...aitState.state.safeAreaInsets }),
1066
+ subscribe: ({ onEvent }) => {
1067
+ return aitState.subscribe(() => onEvent({ ...aitState.state.safeAreaInsets }));
1068
+ }
172
1069
  };
1070
+ /** @deprecated */
173
1071
  function getSafeAreaInsets() {
174
- return aitState.state.safeAreaInsets.top;
1072
+ return aitState.state.safeAreaInsets.top;
175
1073
  }
176
-
177
- // src/mock/iap/index.ts
178
- var orderCounter = 0;
179
- function generateOrderId() {
180
- return `mock-order-${++orderCounter}-${Date.now()}`;
181
- }
182
- function buildOrderResult(sku) {
183
- const product = aitState.state.iap.products.find((p) => p.sku === sku);
184
- const amountStr = product?.displayAmount?.replace(/[^0-9]/g, "") ?? "1000";
185
- return {
186
- orderId: generateOrderId(),
187
- displayName: product?.displayName ?? "Mock Product",
188
- displayAmount: product?.displayAmount ?? "1,000\uC6D0",
189
- amount: parseInt(amountStr, 10) || 1e3,
190
- currency: "KRW",
191
- fraction: 0,
192
- miniAppIconUrl: product?.iconUrl || null
193
- };
194
- }
195
- async function handlePurchase(sku, processProductGrant, onEvent, onError) {
196
- const nextResult = aitState.state.iap.nextResult;
197
- await new Promise((r) => setTimeout(r, 300));
198
- if (nextResult !== "success") {
199
- onError({ code: nextResult });
200
- return;
201
- }
202
- const result = buildOrderResult(sku);
203
- try {
204
- const granted = await processProductGrant({ orderId: result.orderId });
205
- if (!granted) {
206
- onError({ code: "PRODUCT_NOT_GRANTED_BY_PARTNER" });
207
- return;
208
- }
209
- } catch (e) {
210
- onError(e);
211
- return;
212
- }
213
- aitState.patch("iap", {
214
- completedOrders: [...aitState.state.iap.completedOrders, {
215
- orderId: result.orderId,
216
- sku,
217
- status: "COMPLETED",
218
- date: (/* @__PURE__ */ new Date()).toISOString()
219
- }]
220
- });
221
- await onEvent({ type: "success", data: result });
222
- }
223
- var IAP = createMockProxy("IAP", {
224
- // 반환되는 cancel 함수는 mock에서는 no-op이다 (실제 SDK는 결제 UI를 닫음)
225
- createOneTimePurchaseOrder(params) {
226
- const sku = params.options.sku ?? params.options.productId ?? "";
227
- handlePurchase(sku, params.options.processProductGrant, params.onEvent, params.onError).catch((e) => console.error("[@ait-co/devtools] IAP unexpected error:", e));
228
- return () => {
229
- };
230
- },
231
- createSubscriptionPurchaseOrder(params) {
232
- handlePurchase(params.options.sku, params.options.processProductGrant, params.onEvent, params.onError).catch((e) => console.error("[@ait-co/devtools] IAP unexpected error:", e));
233
- return () => {
234
- };
235
- },
236
- async getProductItemList() {
237
- return {
238
- products: aitState.state.iap.products.map((p) => ({
239
- ...p,
240
- ...p.type === "SUBSCRIPTION" ? { renewalCycle: p.renewalCycle ?? "MONTHLY" } : {}
241
- }))
242
- };
243
- },
244
- async getPendingOrders() {
245
- return { orders: [...aitState.state.iap.pendingOrders] };
246
- },
247
- async getCompletedOrRefundedOrders() {
248
- return {
249
- hasNext: false,
250
- nextKey: null,
251
- orders: [...aitState.state.iap.completedOrders]
252
- };
253
- },
254
- async completeProductGrant(args) {
255
- const idx = aitState.state.iap.pendingOrders.findIndex((o) => o.orderId === args.params.orderId);
256
- if (idx !== -1) {
257
- const order = aitState.state.iap.pendingOrders[idx];
258
- const pendingOrders = aitState.state.iap.pendingOrders.filter((_, i) => i !== idx);
259
- const completedOrders = [...aitState.state.iap.completedOrders, {
260
- orderId: order.orderId,
261
- sku: order.sku,
262
- status: "COMPLETED",
263
- date: (/* @__PURE__ */ new Date()).toISOString()
264
- }];
265
- aitState.patch("iap", { pendingOrders, completedOrders });
266
- }
267
- return true;
268
- },
269
- async getSubscriptionInfo(_args) {
270
- return {
271
- subscription: {
272
- catalogId: 1,
273
- status: "ACTIVE",
274
- expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
275
- isAutoRenew: true,
276
- gracePeriodExpiresAt: null,
277
- isAccessible: true
278
- }
279
- };
280
- }
281
- });
282
- async function checkoutPayment(options) {
283
- const { nextResult, failReason } = aitState.state.payment;
284
- console.log("[@ait-co/devtools] checkoutPayment:", options.params.payToken);
285
- await new Promise((r) => setTimeout(r, 300));
286
- if (nextResult === "success") {
287
- return { success: true };
288
- }
289
- return { success: false, reason: failReason || "Mock payment failed" };
290
- }
291
-
292
- // src/mock/ads/index.ts
293
- function withIsSupported(fn) {
294
- fn.isSupported = () => true;
295
- return fn;
296
- }
297
- var GoogleAdMob = createMockProxy("GoogleAdMob", {
298
- loadAppsInTossAdMob: withIsSupported((args) => {
299
- setTimeout(() => {
300
- aitState.patch("ads", { isLoaded: true });
301
- args.onEvent({ type: "loaded", data: { adGroupId: args.options?.adGroupId } });
302
- }, 200);
303
- return () => {
304
- };
305
- }),
306
- showAppsInTossAdMob: withIsSupported((args) => {
307
- if (!aitState.state.ads.isLoaded) {
308
- args.onError(new Error("Ad not loaded"));
309
- return () => {
310
- };
311
- }
312
- setTimeout(() => args.onEvent({ type: "requested" }), 50);
313
- setTimeout(() => args.onEvent({ type: "show" }), 100);
314
- setTimeout(() => args.onEvent({ type: "impression" }), 150);
315
- setTimeout(() => {
316
- args.onEvent({ type: "userEarnedReward", data: { unitType: "coins", unitAmount: 10 } });
317
- }, 1e3);
318
- setTimeout(() => {
319
- args.onEvent({ type: "dismissed" });
320
- aitState.patch("ads", { isLoaded: false });
321
- }, 1500);
322
- return () => {
323
- };
324
- }),
325
- isAppsInTossAdMobLoaded: withIsSupported(
326
- async (_options) => aitState.state.ads.isLoaded
327
- )
328
- });
329
- var TossAds = createMockProxy("TossAds", {
330
- initialize: withIsSupported((_options) => {
331
- console.log("[@ait-co/devtools] TossAds.initialize (mock)");
332
- }),
333
- attach: withIsSupported((_adGroupId, target, _options) => {
334
- const el = typeof target === "string" ? document.querySelector(target) : target;
335
- if (el) {
336
- const placeholder = document.createElement("div");
337
- placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:16px;text-align:center;color:#666;font-size:14px;";
338
- placeholder.textContent = "[@ait-co/devtools] TossAds Placeholder";
339
- el.appendChild(placeholder);
340
- }
341
- }),
342
- attachBanner: withIsSupported((_adGroupId, target, _options) => {
343
- const el = typeof target === "string" ? document.querySelector(target) : target;
344
- if (el) {
345
- const placeholder = document.createElement("div");
346
- placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:12px;text-align:center;color:#666;font-size:12px;";
347
- placeholder.textContent = "[@ait-co/devtools] Banner Ad Placeholder";
348
- el.appendChild(placeholder);
349
- }
350
- return { destroy: () => {
351
- } };
352
- }),
353
- destroy: withIsSupported((_slotId) => {
354
- }),
355
- destroyAll: withIsSupported(() => {
356
- })
357
- });
358
- var loadFullScreenAd = withIsSupported((args) => {
359
- setTimeout(() => {
360
- aitState.patch("ads", { isLoaded: true });
361
- args.onEvent({ type: "loaded", data: { adGroupId: args.options?.adGroupId } });
362
- }, 200);
363
- return () => {
364
- };
365
- });
366
- var showFullScreenAd = withIsSupported((args) => {
367
- if (!aitState.state.ads.isLoaded) {
368
- args.onError(new Error("Ad not loaded"));
369
- return () => {
370
- };
371
- }
372
- setTimeout(() => args.onEvent({ type: "show" }), 100);
373
- setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
374
- return () => {
375
- };
376
- });
377
-
378
- // src/mock/game/index.ts
379
- async function grantPromotionReward(params) {
380
- console.log("[@ait-co/devtools] grantPromotionReward:", params.params);
381
- return { key: `mock-reward-${Date.now()}` };
382
- }
383
- async function grantPromotionRewardForGame(params) {
384
- console.log("[@ait-co/devtools] grantPromotionRewardForGame:", params.params);
385
- return { key: `mock-reward-${Date.now()}` };
386
- }
387
- async function submitGameCenterLeaderBoardScore(params) {
388
- aitState.patch("game", {
389
- leaderboardScores: [...aitState.state.game.leaderboardScores, { score: params.score, timestamp: Date.now() }]
390
- });
391
- return { statusCode: "SUCCESS" };
392
- }
393
- async function getGameCenterGameProfile() {
394
- const profile = aitState.state.game.profile;
395
- if (!profile) return { statusCode: "PROFILE_NOT_FOUND" };
396
- return {
397
- statusCode: "SUCCESS",
398
- nickname: profile.nickname,
399
- profileImageUri: profile.profileImageUri
400
- };
401
- }
402
- async function openGameCenterLeaderboard() {
403
- console.log("[@ait-co/devtools] openGameCenterLeaderboard (no-op in browser)");
404
- }
405
- function contactsViral(params) {
406
- setTimeout(() => {
407
- params.onEvent({
408
- type: "close",
409
- data: {
410
- closeReason: "noReward",
411
- sentRewardsCount: 0
412
- }
413
- });
414
- }, 500);
415
- return () => {
416
- };
417
- }
418
-
419
- // src/mock/analytics/index.ts
420
- var Analytics = {
421
- screen: (params) => {
422
- aitState.logAnalytics({ type: "screen", params: params ?? {} });
423
- return Promise.resolve();
424
- },
425
- impression: (params) => {
426
- aitState.logAnalytics({ type: "impression", params: params ?? {} });
427
- return Promise.resolve();
428
- },
429
- click: (params) => {
430
- aitState.logAnalytics({ type: "click", params: params ?? {} });
431
- return Promise.resolve();
432
- }
1074
+ //#endregion
1075
+ //#region src/mock/partner/index.ts
1076
+ const partner = {
1077
+ async addAccessoryButton(options) {
1078
+ console.log("[@ait-co/devtools] partner.addAccessoryButton:", options);
1079
+ },
1080
+ async removeAccessoryButton() {
1081
+ console.log("[@ait-co/devtools] partner.removeAccessoryButton");
1082
+ }
433
1083
  };
434
- async function eventLog(params) {
435
- aitState.logAnalytics({ type: params.log_type, params: { log_name: params.log_name, ...params.params } });
436
- }
1084
+ //#endregion
1085
+ export { Accuracy, Analytics, GoogleAdMob, IAP, SafeAreaInsets, Storage, TossAds, aitState, appLogin, appsInTossEvent, appsInTossSignTossCert, checkoutPayment, closeView, contactsViral, env, eventLog, fetchAlbumPhotos, fetchContacts, generateHapticFeedback, getAppsInTossGlobals, getClipboardText, getCurrentLocation, getDefaultPlaceholderImages, getDeviceId, getGameCenterGameProfile, getGroupId, getIsTossLoginIntegratedService, getLocale, getNetworkStatus, getOperationalEnvironment, getPermission, getPlatformOS, getSafeAreaInsets, getSchemeUri, getServerTime, getTossAppVersion, getTossShareLink, getUserKeyForGame, graniteEvent, grantPromotionReward, grantPromotionRewardForGame, isMinVersionSupported, loadFullScreenAd, onVisibilityChangedByTransparentServiceWeb, openCamera, openGameCenterLeaderboard, openPermissionDialog, openURL, partner, requestPermission, requestReview, saveBase64Data, setClipboardText, setDeviceOrientation, setIosSwipeGestureEnabled, setScreenAwakeMode, setSecureScreen, share, showFullScreenAd, startUpdateLocation, submitGameCenterLeaderBoardScore, tdsEvent };
437
1086
 
438
- // src/mock/partner/index.ts
439
- var partner = {
440
- async addAccessoryButton(options) {
441
- console.log("[@ait-co/devtools] partner.addAccessoryButton:", options);
442
- },
443
- async removeAccessoryButton() {
444
- console.log("[@ait-co/devtools] partner.removeAccessoryButton");
445
- }
446
- };
447
- export {
448
- Accuracy,
449
- Analytics,
450
- GoogleAdMob,
451
- IAP,
452
- SafeAreaInsets,
453
- Storage,
454
- TossAds,
455
- aitState,
456
- appLogin,
457
- appsInTossEvent,
458
- appsInTossSignTossCert,
459
- checkoutPayment,
460
- closeView,
461
- contactsViral,
462
- env,
463
- eventLog,
464
- fetchAlbumPhotos,
465
- fetchContacts,
466
- generateHapticFeedback,
467
- getAppsInTossGlobals,
468
- getClipboardText,
469
- getCurrentLocation,
470
- getDefaultPlaceholderImages,
471
- getDeviceId,
472
- getGameCenterGameProfile,
473
- getGroupId,
474
- getIsTossLoginIntegratedService,
475
- getLocale,
476
- getNetworkStatus,
477
- getOperationalEnvironment,
478
- getPermission,
479
- getPlatformOS,
480
- getSafeAreaInsets,
481
- getSchemeUri,
482
- getServerTime,
483
- getTossAppVersion,
484
- getTossShareLink,
485
- getUserKeyForGame,
486
- graniteEvent,
487
- grantPromotionReward,
488
- grantPromotionRewardForGame,
489
- isMinVersionSupported,
490
- loadFullScreenAd,
491
- onVisibilityChangedByTransparentServiceWeb,
492
- openCamera,
493
- openGameCenterLeaderboard,
494
- openPermissionDialog,
495
- openURL,
496
- partner,
497
- requestPermission,
498
- requestReview,
499
- saveBase64Data,
500
- setClipboardText,
501
- setDeviceOrientation,
502
- setIosSwipeGestureEnabled,
503
- setScreenAwakeMode,
504
- setSecureScreen,
505
- share,
506
- showFullScreenAd,
507
- startUpdateLocation,
508
- submitGameCenterLeaderBoardScore,
509
- tdsEvent
510
- };
511
1087
  //# sourceMappingURL=index.js.map