@ait-co/devtools 0.0.3 → 0.1.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.
@@ -1,3 +1,22 @@
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
1
20
  //#region src/mock/state.ts
2
21
  const DEFAULT_STATE = {
3
22
  platform: "ios",
@@ -109,7 +128,7 @@ var AitStateManager = class {
109
128
  try {
110
129
  this._state.deviceId = generateDeviceId();
111
130
  } catch {
112
- this._state.deviceId = "mock-device-" + Math.random().toString(36).slice(2);
131
+ this._state.deviceId = `mock-device-${Math.random().toString(36).slice(2)}`;
113
132
  }
114
133
  }
115
134
  get state() {
@@ -172,6 +191,133 @@ var AitStateManager = class {
172
191
  const aitState = new AitStateManager();
173
192
  if (typeof window !== "undefined") window.__ait = aitState;
174
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
175
321
  //#region src/mock/auth/index.ts
176
322
  /**
177
323
  * 인증/로그인 mock
@@ -207,7 +353,7 @@ function generatePlaceholderImage(width, height, text, color) {
207
353
  const ctx = canvas.getContext("2d");
208
354
  if (!ctx) {
209
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>`;
210
- return "data:image/svg+xml;base64," + btoa(svg);
356
+ return `data:image/svg+xml;base64,${btoa(svg)}`;
211
357
  }
212
358
  ctx.fillStyle = color;
213
359
  ctx.fillRect(0, 0, width, height);
@@ -247,7 +393,7 @@ const PROMPT_TIMEOUT_MS = 3e4;
247
393
  /** @internal device 모듈 내부 전용 */
248
394
  function waitForPromptResponse(type) {
249
395
  return new Promise((resolve, reject) => {
250
- const eventName = "__ait:prompt-response:" + type;
396
+ const eventName = `__ait:prompt-response:${type}`;
251
397
  const cancelName = "__ait:prompt-cancel";
252
398
  function cleanup() {
253
399
  clearTimeout(timer);
@@ -273,45 +419,6 @@ function waitForPromptResponse(type) {
273
419
  });
274
420
  }
275
421
  //#endregion
276
- //#region src/mock/proxy.ts
277
- /**
278
- * 미구현 API용 Proxy 트립와이어.
279
- *
280
- * 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
281
- * 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
282
- * mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
283
- * 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
284
- * 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
285
- */
286
- const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
287
- function createMockProxy(moduleName, implementations) {
288
- return new Proxy(implementations, { get(target, prop) {
289
- if (typeof prop === "symbol") return void 0;
290
- if (prop in target) return target[prop];
291
- 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}`);
292
- } });
293
- }
294
- //#endregion
295
- //#region src/mock/device/storage.ts
296
- /**
297
- * Storage mock
298
- * localStorage에 `__ait_storage:` prefix로 저장하여 앱 자체 localStorage와 분리
299
- */
300
- const Storage = createMockProxy("Storage", {
301
- getItem: async (key) => {
302
- return localStorage.getItem(`__ait_storage:${key}`);
303
- },
304
- setItem: async (key, value) => {
305
- localStorage.setItem(`__ait_storage:${key}`, value);
306
- },
307
- removeItem: async (key) => {
308
- localStorage.removeItem(`__ait_storage:${key}`);
309
- },
310
- clearItems: async () => {
311
- Object.keys(localStorage).filter((k) => k.startsWith("__ait_storage:")).forEach((k) => localStorage.removeItem(k));
312
- }
313
- });
314
- //#endregion
315
422
  //#region src/mock/permissions.ts
316
423
  /**
317
424
  * 권한 시스템 mock
@@ -340,147 +447,36 @@ function checkPermission(name, fnName) {
340
447
  if (aitState.state.permissions[name] === "denied") throw new Error(`[@ait-co/devtools] ${fnName}: Permission "${name}" is denied. Change it in the DevTools panel.`);
341
448
  }
342
449
  //#endregion
343
- //#region src/mock/device/location.ts
450
+ //#region src/mock/device/camera.ts
344
451
  /**
345
- * Location mock (getCurrentLocation, startUpdateLocation)
452
+ * Camera & Album Photos mock
346
453
  * mock/web/prompt 모드 지원
347
454
  */
348
- var Accuracy = /* @__PURE__ */ function(Accuracy) {
349
- Accuracy[Accuracy["Lowest"] = 1] = "Lowest";
350
- Accuracy[Accuracy["Low"] = 2] = "Low";
351
- Accuracy[Accuracy["Balanced"] = 3] = "Balanced";
352
- Accuracy[Accuracy["High"] = 4] = "High";
353
- Accuracy[Accuracy["Highest"] = 5] = "Highest";
354
- Accuracy[Accuracy["BestForNavigation"] = 6] = "BestForNavigation";
355
- return Accuracy;
356
- }(Accuracy || {});
357
- function buildLocation() {
455
+ async function openCameraMock() {
456
+ const images = getMockImages();
358
457
  return {
359
- coords: { ...aitState.state.location.coords },
360
- timestamp: Date.now(),
361
- accessLocation: aitState.state.location.accessLocation
458
+ id: crypto.randomUUID(),
459
+ dataUri: images[0]
362
460
  };
363
461
  }
364
- async function getCurrentLocationMock() {
365
- return buildLocation();
366
- }
367
- async function getCurrentLocationWeb() {
368
- return new Promise((resolve) => {
369
- if (!navigator.geolocation) {
370
- console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
371
- resolve(buildLocation());
372
- return;
373
- }
374
- navigator.geolocation.getCurrentPosition((pos) => {
375
- resolve({
376
- coords: {
377
- latitude: pos.coords.latitude,
378
- longitude: pos.coords.longitude,
379
- altitude: pos.coords.altitude ?? 0,
380
- accuracy: pos.coords.accuracy,
381
- altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
382
- heading: pos.coords.heading ?? 0
383
- },
384
- timestamp: pos.timestamp,
385
- accessLocation: "FINE"
386
- });
387
- }, () => {
388
- console.warn("[@ait-co/devtools] Geolocation failed, falling back to mock");
389
- resolve(buildLocation());
390
- });
391
- });
392
- }
393
- async function getCurrentLocationPrompt() {
394
- return waitForPromptResponse("location");
395
- }
396
- const _getCurrentLocation = async (_options) => {
397
- checkPermission("geolocation", "getCurrentLocation");
398
- const mode = aitState.state.deviceModes.location;
399
- if (mode === "web") return getCurrentLocationWeb();
400
- if (mode === "prompt") return getCurrentLocationPrompt();
401
- return getCurrentLocationMock();
402
- };
403
- const getCurrentLocation = withPermission(_getCurrentLocation, "geolocation");
404
- function startUpdateLocationMock(eventParams) {
405
- const { onEvent, options } = eventParams;
406
- const interval = Math.max(options.timeInterval, 500);
407
- const id = setInterval(() => {
408
- const loc = buildLocation();
409
- loc.coords.latitude += (Math.random() - .5) * 1e-4;
410
- loc.coords.longitude += (Math.random() - .5) * 1e-4;
411
- onEvent(loc);
412
- }, interval);
413
- return () => clearInterval(id);
414
- }
415
- function startUpdateLocationWeb(eventParams) {
416
- const { onEvent, onError } = eventParams;
417
- if (!navigator.geolocation) {
418
- console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
419
- return startUpdateLocationMock(eventParams);
420
- }
421
- const watchId = navigator.geolocation.watchPosition((pos) => {
422
- onEvent({
423
- coords: {
424
- latitude: pos.coords.latitude,
425
- longitude: pos.coords.longitude,
426
- altitude: pos.coords.altitude ?? 0,
427
- accuracy: pos.coords.accuracy,
428
- altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
429
- heading: pos.coords.heading ?? 0
430
- },
431
- timestamp: pos.timestamp,
432
- accessLocation: "FINE"
433
- });
434
- }, (err) => onError(err));
435
- return () => navigator.geolocation.clearWatch(watchId);
436
- }
437
- function startUpdateLocationPrompt(eventParams) {
438
- const { onEvent } = eventParams;
439
- const handler = (e) => {
440
- onEvent(e.detail);
441
- };
442
- window.addEventListener("__ait:prompt-response:location-update", handler);
443
- window.dispatchEvent(new CustomEvent("__ait:prompt-request", { detail: { type: "location-update" } }));
444
- return () => window.removeEventListener("__ait:prompt-response:location-update", handler);
445
- }
446
- const _startUpdateLocation = (eventParams) => {
447
- const mode = aitState.state.deviceModes.location;
448
- if (mode === "web") return startUpdateLocationWeb(eventParams);
449
- if (mode === "prompt") return startUpdateLocationPrompt(eventParams);
450
- return startUpdateLocationMock(eventParams);
451
- };
452
- const startUpdateLocation = withPermission(_startUpdateLocation, "geolocation");
453
- //#endregion
454
- //#region src/mock/device/camera.ts
455
- /**
456
- * Camera & Album Photos mock
457
- * mock/web/prompt 모드 지원
458
- */
459
- async function openCameraMock() {
460
- const images = getMockImages();
461
- return {
462
- id: crypto.randomUUID(),
463
- dataUri: images[0]
464
- };
465
- }
466
- async function openCameraWeb() {
467
- return new Promise((resolve, reject) => {
468
- const input = document.createElement("input");
469
- input.type = "file";
470
- input.accept = "image/*";
471
- input.capture = "environment";
472
- let settled = false;
473
- input.onchange = () => {
474
- settled = true;
475
- const file = input.files?.[0];
476
- if (!file) {
477
- reject(/* @__PURE__ */ new Error("No file selected"));
478
- return;
479
- }
480
- const reader = new FileReader();
481
- reader.onload = () => resolve({
482
- id: crypto.randomUUID(),
483
- dataUri: reader.result
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
484
480
  });
485
481
  reader.onerror = () => reject(/* @__PURE__ */ new Error("Failed to read file"));
486
482
  reader.readAsDataURL(file);
@@ -630,6 +626,117 @@ async function saveBase64Data(params) {
630
626
  a.click();
631
627
  }
632
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
633
740
  //#region src/mock/device/network.ts
634
741
  /**
635
742
  * Network Status mock (mode-aware helper)
@@ -657,133 +764,69 @@ function getNetworkStatusByMode() {
657
764
  return null;
658
765
  }
659
766
  //#endregion
660
- //#region src/mock/navigation/index.ts
767
+ //#region src/mock/device/storage.ts
661
768
  /**
662
- * 화면/네비게이션/이벤트 mock
769
+ * Storage mock
770
+ * localStorage에 `__ait_storage:` prefix로 저장하여 앱 자체 localStorage와 분리
663
771
  */
664
- async function closeView() {
665
- console.log("[@ait-co/devtools] closeView called");
666
- window.history.back();
667
- }
668
- async function openURL(url) {
669
- console.log("[@ait-co/devtools] openURL:", url);
670
- window.open(url, "_blank");
671
- }
672
- async function share(message) {
673
- if (navigator.share) {
674
- await navigator.share({ text: message.message });
675
- return;
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);
676
785
  }
677
- console.log("[@ait-co/devtools] share:", message.message);
678
- }
679
- async function getTossShareLink(path, _ogImageUrl) {
680
- return `https://toss.im/share/mock${path}`;
681
- }
682
- async function setIosSwipeGestureEnabled(_options) {
683
- console.log("[@ait-co/devtools] setIosSwipeGestureEnabled:", _options.isEnabled);
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()}` };
684
795
  }
685
- async function setDeviceOrientation(_options) {
686
- console.log("[@ait-co/devtools] setDeviceOrientation:", _options.type);
796
+ async function grantPromotionRewardForGame(params) {
797
+ console.log("[@ait-co/devtools] grantPromotionRewardForGame:", params.params);
798
+ return { key: `mock-reward-${Date.now()}` };
687
799
  }
688
- async function setScreenAwakeMode(options) {
689
- console.log("[@ait-co/devtools] setScreenAwakeMode:", options.enabled);
690
- return { enabled: options.enabled };
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" };
691
806
  }
692
- async function setSecureScreen(options) {
693
- console.log("[@ait-co/devtools] setSecureScreen:", options.enabled);
694
- return { enabled: options.enabled };
695
- }
696
- async function requestReview() {
697
- console.log("[@ait-co/devtools] requestReview called");
698
- }
699
- requestReview.isSupported = () => true;
700
- function getPlatformOS() {
701
- return aitState.state.platform;
702
- }
703
- function getOperationalEnvironment() {
704
- return aitState.state.environment;
705
- }
706
- function getTossAppVersion() {
707
- return aitState.state.appVersion;
708
- }
709
- function isMinVersionSupported(minVersions) {
710
- const required = aitState.state.platform === "ios" ? minVersions.ios : minVersions.android;
711
- if (required === "always") return true;
712
- if (required === "never") return false;
713
- const current = aitState.state.appVersion.split(".").map(Number);
714
- const min = required.split(".").map(Number);
715
- for (let i = 0; i < 3; i++) {
716
- if ((current[i] ?? 0) > (min[i] ?? 0)) return true;
717
- if ((current[i] ?? 0) < (min[i] ?? 0)) return false;
718
- }
719
- return true;
720
- }
721
- function getSchemeUri() {
722
- return aitState.state.schemeUri || window.location.pathname;
723
- }
724
- function getLocale() {
725
- return aitState.state.locale;
726
- }
727
- function getDeviceId() {
728
- return aitState.state.deviceId;
729
- }
730
- function getGroupId() {
731
- return aitState.state.groupId;
732
- }
733
- async function getNetworkStatus() {
734
- const modeResult = getNetworkStatusByMode();
735
- if (modeResult) return modeResult;
736
- return aitState.state.networkStatus;
737
- }
738
- async function getServerTime() {
739
- return Date.now();
740
- }
741
- getServerTime.isSupported = () => true;
742
- const graniteEvent = { addEventListener(event, { onEvent, onError }) {
743
- const handler = () => {
744
- try {
745
- onEvent();
746
- } catch (e) {
747
- onError?.(e instanceof Error ? e : new Error(String(e)));
748
- }
749
- };
750
- window.addEventListener(`__ait:${event}`, handler);
751
- return () => window.removeEventListener(`__ait:${event}`, handler);
752
- } };
753
- const appsInTossEvent = { addEventListener(_event, _handlers) {
754
- return () => {};
755
- } };
756
- const tdsEvent = { addEventListener(event, { onEvent }) {
757
- const handler = (e) => {
758
- const detail = e.detail;
759
- onEvent(detail);
760
- };
761
- window.addEventListener(`__ait:${event}`, handler);
762
- return () => window.removeEventListener(`__ait:${event}`, handler);
763
- } };
764
- function onVisibilityChangedByTransparentServiceWeb(eventParams) {
765
- const handler = () => eventParams.onEvent(!document.hidden);
766
- document.addEventListener("visibilitychange", handler);
767
- return () => document.removeEventListener("visibilitychange", handler);
768
- }
769
- const env = { getDeploymentId: () => aitState.state.deploymentId };
770
- function getAppsInTossGlobals() {
807
+ async function getGameCenterGameProfile() {
808
+ const profile = aitState.state.game.profile;
809
+ if (!profile) return { statusCode: "PROFILE_NOT_FOUND" };
771
810
  return {
772
- deploymentId: aitState.state.deploymentId,
773
- brandDisplayName: aitState.state.brand.displayName,
774
- brandIcon: aitState.state.brand.icon,
775
- brandPrimaryColor: aitState.state.brand.primaryColor
811
+ statusCode: "SUCCESS",
812
+ nickname: profile.nickname,
813
+ profileImageUri: profile.profileImageUri
776
814
  };
777
815
  }
778
- const SafeAreaInsets = {
779
- get: () => ({ ...aitState.state.safeAreaInsets }),
780
- subscribe: ({ onEvent }) => {
781
- return aitState.subscribe(() => onEvent({ ...aitState.state.safeAreaInsets }));
782
- }
783
- };
784
- /** @deprecated */
785
- function getSafeAreaInsets() {
786
- return aitState.state.safeAreaInsets.top;
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 () => {};
787
830
  }
788
831
  //#endregion
789
832
  //#region src/mock/iap/index.ts
@@ -900,175 +943,133 @@ async function checkoutPayment(options) {
900
943
  };
901
944
  }
902
945
  //#endregion
903
- //#region src/mock/ads/index.ts
946
+ //#region src/mock/navigation/index.ts
904
947
  /**
905
- * 광고 mock (GoogleAdMob, TossAds, FullScreenAd)
948
+ * 화면/네비게이션/이벤트 mock
906
949
  */
907
- function withIsSupported(fn) {
908
- fn.isSupported = () => true;
909
- return fn;
950
+ async function closeView() {
951
+ console.log("[@ait-co/devtools] closeView called");
952
+ window.history.back();
910
953
  }
911
- const GoogleAdMob = createMockProxy("GoogleAdMob", {
912
- loadAppsInTossAdMob: withIsSupported((args) => {
913
- setTimeout(() => {
914
- aitState.patch("ads", { isLoaded: true });
915
- args.onEvent({
916
- type: "loaded",
917
- data: { adGroupId: args.options?.adGroupId }
918
- });
919
- }, 200);
920
- return () => {};
921
- }),
922
- showAppsInTossAdMob: withIsSupported((args) => {
923
- if (!aitState.state.ads.isLoaded) {
924
- args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
925
- return () => {};
926
- }
927
- setTimeout(() => args.onEvent({ type: "requested" }), 50);
928
- setTimeout(() => args.onEvent({ type: "show" }), 100);
929
- setTimeout(() => args.onEvent({ type: "impression" }), 150);
930
- setTimeout(() => {
931
- args.onEvent({
932
- type: "userEarnedReward",
933
- data: {
934
- unitType: "coins",
935
- unitAmount: 10
936
- }
937
- });
938
- }, 1e3);
939
- setTimeout(() => {
940
- args.onEvent({ type: "dismissed" });
941
- aitState.patch("ads", { isLoaded: false });
942
- }, 1500);
943
- return () => {};
944
- }),
945
- isAppsInTossAdMobLoaded: withIsSupported(async (_options) => aitState.state.ads.isLoaded)
946
- });
947
- const TossAds = createMockProxy("TossAds", {
948
- initialize: withIsSupported((_options) => {
949
- console.log("[@ait-co/devtools] TossAds.initialize (mock)");
950
- }),
951
- attach: withIsSupported((_adGroupId, target, _options) => {
952
- const el = typeof target === "string" ? document.querySelector(target) : target;
953
- if (el) {
954
- const placeholder = document.createElement("div");
955
- placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:16px;text-align:center;color:#666;font-size:14px;";
956
- placeholder.textContent = "[@ait-co/devtools] TossAds Placeholder";
957
- el.appendChild(placeholder);
958
- }
959
- }),
960
- attachBanner: withIsSupported((_adGroupId, target, _options) => {
961
- const el = typeof target === "string" ? document.querySelector(target) : target;
962
- if (el) {
963
- const placeholder = document.createElement("div");
964
- placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:12px;text-align:center;color:#666;font-size:12px;";
965
- placeholder.textContent = "[@ait-co/devtools] Banner Ad Placeholder";
966
- el.appendChild(placeholder);
967
- }
968
- return { destroy: () => {} };
969
- }),
970
- destroy: withIsSupported((_slotId) => {}),
971
- destroyAll: withIsSupported(() => {})
972
- });
973
- const loadFullScreenAd = withIsSupported((args) => {
974
- setTimeout(() => {
975
- aitState.patch("ads", { isLoaded: true });
976
- args.onEvent({
977
- type: "loaded",
978
- data: { adGroupId: args.options?.adGroupId }
979
- });
980
- }, 200);
981
- return () => {};
982
- });
983
- const showFullScreenAd = withIsSupported((args) => {
984
- if (!aitState.state.ads.isLoaded) {
985
- args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
986
- return () => {};
954
+ async function openURL(url) {
955
+ console.log("[@ait-co/devtools] openURL:", url);
956
+ window.open(url, "_blank");
957
+ }
958
+ async function share(message) {
959
+ if (navigator.share) {
960
+ await navigator.share({ text: message.message });
961
+ return;
987
962
  }
988
- setTimeout(() => args.onEvent({ type: "show" }), 100);
989
- setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
990
- return () => {};
991
- });
992
- //#endregion
993
- //#region src/mock/game/index.ts
994
- /**
995
- * 게임/프로모션 mock
996
- */
997
- async function grantPromotionReward(params) {
998
- console.log("[@ait-co/devtools] grantPromotionReward:", params.params);
999
- return { key: `mock-reward-${Date.now()}` };
963
+ console.log("[@ait-co/devtools] share:", message.message);
1000
964
  }
1001
- async function grantPromotionRewardForGame(params) {
1002
- console.log("[@ait-co/devtools] grantPromotionRewardForGame:", params.params);
1003
- return { key: `mock-reward-${Date.now()}` };
965
+ async function getTossShareLink(path, _ogImageUrl) {
966
+ return `https://toss.im/share/mock${path}`;
1004
967
  }
1005
- async function submitGameCenterLeaderBoardScore(params) {
1006
- aitState.patch("game", { leaderboardScores: [...aitState.state.game.leaderboardScores, {
1007
- score: params.score,
1008
- timestamp: Date.now()
1009
- }] });
1010
- return { statusCode: "SUCCESS" };
968
+ async function setIosSwipeGestureEnabled(_options) {
969
+ console.log("[@ait-co/devtools] setIosSwipeGestureEnabled:", _options.isEnabled);
1011
970
  }
1012
- async function getGameCenterGameProfile() {
1013
- const profile = aitState.state.game.profile;
1014
- if (!profile) return { statusCode: "PROFILE_NOT_FOUND" };
1015
- return {
1016
- statusCode: "SUCCESS",
1017
- nickname: profile.nickname,
1018
- profileImageUri: profile.profileImageUri
1019
- };
971
+ async function setDeviceOrientation(_options) {
972
+ console.log("[@ait-co/devtools] setDeviceOrientation:", _options.type);
1020
973
  }
1021
- async function openGameCenterLeaderboard() {
1022
- console.log("[@ait-co/devtools] openGameCenterLeaderboard (no-op in browser)");
974
+ async function setScreenAwakeMode(options) {
975
+ console.log("[@ait-co/devtools] setScreenAwakeMode:", options.enabled);
976
+ return { enabled: options.enabled };
1023
977
  }
1024
- function contactsViral(params) {
1025
- setTimeout(() => {
1026
- params.onEvent({
1027
- type: "close",
1028
- data: {
1029
- closeReason: "noReward",
1030
- sentRewardsCount: 0
1031
- }
1032
- });
1033
- }, 500);
978
+ async function setSecureScreen(options) {
979
+ console.log("[@ait-co/devtools] setSecureScreen:", options.enabled);
980
+ return { enabled: options.enabled };
981
+ }
982
+ async function requestReview() {
983
+ console.log("[@ait-co/devtools] requestReview called");
984
+ }
985
+ requestReview.isSupported = () => true;
986
+ function getPlatformOS() {
987
+ return aitState.state.platform;
988
+ }
989
+ function getOperationalEnvironment() {
990
+ return aitState.state.environment;
991
+ }
992
+ function getTossAppVersion() {
993
+ return aitState.state.appVersion;
994
+ }
995
+ function isMinVersionSupported(minVersions) {
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;
1006
+ }
1007
+ function getSchemeUri() {
1008
+ return aitState.state.schemeUri || window.location.pathname;
1009
+ }
1010
+ function getLocale() {
1011
+ return aitState.state.locale;
1012
+ }
1013
+ function getDeviceId() {
1014
+ return aitState.state.deviceId;
1015
+ }
1016
+ function getGroupId() {
1017
+ return aitState.state.groupId;
1018
+ }
1019
+ async function getNetworkStatus() {
1020
+ const modeResult = getNetworkStatusByMode();
1021
+ if (modeResult) return modeResult;
1022
+ return aitState.state.networkStatus;
1023
+ }
1024
+ async function getServerTime() {
1025
+ return Date.now();
1026
+ }
1027
+ getServerTime.isSupported = () => true;
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) {
1034
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
+ } };
1050
+ function onVisibilityChangedByTransparentServiceWeb(eventParams) {
1051
+ const handler = () => eventParams.onEvent(!document.hidden);
1052
+ document.addEventListener("visibilitychange", handler);
1053
+ return () => document.removeEventListener("visibilitychange", handler);
1035
1054
  }
1036
- //#endregion
1037
- //#region src/mock/analytics/index.ts
1038
- /**
1039
- * Analytics mock
1040
- */
1041
- const Analytics = {
1042
- screen: (params) => {
1043
- aitState.logAnalytics({
1044
- type: "screen",
1045
- params: params ?? {}
1046
- });
1047
- return Promise.resolve();
1048
- },
1049
- impression: (params) => {
1050
- aitState.logAnalytics({
1051
- type: "impression",
1052
- params: params ?? {}
1053
- });
1054
- return Promise.resolve();
1055
- },
1056
- click: (params) => {
1057
- aitState.logAnalytics({
1058
- type: "click",
1059
- params: params ?? {}
1060
- });
1061
- return Promise.resolve();
1055
+ const env = { getDeploymentId: () => aitState.state.deploymentId };
1056
+ function getAppsInTossGlobals() {
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 }));
1062
1068
  }
1063
1069
  };
1064
- async function eventLog(params) {
1065
- aitState.logAnalytics({
1066
- type: params.log_type,
1067
- params: {
1068
- log_name: params.log_name,
1069
- ...params.params
1070
- }
1071
- });
1070
+ /** @deprecated */
1071
+ function getSafeAreaInsets() {
1072
+ return aitState.state.safeAreaInsets.top;
1072
1073
  }
1073
1074
  //#endregion
1074
1075
  //#region src/mock/partner/index.ts