@ait-co/devtools 0.1.73 → 0.1.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/devtools-opener-BbUXBzgA.js.map +1 -1
- package/dist/devtools-opener-Bp671YXu.cjs.map +1 -1
- package/dist/devtools-opener-D84kZFtR.js.map +1 -1
- package/dist/devtools-opener-h6A-UjzC.cjs.map +1 -1
- package/dist/mcp/cli.js +191 -76
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mock/index.d.ts +50 -2
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +1210 -1110
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +828 -820
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-Ditd2ndz.js → qr-http-server-CDO6o2nr.js} +69 -12
- package/dist/qr-http-server-CDO6o2nr.js.map +1 -0
- package/dist/{qr-http-server-0uN5jxLW.cjs → qr-http-server-D0v9ooAD.cjs} +69 -12
- package/dist/qr-http-server-D0v9ooAD.cjs.map +1 -0
- package/dist/{qr-http-server-TQG61eI4.js → qr-http-server-DznDIcJF.js} +69 -12
- package/dist/qr-http-server-DznDIcJF.js.map +1 -0
- package/dist/{qr-http-server-BTjpFS3p.cjs → qr-http-server-jMC1nVqY.cjs} +69 -12
- package/dist/qr-http-server-jMC1nVqY.cjs.map +1 -0
- package/dist/{tunnel-BXAWl2tI.cjs → tunnel-D7f-0enB.cjs} +3 -2
- package/dist/{tunnel-BXAWl2tI.cjs.map → tunnel-D7f-0enB.cjs.map} +1 -1
- package/dist/{tunnel-BxGnLAat.js → tunnel-km3KkZrF.js} +3 -2
- package/dist/{tunnel-BxGnLAat.js.map → tunnel-km3KkZrF.js.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +2 -1
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts.map +1 -1
- package/dist/unplugin/tunnel.d.ts.map +1 -1
- package/dist/unplugin/tunnel.js +2 -1
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +1 -1
- package/dist/qr-http-server-0uN5jxLW.cjs.map +0 -1
- package/dist/qr-http-server-BTjpFS3p.cjs.map +0 -1
- package/dist/qr-http-server-Ditd2ndz.js.map +0 -1
- package/dist/qr-http-server-TQG61eI4.js.map +0 -1
package/dist/mock/index.js
CHANGED
|
@@ -238,1073 +238,1312 @@ if (!globalRef[SINGLETON_KEY]) globalRef[SINGLETON_KEY] = new AitStateManager();
|
|
|
238
238
|
const aitState = globalRef[SINGLETON_KEY];
|
|
239
239
|
if (typeof window !== "undefined") window.__ait = aitState;
|
|
240
240
|
//#endregion
|
|
241
|
-
//#region src/mock/
|
|
241
|
+
//#region src/mock/device/_helpers.ts
|
|
242
242
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* In the AITC Sandbox PWA (env 2) the dev app runs inside the launcher's
|
|
246
|
-
* full-viewport `<iframe>`. The launcher is the top-level document, so its
|
|
247
|
-
* `env(safe-area-inset-*)` measurement is the ground truth for the real device
|
|
248
|
-
* geometry. The framed page's mock would otherwise report a synthetic preset
|
|
249
|
-
* value (e.g. top=54), which sdk-example then double-pads on top of a viewport
|
|
250
|
-
* that already starts below the status bar — the env-2 "dead band" defect.
|
|
251
|
-
*
|
|
252
|
-
* The launcher forwards its measured insets to the framed page with
|
|
253
|
-
* `postMessage({ type: 'ait:safe-area-insets', insets })` (on iframe load and on
|
|
254
|
-
* resize/orientationchange). This module installs the receive half: it validates
|
|
255
|
-
* the envelope and writes the real insets into the mock `SafeAreaInsets` state,
|
|
256
|
-
* which fires the existing subscribe path (see navigation/index.ts) so apps that
|
|
257
|
-
* subscribe re-read the corrected values.
|
|
258
|
-
*
|
|
259
|
-
* Origin: the inset values are non-sensitive geometry (four small numbers), so
|
|
260
|
-
* we do NOT restrict by origin — the launcher posts cross-origin from a
|
|
261
|
-
* *.trycloudflare.com tunnel with targetOrigin '*'. Shape + range validation is
|
|
262
|
-
* still mandatory: a malformed or out-of-range message is silently ignored so a
|
|
263
|
-
* stray postMessage from any frame can never corrupt the mock state.
|
|
264
|
-
*
|
|
265
|
-
* Message-driven by design: env 1 (desktop browser, no launcher) never receives
|
|
266
|
-
* this message, so the panel preset stays authoritative there with zero special
|
|
267
|
-
* casing here.
|
|
243
|
+
* 디바이스 모듈 내부 공유 헬퍼
|
|
268
244
|
*/
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
245
|
+
function generatePlaceholderImage(width, height, text, color) {
|
|
246
|
+
const canvas = document.createElement("canvas");
|
|
247
|
+
canvas.width = width;
|
|
248
|
+
canvas.height = height;
|
|
249
|
+
const ctx = canvas.getContext("2d");
|
|
250
|
+
if (!ctx) {
|
|
251
|
+
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>`;
|
|
252
|
+
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
253
|
+
}
|
|
254
|
+
ctx.fillStyle = color;
|
|
255
|
+
ctx.fillRect(0, 0, width, height);
|
|
256
|
+
ctx.fillStyle = "white";
|
|
257
|
+
ctx.font = "16px sans-serif";
|
|
258
|
+
ctx.textAlign = "center";
|
|
259
|
+
ctx.textBaseline = "middle";
|
|
260
|
+
ctx.fillText(text, width / 2, height / 2);
|
|
261
|
+
return canvas.toDataURL("image/png");
|
|
274
262
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
263
|
+
const DEFAULT_PLACEHOLDERS = [
|
|
264
|
+
{
|
|
265
|
+
text: "Mock Photo 1",
|
|
266
|
+
color: "#3182F6"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
text: "Mock Photo 2",
|
|
270
|
+
color: "#27ae60"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
text: "Mock Photo 3",
|
|
274
|
+
color: "#e67e22"
|
|
275
|
+
}
|
|
276
|
+
];
|
|
277
|
+
let cachedPlaceholders = null;
|
|
278
|
+
function getDefaultPlaceholderImages() {
|
|
279
|
+
if (!cachedPlaceholders) cachedPlaceholders = DEFAULT_PLACEHOLDERS.map((p) => generatePlaceholderImage(320, 240, p.text, p.color));
|
|
280
|
+
return [...cachedPlaceholders];
|
|
293
281
|
}
|
|
294
|
-
/**
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
function applyForwardedSafeAreaInsets(insets) {
|
|
300
|
-
const current = aitState.state.safeAreaInsets;
|
|
301
|
-
if (current.top === insets.top && current.bottom === insets.bottom && current.left === insets.left && current.right === insets.right) return;
|
|
302
|
-
aitState.update({ safeAreaInsets: insets });
|
|
282
|
+
/** @internal device 모듈 내부 전용 */
|
|
283
|
+
function getMockImages() {
|
|
284
|
+
const images = aitState.state.mockData.images;
|
|
285
|
+
if (images.length > 0) return images;
|
|
286
|
+
return getDefaultPlaceholderImages();
|
|
303
287
|
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
288
|
+
const PROMPT_TIMEOUT_MS = 3e4;
|
|
289
|
+
/** @internal device 모듈 내부 전용 */
|
|
290
|
+
function waitForPromptResponse(type) {
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
const eventName = `__ait:prompt-response:${type}`;
|
|
293
|
+
const cancelName = "__ait:prompt-cancel";
|
|
294
|
+
function cleanup() {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
window.removeEventListener(eventName, handler);
|
|
297
|
+
window.removeEventListener(cancelName, cancelHandler);
|
|
298
|
+
}
|
|
299
|
+
const timer = setTimeout(() => {
|
|
300
|
+
cleanup();
|
|
301
|
+
const hint = !!document.querySelector(".ait-panel") ? "Please provide input via the DevTools panel." : "Is @ait-co/devtools/panel imported?";
|
|
302
|
+
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] Prompt timeout for "${type}" after ${PROMPT_TIMEOUT_MS / 1e3}s. ${hint}`));
|
|
303
|
+
}, PROMPT_TIMEOUT_MS);
|
|
304
|
+
const handler = (e) => {
|
|
305
|
+
cleanup();
|
|
306
|
+
resolve(e.detail);
|
|
307
|
+
};
|
|
308
|
+
const cancelHandler = () => {
|
|
309
|
+
cleanup();
|
|
310
|
+
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] Prompt cancelled for "${type}"`));
|
|
311
|
+
};
|
|
312
|
+
window.addEventListener(eventName, handler);
|
|
313
|
+
window.addEventListener(cancelName, cancelHandler);
|
|
314
|
+
window.dispatchEvent(new CustomEvent("__ait:prompt-request", { detail: { type } }));
|
|
317
315
|
});
|
|
318
316
|
}
|
|
319
317
|
//#endregion
|
|
320
|
-
//#region src/mock/
|
|
318
|
+
//#region src/mock/permissions.ts
|
|
321
319
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* @param apiName - 로그에 기록할 SDK 메서드 이름 (예: `'setScreenAwakeMode'`)
|
|
325
|
-
* @param fidelity - 이 mock의 fidelity grade ('faithful' | 'partial' | 'inert')
|
|
326
|
-
* @param fn - 실제 mock 구현체. 시그니처를 그대로 통과시킨다.
|
|
327
|
-
* @returns fn과 동일한 타입의 래퍼 함수
|
|
320
|
+
* 권한 시스템 mock
|
|
321
|
+
* 각 디바이스 API (.getPermission, .openPermissionDialog)에 부착된다.
|
|
328
322
|
*/
|
|
329
|
-
function observe(apiName, fidelity, fn) {
|
|
330
|
-
return (...args) => {
|
|
331
|
-
const timestamp = Date.now();
|
|
332
|
-
const safeArgs = args.map((a) => safeSerialize(a));
|
|
333
|
-
const result = fn(...args);
|
|
334
|
-
if (result instanceof Promise) {
|
|
335
|
-
aitState.logSdkCall({
|
|
336
|
-
method: apiName,
|
|
337
|
-
args: safeArgs,
|
|
338
|
-
timestamp,
|
|
339
|
-
status: "pending",
|
|
340
|
-
fidelity
|
|
341
|
-
});
|
|
342
|
-
result.then((value) => {
|
|
343
|
-
aitState.logSdkCall({
|
|
344
|
-
method: apiName,
|
|
345
|
-
args: safeArgs,
|
|
346
|
-
timestamp,
|
|
347
|
-
status: "resolved",
|
|
348
|
-
result: safeSerialize(value),
|
|
349
|
-
fidelity
|
|
350
|
-
});
|
|
351
|
-
}, (err) => {
|
|
352
|
-
aitState.logSdkCall({
|
|
353
|
-
method: apiName,
|
|
354
|
-
args: safeArgs,
|
|
355
|
-
timestamp,
|
|
356
|
-
status: "rejected",
|
|
357
|
-
error: err instanceof Error ? err.message : String(err),
|
|
358
|
-
fidelity
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
return result;
|
|
362
|
-
}
|
|
363
|
-
aitState.logSdkCall({
|
|
364
|
-
method: apiName,
|
|
365
|
-
args: safeArgs,
|
|
366
|
-
timestamp,
|
|
367
|
-
status: "resolved",
|
|
368
|
-
result: safeSerialize(result),
|
|
369
|
-
fidelity
|
|
370
|
-
});
|
|
371
|
-
return result;
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
323
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
* - 함수 — `'[Function: name]'` 문자열.
|
|
378
|
-
* - 기타 객체 — JSON.stringify 실패 시 `'[unserializable]'`.
|
|
324
|
+
* web-framework 3.0+ 권한 에러 기반 클래스.
|
|
325
|
+
* `instanceof PermissionError`로 체크하는 코드와 호환된다.
|
|
379
326
|
*/
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
return JSON.parse(JSON.stringify(value));
|
|
386
|
-
} catch {
|
|
387
|
-
return "[unserializable]";
|
|
327
|
+
var PermissionError = class extends Error {
|
|
328
|
+
constructor({ methodName, message }) {
|
|
329
|
+
super(message ?? `${methodName}: permission denied`);
|
|
330
|
+
this.name = `${methodName}PermissionError`;
|
|
388
331
|
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
332
|
+
};
|
|
333
|
+
/** openCamera 권한 에러 */
|
|
334
|
+
var OpenCameraPermissionError = class extends PermissionError {
|
|
335
|
+
constructor() {
|
|
336
|
+
super({ methodName: "openCamera" });
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
/** fetchAlbumPhotos 권한 에러 */
|
|
340
|
+
var FetchAlbumPhotosPermissionError = class extends PermissionError {
|
|
341
|
+
constructor() {
|
|
342
|
+
super({ methodName: "fetchAlbumPhotos" });
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
/** fetchContacts 권한 에러 */
|
|
346
|
+
var FetchContactsPermissionError = class extends PermissionError {
|
|
347
|
+
constructor() {
|
|
348
|
+
super({ methodName: "fetchContacts" });
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
/** getCurrentLocation 권한 에러 */
|
|
352
|
+
var GetCurrentLocationPermissionError = class extends PermissionError {
|
|
353
|
+
constructor() {
|
|
354
|
+
super({ methodName: "getCurrentLocation" });
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
/** getClipboardText 권한 에러 */
|
|
358
|
+
var GetClipboardTextPermissionError = class extends PermissionError {
|
|
359
|
+
constructor() {
|
|
360
|
+
super({ methodName: "getClipboardText" });
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
/** setClipboardText 권한 에러 */
|
|
364
|
+
var SetClipboardTextPermissionError = class extends PermissionError {
|
|
365
|
+
constructor() {
|
|
366
|
+
super({ methodName: "setClipboardText" });
|
|
367
|
+
}
|
|
368
|
+
};
|
|
392
369
|
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
|
|
396
|
-
* 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
|
|
397
|
-
* mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
|
|
398
|
-
* 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
|
|
399
|
-
* 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
|
|
400
|
-
*
|
|
401
|
-
* ## KNOWN_UNIMPLEMENTED 정책
|
|
402
|
-
* SDK에 존재하는 것으로 알려져 있으나 현재 mock이 없는 API 이름만 이 집합에 둔다.
|
|
403
|
-
* 이 경우에만 throw 대신 🔴 inert no-op을 반환하고 sdkCallLog에 기록한다.
|
|
404
|
-
* 완전히 미지의 이름은 여전히 throw — "잘 되는 척" 방지.
|
|
370
|
+
* startUpdateLocation 권한 에러.
|
|
371
|
+
* web-framework 3.0에서 GetCurrentLocationPermissionError의 alias.
|
|
405
372
|
*/
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
373
|
+
const StartUpdateLocationPermissionError = GetCurrentLocationPermissionError;
|
|
374
|
+
const permissionErrorMap = {
|
|
375
|
+
openCamera: OpenCameraPermissionError,
|
|
376
|
+
fetchAlbumPhotos: FetchAlbumPhotosPermissionError,
|
|
377
|
+
fetchAlbumItems: FetchAlbumPhotosPermissionError,
|
|
378
|
+
fetchContacts: FetchContactsPermissionError,
|
|
379
|
+
getCurrentLocation: GetCurrentLocationPermissionError,
|
|
380
|
+
getClipboardText: GetClipboardTextPermissionError,
|
|
381
|
+
setClipboardText: SetClipboardTextPermissionError
|
|
382
|
+
};
|
|
383
|
+
async function getPermission(name) {
|
|
384
|
+
return aitState.state.permissions[name];
|
|
385
|
+
}
|
|
386
|
+
async function openPermissionDialog(name) {
|
|
387
|
+
if (aitState.state.permissions[name] === "allowed") return "allowed";
|
|
388
|
+
aitState.patch("permissions", { [name]: "allowed" });
|
|
389
|
+
return "allowed";
|
|
390
|
+
}
|
|
391
|
+
async function requestPermission(permission) {
|
|
392
|
+
return openPermissionDialog(permission.name);
|
|
393
|
+
}
|
|
394
|
+
/** 권한이 필요한 함수에 .getPermission(), .openPermissionDialog()를 부착 */
|
|
395
|
+
function withPermission(fn, permissionName) {
|
|
396
|
+
const enhanced = fn;
|
|
397
|
+
enhanced.getPermission = () => getPermission(permissionName);
|
|
398
|
+
enhanced.openPermissionDialog = () => openPermissionDialog(permissionName);
|
|
399
|
+
return enhanced;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* 권한 체크 후 denied면 per-API *PermissionError 서브클래스를 throw한다.
|
|
403
|
+
* 실 3.0 SDK 동작과 일치 — `instanceof PermissionError` 분기가 mock에서도 동작한다 (#372).
|
|
411
404
|
*/
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (KNOWN_UNIMPLEMENTED.has(name)) return (...args) => {
|
|
419
|
-
console.warn(`[@ait-co/devtools] ${moduleName}.${name} is known-unimplemented (🔴 inert). Returning undefined. Please file or upvote an issue: ${ISSUES_URL}`);
|
|
420
|
-
aitState.logSdkCall({
|
|
421
|
-
method: `${moduleName}.${name}`,
|
|
422
|
-
args,
|
|
423
|
-
timestamp: Date.now(),
|
|
424
|
-
status: "resolved",
|
|
425
|
-
result: void 0,
|
|
426
|
-
fidelity: "inert"
|
|
427
|
-
});
|
|
428
|
-
};
|
|
429
|
-
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}`);
|
|
430
|
-
} });
|
|
405
|
+
function checkPermission(name, fnName) {
|
|
406
|
+
if (aitState.state.permissions[name] === "denied") {
|
|
407
|
+
const ErrorClass = permissionErrorMap[fnName];
|
|
408
|
+
if (ErrorClass) throw new ErrorClass();
|
|
409
|
+
throw new PermissionError({ methodName: fnName });
|
|
410
|
+
}
|
|
431
411
|
}
|
|
432
412
|
//#endregion
|
|
433
|
-
//#region src/mock/
|
|
413
|
+
//#region src/mock/device/camera.ts
|
|
434
414
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
* 변경 이력 (#196):
|
|
438
|
-
* - slot 레지스트리로 TossAds destroy/destroyAll 누수 수정 (🟡→🟢)
|
|
439
|
-
* - attachBanner BannerSlotCallbacks 발화 (onAdRendered/onAdImpression/onNoFill 등)
|
|
440
|
-
* - initialize onInitialized/onInitializationFailed 발화
|
|
441
|
-
* - AdMob reward 파라미터화 (state.ads.rewardUnitType/rewardAmount)
|
|
442
|
-
* - 모든 호출 observe()로 sdkCallLog에 기록
|
|
415
|
+
* Camera & Album Photos & Album Items mock
|
|
416
|
+
* mock/web/prompt 모드 지원
|
|
443
417
|
*/
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
return
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
function _nextSlotId(adGroupId) {
|
|
451
|
-
_slotCounter += 1;
|
|
452
|
-
return `mock-slot-${adGroupId}-${_slotCounter}`;
|
|
418
|
+
async function openCameraMock() {
|
|
419
|
+
const images = getMockImages();
|
|
420
|
+
return {
|
|
421
|
+
id: crypto.randomUUID(),
|
|
422
|
+
dataUri: images[0]
|
|
423
|
+
};
|
|
453
424
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
425
|
+
async function openCameraWeb() {
|
|
426
|
+
return new Promise((resolve, reject) => {
|
|
427
|
+
const input = document.createElement("input");
|
|
428
|
+
input.type = "file";
|
|
429
|
+
input.accept = "image/*";
|
|
430
|
+
input.capture = "environment";
|
|
431
|
+
let settled = false;
|
|
432
|
+
input.onchange = () => {
|
|
433
|
+
settled = true;
|
|
434
|
+
const file = input.files?.[0];
|
|
435
|
+
if (!file) {
|
|
436
|
+
reject(/* @__PURE__ */ new Error("No file selected"));
|
|
459
437
|
return;
|
|
460
438
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
});
|
|
466
|
-
}, 200);
|
|
467
|
-
return () => {};
|
|
468
|
-
})),
|
|
469
|
-
showAppsInTossAdMob: withIsSupported(observe("GoogleAdMob.showAppsInTossAdMob", "faithful", (args) => {
|
|
470
|
-
if (!aitState.state.ads.isLoaded) {
|
|
471
|
-
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
472
|
-
return () => {};
|
|
473
|
-
}
|
|
474
|
-
setTimeout(() => args.onEvent({ type: "requested" }), 50);
|
|
475
|
-
setTimeout(() => args.onEvent({ type: "show" }), 100);
|
|
476
|
-
setTimeout(() => args.onEvent({ type: "impression" }), 150);
|
|
477
|
-
setTimeout(() => {
|
|
478
|
-
const { rewardUnitType, rewardAmount } = aitState.state.ads;
|
|
479
|
-
args.onEvent({
|
|
480
|
-
type: "userEarnedReward",
|
|
481
|
-
data: {
|
|
482
|
-
unitType: rewardUnitType,
|
|
483
|
-
unitAmount: rewardAmount
|
|
484
|
-
}
|
|
439
|
+
const reader = new FileReader();
|
|
440
|
+
reader.onload = () => resolve({
|
|
441
|
+
id: crypto.randomUUID(),
|
|
442
|
+
dataUri: reader.result
|
|
485
443
|
});
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
args.onEvent({ type: "dismissed" });
|
|
489
|
-
aitState.patch("ads", { isLoaded: false });
|
|
490
|
-
}, 1500);
|
|
491
|
-
return () => {};
|
|
492
|
-
})),
|
|
493
|
-
isAppsInTossAdMobLoaded: withIsSupported(observe("GoogleAdMob.isAppsInTossAdMobLoaded", "faithful", async (_options) => aitState.state.ads.isLoaded))
|
|
494
|
-
});
|
|
495
|
-
const TossAds = createMockProxy("TossAds", {
|
|
496
|
-
initialize: withIsSupported(observe("TossAds.initialize", "partial", (options) => {
|
|
497
|
-
if (aitState.state.ads.forceNoFill) {
|
|
498
|
-
options.callbacks?.onInitializationFailed?.(/* @__PURE__ */ new Error("No fill"));
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
options.callbacks?.onInitialized?.();
|
|
502
|
-
})),
|
|
503
|
-
attach: withIsSupported(observe("TossAds.attach", "partial", (_adGroupId, target, _options) => {
|
|
504
|
-
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
505
|
-
if (el) {
|
|
506
|
-
const placeholder = document.createElement("div");
|
|
507
|
-
placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:16px;text-align:center;color:#666;font-size:14px;";
|
|
508
|
-
placeholder.textContent = "[@ait-co/devtools] TossAds Placeholder";
|
|
509
|
-
el.appendChild(placeholder);
|
|
510
|
-
}
|
|
511
|
-
})),
|
|
512
|
-
attachBanner: withIsSupported(observe("TossAds.attachBanner", "faithful", (adGroupId, target, options) => {
|
|
513
|
-
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
514
|
-
const slotId = _nextSlotId(adGroupId);
|
|
515
|
-
const placeholder = document.createElement("div");
|
|
516
|
-
const theme = options?.theme ?? "auto";
|
|
517
|
-
const variant = options?.variant ?? "card";
|
|
518
|
-
const isDark = theme === "dark" || theme === "auto" && typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
519
|
-
const bg = isDark ? "#1a1a1a" : "#f0f0f0";
|
|
520
|
-
const textColor = isDark ? "#aaa" : "#666";
|
|
521
|
-
const borderColor = isDark ? "#555" : "#999";
|
|
522
|
-
const height = variant === "expanded" ? "120px" : "60px";
|
|
523
|
-
placeholder.dataset.aitSlotId = slotId;
|
|
524
|
-
placeholder.style.cssText = `background:${bg};border:1px dashed ${borderColor};padding:8px 12px;text-align:center;color:${textColor};font-size:12px;min-height:${height};display:flex;align-items:center;justify-content:center;`;
|
|
525
|
-
placeholder.textContent = `[@ait-co/devtools] Banner Ad (${variant})`;
|
|
526
|
-
if (el) {
|
|
527
|
-
el.appendChild(placeholder);
|
|
528
|
-
_slotRegistry.set(slotId, placeholder);
|
|
529
|
-
}
|
|
530
|
-
const destroySlot = () => {
|
|
531
|
-
const registered = _slotRegistry.get(slotId);
|
|
532
|
-
if (registered) {
|
|
533
|
-
registered.remove();
|
|
534
|
-
_slotRegistry.delete(slotId);
|
|
535
|
-
}
|
|
444
|
+
reader.onerror = () => reject(/* @__PURE__ */ new Error("Failed to read file"));
|
|
445
|
+
reader.readAsDataURL(file);
|
|
536
446
|
};
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
447
|
+
const onFocus = () => {
|
|
448
|
+
setTimeout(() => {
|
|
449
|
+
if (!settled) reject(/* @__PURE__ */ new Error("File picker cancelled"));
|
|
450
|
+
window.removeEventListener("focus", onFocus);
|
|
451
|
+
}, 300);
|
|
452
|
+
};
|
|
453
|
+
window.addEventListener("focus", onFocus);
|
|
454
|
+
input.click();
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
async function openCameraPrompt() {
|
|
458
|
+
const dataUri = await waitForPromptResponse("camera");
|
|
459
|
+
return {
|
|
460
|
+
id: crypto.randomUUID(),
|
|
461
|
+
dataUri
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const _openCamera = async (_options) => {
|
|
465
|
+
checkPermission("camera", "openCamera");
|
|
466
|
+
const mode = aitState.state.deviceModes.camera;
|
|
467
|
+
if (mode === "web") return openCameraWeb();
|
|
468
|
+
if (mode === "prompt") return openCameraPrompt();
|
|
469
|
+
return openCameraMock();
|
|
470
|
+
};
|
|
471
|
+
const openCamera = withPermission(_openCamera, "camera");
|
|
472
|
+
async function fetchAlbumPhotosMock(maxCount) {
|
|
473
|
+
return getMockImages().slice(0, maxCount).map((dataUri) => ({
|
|
474
|
+
id: crypto.randomUUID(),
|
|
475
|
+
dataUri
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
async function fetchAlbumPhotosWeb(maxCount) {
|
|
479
|
+
return new Promise((resolve, reject) => {
|
|
480
|
+
const input = document.createElement("input");
|
|
481
|
+
input.type = "file";
|
|
482
|
+
input.accept = "image/*";
|
|
483
|
+
input.multiple = true;
|
|
484
|
+
let settled = false;
|
|
485
|
+
input.onchange = async () => {
|
|
486
|
+
settled = true;
|
|
487
|
+
const files = Array.from(input.files ?? []).slice(0, maxCount);
|
|
488
|
+
if (files.length === 0) {
|
|
489
|
+
reject(/* @__PURE__ */ new Error("No files selected"));
|
|
553
490
|
return;
|
|
554
491
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
492
|
+
resolve(await Promise.all(files.map((file) => new Promise((res, rej) => {
|
|
493
|
+
const reader = new FileReader();
|
|
494
|
+
reader.onload = () => res({
|
|
495
|
+
id: crypto.randomUUID(),
|
|
496
|
+
dataUri: reader.result
|
|
497
|
+
});
|
|
498
|
+
reader.onerror = () => rej(/* @__PURE__ */ new Error("Failed to read file"));
|
|
499
|
+
reader.readAsDataURL(file);
|
|
500
|
+
}))));
|
|
501
|
+
};
|
|
502
|
+
const onFocus = () => {
|
|
503
|
+
setTimeout(() => {
|
|
504
|
+
if (!settled) reject(/* @__PURE__ */ new Error("File picker cancelled"));
|
|
505
|
+
window.removeEventListener("focus", onFocus);
|
|
506
|
+
}, 300);
|
|
507
|
+
};
|
|
508
|
+
window.addEventListener("focus", onFocus);
|
|
509
|
+
input.click();
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
async function fetchAlbumPhotosPrompt(maxCount) {
|
|
513
|
+
return (await waitForPromptResponse("photos")).slice(0, maxCount).map((dataUri) => ({
|
|
514
|
+
id: crypto.randomUUID(),
|
|
515
|
+
dataUri
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
const _fetchAlbumPhotos = async (options) => {
|
|
519
|
+
checkPermission("photos", "fetchAlbumPhotos");
|
|
520
|
+
const maxCount = options?.maxCount ?? 10;
|
|
521
|
+
const mode = aitState.state.deviceModes.photos;
|
|
522
|
+
if (mode === "web") return fetchAlbumPhotosWeb(maxCount);
|
|
523
|
+
if (mode === "prompt") return fetchAlbumPhotosPrompt(maxCount);
|
|
524
|
+
return fetchAlbumPhotosMock(maxCount);
|
|
525
|
+
};
|
|
526
|
+
const fetchAlbumPhotos = withPermission(_fetchAlbumPhotos, "photos");
|
|
527
|
+
async function fetchAlbumItemsMock(maxCount, types) {
|
|
528
|
+
return getMockImages().slice(0, maxCount).filter(() => types.includes("PHOTO")).map((dataUri) => ({
|
|
529
|
+
id: crypto.randomUUID(),
|
|
530
|
+
dataUri,
|
|
531
|
+
type: "PHOTO"
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
async function fetchAlbumItemsWeb(maxCount, types) {
|
|
535
|
+
return new Promise((resolve) => {
|
|
536
|
+
const input = document.createElement("input");
|
|
537
|
+
input.type = "file";
|
|
538
|
+
input.accept = types.includes("VIDEO") ? "image/*,video/*" : "image/*";
|
|
539
|
+
input.multiple = true;
|
|
540
|
+
let settled = false;
|
|
541
|
+
input.onchange = async () => {
|
|
542
|
+
settled = true;
|
|
543
|
+
const files = Array.from(input.files ?? []).slice(0, maxCount);
|
|
544
|
+
if (files.length === 0) {
|
|
545
|
+
resolve([]);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
resolve(await Promise.all(files.map((file) => new Promise((res, rej) => {
|
|
549
|
+
const itemType = file.type.startsWith("video/") ? "VIDEO" : "PHOTO";
|
|
550
|
+
const reader = new FileReader();
|
|
551
|
+
reader.onload = () => res({
|
|
552
|
+
id: crypto.randomUUID(),
|
|
553
|
+
dataUri: reader.result,
|
|
554
|
+
type: itemType
|
|
555
|
+
});
|
|
556
|
+
reader.onerror = () => rej(/* @__PURE__ */ new Error("Failed to read file"));
|
|
557
|
+
reader.readAsDataURL(file);
|
|
558
|
+
}))));
|
|
559
|
+
};
|
|
560
|
+
const onFocus = () => {
|
|
561
|
+
setTimeout(() => {
|
|
562
|
+
if (!settled) resolve([]);
|
|
563
|
+
window.removeEventListener("focus", onFocus);
|
|
564
|
+
}, 300);
|
|
565
|
+
};
|
|
566
|
+
window.addEventListener("focus", onFocus);
|
|
567
|
+
input.click();
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
async function fetchAlbumItemsPrompt(maxCount) {
|
|
571
|
+
return (await waitForPromptResponse("photos")).slice(0, maxCount).map((dataUri) => ({
|
|
572
|
+
id: crypto.randomUUID(),
|
|
573
|
+
dataUri,
|
|
574
|
+
type: "PHOTO"
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
const _fetchAlbumItems = async (options) => {
|
|
578
|
+
checkPermission("photos", "fetchAlbumItems");
|
|
579
|
+
const maxCount = options?.maxCount ?? 10;
|
|
580
|
+
const types = options?.types ?? ["PHOTO"];
|
|
581
|
+
const mode = aitState.state.deviceModes.photos;
|
|
582
|
+
if (mode === "web") return fetchAlbumItemsWeb(maxCount, types);
|
|
583
|
+
if (mode === "prompt") return fetchAlbumItemsPrompt(maxCount);
|
|
584
|
+
return fetchAlbumItemsMock(maxCount, types);
|
|
585
|
+
};
|
|
586
|
+
const fetchAlbumItems = withPermission(_fetchAlbumItems, "photos");
|
|
603
587
|
//#endregion
|
|
604
|
-
//#region src/mock/
|
|
588
|
+
//#region src/mock/device/clipboard.ts
|
|
605
589
|
/**
|
|
606
|
-
*
|
|
590
|
+
* Clipboard mock
|
|
591
|
+
* mock/web 모드 지원
|
|
607
592
|
*/
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
return
|
|
615
|
-
},
|
|
616
|
-
impression: (params) => {
|
|
617
|
-
aitState.logAnalytics({
|
|
618
|
-
type: "impression",
|
|
619
|
-
params: params ?? {}
|
|
620
|
-
});
|
|
621
|
-
return Promise.resolve();
|
|
622
|
-
},
|
|
623
|
-
click: (params) => {
|
|
624
|
-
aitState.logAnalytics({
|
|
625
|
-
type: "click",
|
|
626
|
-
params: params ?? {}
|
|
627
|
-
});
|
|
628
|
-
return Promise.resolve();
|
|
593
|
+
const _getClipboardText = async () => {
|
|
594
|
+
checkPermission("clipboard", "getClipboardText");
|
|
595
|
+
if (aitState.state.deviceModes.clipboard === "mock") return aitState.state.mockData.clipboardText;
|
|
596
|
+
try {
|
|
597
|
+
return await navigator.clipboard.readText();
|
|
598
|
+
} catch {
|
|
599
|
+
return "";
|
|
629
600
|
}
|
|
630
601
|
};
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
602
|
+
const getClipboardText = withPermission(_getClipboardText, "clipboard");
|
|
603
|
+
const _setClipboardText = async (text) => {
|
|
604
|
+
checkPermission("clipboard", "setClipboardText");
|
|
605
|
+
if (aitState.state.deviceModes.clipboard === "mock") {
|
|
606
|
+
aitState.patch("mockData", { clipboardText: text });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
await navigator.clipboard.writeText(text);
|
|
610
|
+
};
|
|
611
|
+
const setClipboardText = withPermission(_setClipboardText, "clipboard");
|
|
640
612
|
//#endregion
|
|
641
|
-
//#region src/mock/
|
|
613
|
+
//#region src/mock/device/contacts.ts
|
|
642
614
|
/**
|
|
643
|
-
*
|
|
615
|
+
* Contacts mock
|
|
644
616
|
*/
|
|
645
|
-
async
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
async function getUserKeyForGame() {
|
|
655
|
-
if (!aitState.state.auth.userKeyHash) return void 0;
|
|
656
|
-
return {
|
|
657
|
-
hash: aitState.state.auth.userKeyHash,
|
|
658
|
-
type: "HASH"
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
async function getAnonymousKey() {
|
|
662
|
-
if (!aitState.state.auth.anonymousKeyHash) return void 0;
|
|
617
|
+
const _fetchContacts = async (options) => {
|
|
618
|
+
checkPermission("contacts", "fetchContacts");
|
|
619
|
+
let contacts = aitState.state.contacts;
|
|
620
|
+
if (options.query?.contains) {
|
|
621
|
+
const q = options.query.contains.toLowerCase();
|
|
622
|
+
contacts = contacts.filter((c) => c.name.toLowerCase().includes(q) || c.phoneNumber.includes(q));
|
|
623
|
+
}
|
|
624
|
+
const sliced = contacts.slice(options.offset, options.offset + options.size);
|
|
625
|
+
const nextOffset = options.offset + options.size;
|
|
663
626
|
return {
|
|
664
|
-
|
|
665
|
-
|
|
627
|
+
result: sliced,
|
|
628
|
+
nextOffset: nextOffset < contacts.length ? nextOffset : null,
|
|
629
|
+
done: nextOffset >= contacts.length
|
|
666
630
|
};
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
console.log("[@ait-co/devtools] appsInTossSignTossCert called (no-op in mock)");
|
|
670
|
-
}
|
|
631
|
+
};
|
|
632
|
+
const fetchContacts = withPermission(_fetchContacts, "contacts");
|
|
671
633
|
//#endregion
|
|
672
|
-
//#region src/mock/device/
|
|
634
|
+
//#region src/mock/device/haptic.ts
|
|
673
635
|
/**
|
|
674
|
-
*
|
|
636
|
+
* Haptic Feedback & saveBase64Data mock
|
|
637
|
+
*
|
|
638
|
+
* generateHapticFeedback — 영역 3 (하드웨어 API 관측):
|
|
639
|
+
* - 10종 HapticFeedbackType을 navigator.vibrate 패턴으로 매핑(근사, best-effort).
|
|
640
|
+
* - `typeof navigator.vibrate === 'function'` 가드 — API 없는 환경에서 throw 없이 skip.
|
|
641
|
+
* - sdkCallLog에 🟡(partial)로 기록. params: { hapticType, vibrated: boolean }.
|
|
642
|
+
* - 시그니처 불변 — __typecheck.ts의 Assert<Mock, Original> 통과.
|
|
675
643
|
*/
|
|
676
|
-
function generatePlaceholderImage(width, height, text, color) {
|
|
677
|
-
const canvas = document.createElement("canvas");
|
|
678
|
-
canvas.width = width;
|
|
679
|
-
canvas.height = height;
|
|
680
|
-
const ctx = canvas.getContext("2d");
|
|
681
|
-
if (!ctx) {
|
|
682
|
-
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>`;
|
|
683
|
-
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
684
|
-
}
|
|
685
|
-
ctx.fillStyle = color;
|
|
686
|
-
ctx.fillRect(0, 0, width, height);
|
|
687
|
-
ctx.fillStyle = "white";
|
|
688
|
-
ctx.font = "16px sans-serif";
|
|
689
|
-
ctx.textAlign = "center";
|
|
690
|
-
ctx.textBaseline = "middle";
|
|
691
|
-
ctx.fillText(text, width / 2, height / 2);
|
|
692
|
-
return canvas.toDataURL("image/png");
|
|
693
|
-
}
|
|
694
|
-
const DEFAULT_PLACEHOLDERS = [
|
|
695
|
-
{
|
|
696
|
-
text: "Mock Photo 1",
|
|
697
|
-
color: "#3182F6"
|
|
698
|
-
},
|
|
699
|
-
{
|
|
700
|
-
text: "Mock Photo 2",
|
|
701
|
-
color: "#27ae60"
|
|
702
|
-
},
|
|
703
|
-
{
|
|
704
|
-
text: "Mock Photo 3",
|
|
705
|
-
color: "#e67e22"
|
|
706
|
-
}
|
|
707
|
-
];
|
|
708
|
-
let cachedPlaceholders = null;
|
|
709
|
-
function getDefaultPlaceholderImages() {
|
|
710
|
-
if (!cachedPlaceholders) cachedPlaceholders = DEFAULT_PLACEHOLDERS.map((p) => generatePlaceholderImage(320, 240, p.text, p.color));
|
|
711
|
-
return [...cachedPlaceholders];
|
|
712
|
-
}
|
|
713
|
-
/** @internal device 모듈 내부 전용 */
|
|
714
|
-
function getMockImages() {
|
|
715
|
-
const images = aitState.state.mockData.images;
|
|
716
|
-
if (images.length > 0) return images;
|
|
717
|
-
return getDefaultPlaceholderImages();
|
|
718
|
-
}
|
|
719
|
-
const PROMPT_TIMEOUT_MS = 3e4;
|
|
720
|
-
/** @internal device 모듈 내부 전용 */
|
|
721
|
-
function waitForPromptResponse(type) {
|
|
722
|
-
return new Promise((resolve, reject) => {
|
|
723
|
-
const eventName = `__ait:prompt-response:${type}`;
|
|
724
|
-
const cancelName = "__ait:prompt-cancel";
|
|
725
|
-
function cleanup() {
|
|
726
|
-
clearTimeout(timer);
|
|
727
|
-
window.removeEventListener(eventName, handler);
|
|
728
|
-
window.removeEventListener(cancelName, cancelHandler);
|
|
729
|
-
}
|
|
730
|
-
const timer = setTimeout(() => {
|
|
731
|
-
cleanup();
|
|
732
|
-
const hint = !!document.querySelector(".ait-panel") ? "Please provide input via the DevTools panel." : "Is @ait-co/devtools/panel imported?";
|
|
733
|
-
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] Prompt timeout for "${type}" after ${PROMPT_TIMEOUT_MS / 1e3}s. ${hint}`));
|
|
734
|
-
}, PROMPT_TIMEOUT_MS);
|
|
735
|
-
const handler = (e) => {
|
|
736
|
-
cleanup();
|
|
737
|
-
resolve(e.detail);
|
|
738
|
-
};
|
|
739
|
-
const cancelHandler = () => {
|
|
740
|
-
cleanup();
|
|
741
|
-
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] Prompt cancelled for "${type}"`));
|
|
742
|
-
};
|
|
743
|
-
window.addEventListener(eventName, handler);
|
|
744
|
-
window.addEventListener(cancelName, cancelHandler);
|
|
745
|
-
window.dispatchEvent(new CustomEvent("__ait:prompt-request", { detail: { type } }));
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
//#endregion
|
|
749
|
-
//#region src/mock/permissions.ts
|
|
750
|
-
/**
|
|
751
|
-
* 권한 시스템 mock
|
|
752
|
-
* 각 디바이스 API (.getPermission, .openPermissionDialog)에 부착된다.
|
|
753
|
-
*/
|
|
754
|
-
/**
|
|
755
|
-
* web-framework 3.0+ 권한 에러 기반 클래스.
|
|
756
|
-
* `instanceof PermissionError`로 체크하는 코드와 호환된다.
|
|
757
|
-
*/
|
|
758
|
-
var PermissionError = class extends Error {
|
|
759
|
-
constructor({ methodName, message }) {
|
|
760
|
-
super(message ?? `${methodName}: permission denied`);
|
|
761
|
-
this.name = `${methodName}PermissionError`;
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
/** openCamera 권한 에러 */
|
|
765
|
-
var OpenCameraPermissionError = class extends PermissionError {
|
|
766
|
-
constructor() {
|
|
767
|
-
super({ methodName: "openCamera" });
|
|
768
|
-
}
|
|
769
|
-
};
|
|
770
|
-
/** fetchAlbumPhotos 권한 에러 */
|
|
771
|
-
var FetchAlbumPhotosPermissionError = class extends PermissionError {
|
|
772
|
-
constructor() {
|
|
773
|
-
super({ methodName: "fetchAlbumPhotos" });
|
|
774
|
-
}
|
|
775
|
-
};
|
|
776
|
-
/** fetchContacts 권한 에러 */
|
|
777
|
-
var FetchContactsPermissionError = class extends PermissionError {
|
|
778
|
-
constructor() {
|
|
779
|
-
super({ methodName: "fetchContacts" });
|
|
780
|
-
}
|
|
781
|
-
};
|
|
782
|
-
/** getCurrentLocation 권한 에러 */
|
|
783
|
-
var GetCurrentLocationPermissionError = class extends PermissionError {
|
|
784
|
-
constructor() {
|
|
785
|
-
super({ methodName: "getCurrentLocation" });
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
/** getClipboardText 권한 에러 */
|
|
789
|
-
var GetClipboardTextPermissionError = class extends PermissionError {
|
|
790
|
-
constructor() {
|
|
791
|
-
super({ methodName: "getClipboardText" });
|
|
792
|
-
}
|
|
793
|
-
};
|
|
794
|
-
/** setClipboardText 권한 에러 */
|
|
795
|
-
var SetClipboardTextPermissionError = class extends PermissionError {
|
|
796
|
-
constructor() {
|
|
797
|
-
super({ methodName: "setClipboardText" });
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
644
|
/**
|
|
801
|
-
*
|
|
802
|
-
*
|
|
645
|
+
* HapticFeedbackType 10종 → navigator.vibrate 패턴 매핑.
|
|
646
|
+
* 숫자: 진동 ms. 배열: [진동, 정지, 진동, …] 교대 패턴.
|
|
803
647
|
*/
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
648
|
+
const HAPTIC_VIBRATE_PATTERN = {
|
|
649
|
+
tickWeak: 10,
|
|
650
|
+
tap: 20,
|
|
651
|
+
tickMedium: 30,
|
|
652
|
+
softMedium: 40,
|
|
653
|
+
basicWeak: 15,
|
|
654
|
+
basicMedium: 50,
|
|
655
|
+
success: [
|
|
656
|
+
10,
|
|
657
|
+
40,
|
|
658
|
+
10
|
|
659
|
+
],
|
|
660
|
+
error: [
|
|
661
|
+
40,
|
|
662
|
+
30,
|
|
663
|
+
40
|
|
664
|
+
],
|
|
665
|
+
wiggle: [
|
|
666
|
+
20,
|
|
667
|
+
20,
|
|
668
|
+
20,
|
|
669
|
+
20,
|
|
670
|
+
20
|
|
671
|
+
],
|
|
672
|
+
confetti: [
|
|
673
|
+
10,
|
|
674
|
+
20,
|
|
675
|
+
10,
|
|
676
|
+
20,
|
|
677
|
+
10,
|
|
678
|
+
20,
|
|
679
|
+
10
|
|
680
|
+
]
|
|
813
681
|
};
|
|
814
|
-
async function
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
682
|
+
async function generateHapticFeedback(options) {
|
|
683
|
+
const timestamp = Date.now();
|
|
684
|
+
aitState.logAnalytics({
|
|
685
|
+
type: "haptic",
|
|
686
|
+
params: { hapticType: options.type }
|
|
687
|
+
});
|
|
688
|
+
const pattern = HAPTIC_VIBRATE_PATTERN[options.type] ?? 30;
|
|
689
|
+
const vibrated = typeof navigator.vibrate === "function" ? navigator.vibrate(pattern) : false;
|
|
690
|
+
aitState.logSdkCall({
|
|
691
|
+
method: "generateHapticFeedback",
|
|
692
|
+
args: [{ type: options.type }],
|
|
693
|
+
timestamp,
|
|
694
|
+
status: "resolved",
|
|
695
|
+
result: {
|
|
696
|
+
hapticType: options.type,
|
|
697
|
+
vibrated
|
|
698
|
+
},
|
|
699
|
+
fidelity: "partial"
|
|
700
|
+
});
|
|
831
701
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (aitState.state.permissions[name] === "denied") {
|
|
838
|
-
const ErrorClass = permissionErrorMap[fnName];
|
|
839
|
-
if (ErrorClass) throw new ErrorClass();
|
|
840
|
-
throw new PermissionError({ methodName: fnName });
|
|
841
|
-
}
|
|
702
|
+
async function saveBase64Data(params) {
|
|
703
|
+
const a = document.createElement("a");
|
|
704
|
+
a.href = `data:${params.mimeType};base64,${params.data}`;
|
|
705
|
+
a.download = params.fileName;
|
|
706
|
+
a.click();
|
|
842
707
|
}
|
|
843
708
|
//#endregion
|
|
844
|
-
//#region src/mock/device/
|
|
709
|
+
//#region src/mock/device/location.ts
|
|
845
710
|
/**
|
|
846
|
-
*
|
|
711
|
+
* Location mock (getCurrentLocation, startUpdateLocation)
|
|
847
712
|
* mock/web/prompt 모드 지원
|
|
848
713
|
*/
|
|
849
|
-
|
|
850
|
-
|
|
714
|
+
var Accuracy = /* @__PURE__ */ function(Accuracy) {
|
|
715
|
+
Accuracy[Accuracy["Lowest"] = 1] = "Lowest";
|
|
716
|
+
Accuracy[Accuracy["Low"] = 2] = "Low";
|
|
717
|
+
Accuracy[Accuracy["Balanced"] = 3] = "Balanced";
|
|
718
|
+
Accuracy[Accuracy["High"] = 4] = "High";
|
|
719
|
+
Accuracy[Accuracy["Highest"] = 5] = "Highest";
|
|
720
|
+
Accuracy[Accuracy["BestForNavigation"] = 6] = "BestForNavigation";
|
|
721
|
+
return Accuracy;
|
|
722
|
+
}(Accuracy || {});
|
|
723
|
+
function buildLocation() {
|
|
851
724
|
return {
|
|
852
|
-
|
|
853
|
-
|
|
725
|
+
coords: { ...aitState.state.location.coords },
|
|
726
|
+
timestamp: Date.now(),
|
|
727
|
+
accessLocation: aitState.state.location.accessLocation
|
|
854
728
|
};
|
|
855
729
|
}
|
|
856
|
-
async function
|
|
857
|
-
return
|
|
858
|
-
const input = document.createElement("input");
|
|
859
|
-
input.type = "file";
|
|
860
|
-
input.accept = "image/*";
|
|
861
|
-
input.capture = "environment";
|
|
862
|
-
let settled = false;
|
|
863
|
-
input.onchange = () => {
|
|
864
|
-
settled = true;
|
|
865
|
-
const file = input.files?.[0];
|
|
866
|
-
if (!file) {
|
|
867
|
-
reject(/* @__PURE__ */ new Error("No file selected"));
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
const reader = new FileReader();
|
|
871
|
-
reader.onload = () => resolve({
|
|
872
|
-
id: crypto.randomUUID(),
|
|
873
|
-
dataUri: reader.result
|
|
874
|
-
});
|
|
875
|
-
reader.onerror = () => reject(/* @__PURE__ */ new Error("Failed to read file"));
|
|
876
|
-
reader.readAsDataURL(file);
|
|
877
|
-
};
|
|
878
|
-
const onFocus = () => {
|
|
879
|
-
setTimeout(() => {
|
|
880
|
-
if (!settled) reject(/* @__PURE__ */ new Error("File picker cancelled"));
|
|
881
|
-
window.removeEventListener("focus", onFocus);
|
|
882
|
-
}, 300);
|
|
883
|
-
};
|
|
884
|
-
window.addEventListener("focus", onFocus);
|
|
885
|
-
input.click();
|
|
886
|
-
});
|
|
730
|
+
async function getCurrentLocationMock() {
|
|
731
|
+
return buildLocation();
|
|
887
732
|
}
|
|
888
|
-
async function
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
input.type = "file";
|
|
913
|
-
input.accept = "image/*";
|
|
914
|
-
input.multiple = true;
|
|
915
|
-
let settled = false;
|
|
916
|
-
input.onchange = async () => {
|
|
917
|
-
settled = true;
|
|
918
|
-
const files = Array.from(input.files ?? []).slice(0, maxCount);
|
|
919
|
-
if (files.length === 0) {
|
|
920
|
-
reject(/* @__PURE__ */ new Error("No files selected"));
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
resolve(await Promise.all(files.map((file) => new Promise((res, rej) => {
|
|
924
|
-
const reader = new FileReader();
|
|
925
|
-
reader.onload = () => res({
|
|
926
|
-
id: crypto.randomUUID(),
|
|
927
|
-
dataUri: reader.result
|
|
928
|
-
});
|
|
929
|
-
reader.onerror = () => rej(/* @__PURE__ */ new Error("Failed to read file"));
|
|
930
|
-
reader.readAsDataURL(file);
|
|
931
|
-
}))));
|
|
932
|
-
};
|
|
933
|
-
const onFocus = () => {
|
|
934
|
-
setTimeout(() => {
|
|
935
|
-
if (!settled) reject(/* @__PURE__ */ new Error("File picker cancelled"));
|
|
936
|
-
window.removeEventListener("focus", onFocus);
|
|
937
|
-
}, 300);
|
|
938
|
-
};
|
|
939
|
-
window.addEventListener("focus", onFocus);
|
|
940
|
-
input.click();
|
|
733
|
+
async function getCurrentLocationWeb() {
|
|
734
|
+
return new Promise((resolve) => {
|
|
735
|
+
if (!navigator.geolocation) {
|
|
736
|
+
console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
|
|
737
|
+
resolve(buildLocation());
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
navigator.geolocation.getCurrentPosition((pos) => {
|
|
741
|
+
resolve({
|
|
742
|
+
coords: {
|
|
743
|
+
latitude: pos.coords.latitude,
|
|
744
|
+
longitude: pos.coords.longitude,
|
|
745
|
+
altitude: pos.coords.altitude ?? 0,
|
|
746
|
+
accuracy: pos.coords.accuracy,
|
|
747
|
+
altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
|
|
748
|
+
heading: pos.coords.heading ?? 0
|
|
749
|
+
},
|
|
750
|
+
timestamp: pos.timestamp,
|
|
751
|
+
accessLocation: "FINE"
|
|
752
|
+
});
|
|
753
|
+
}, () => {
|
|
754
|
+
console.warn("[@ait-co/devtools] Geolocation failed, falling back to mock");
|
|
755
|
+
resolve(buildLocation());
|
|
756
|
+
});
|
|
941
757
|
});
|
|
942
758
|
}
|
|
943
|
-
async function
|
|
944
|
-
return
|
|
945
|
-
id: crypto.randomUUID(),
|
|
946
|
-
dataUri
|
|
947
|
-
}));
|
|
759
|
+
async function getCurrentLocationPrompt() {
|
|
760
|
+
return waitForPromptResponse("location");
|
|
948
761
|
}
|
|
949
|
-
const
|
|
950
|
-
checkPermission("
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
if (mode === "
|
|
954
|
-
|
|
955
|
-
return fetchAlbumPhotosMock(maxCount);
|
|
762
|
+
const _getCurrentLocation = async (_options) => {
|
|
763
|
+
checkPermission("geolocation", "getCurrentLocation");
|
|
764
|
+
const mode = aitState.state.deviceModes.location;
|
|
765
|
+
if (mode === "web") return getCurrentLocationWeb();
|
|
766
|
+
if (mode === "prompt") return getCurrentLocationPrompt();
|
|
767
|
+
return getCurrentLocationMock();
|
|
956
768
|
};
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
769
|
+
const getCurrentLocation = withPermission(_getCurrentLocation, "geolocation");
|
|
770
|
+
function startUpdateLocationMock(eventParams) {
|
|
771
|
+
const { onEvent, options } = eventParams;
|
|
772
|
+
const interval = Math.max(options.timeInterval, 500);
|
|
773
|
+
const id = setInterval(() => {
|
|
774
|
+
const loc = buildLocation();
|
|
775
|
+
loc.coords.latitude += (Math.random() - .5) * 1e-4;
|
|
776
|
+
loc.coords.longitude += (Math.random() - .5) * 1e-4;
|
|
777
|
+
onEvent(loc);
|
|
778
|
+
}, interval);
|
|
779
|
+
return () => clearInterval(id);
|
|
964
780
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
});
|
|
987
|
-
reader.onerror = () => rej(/* @__PURE__ */ new Error("Failed to read file"));
|
|
988
|
-
reader.readAsDataURL(file);
|
|
989
|
-
}))));
|
|
990
|
-
};
|
|
991
|
-
const onFocus = () => {
|
|
992
|
-
setTimeout(() => {
|
|
993
|
-
if (!settled) resolve([]);
|
|
994
|
-
window.removeEventListener("focus", onFocus);
|
|
995
|
-
}, 300);
|
|
996
|
-
};
|
|
997
|
-
window.addEventListener("focus", onFocus);
|
|
998
|
-
input.click();
|
|
999
|
-
});
|
|
781
|
+
function startUpdateLocationWeb(eventParams) {
|
|
782
|
+
const { onEvent, onError } = eventParams;
|
|
783
|
+
if (!navigator.geolocation) {
|
|
784
|
+
console.warn("[@ait-co/devtools] Geolocation API not available, falling back to mock");
|
|
785
|
+
return startUpdateLocationMock(eventParams);
|
|
786
|
+
}
|
|
787
|
+
const watchId = navigator.geolocation.watchPosition((pos) => {
|
|
788
|
+
onEvent({
|
|
789
|
+
coords: {
|
|
790
|
+
latitude: pos.coords.latitude,
|
|
791
|
+
longitude: pos.coords.longitude,
|
|
792
|
+
altitude: pos.coords.altitude ?? 0,
|
|
793
|
+
accuracy: pos.coords.accuracy,
|
|
794
|
+
altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
|
|
795
|
+
heading: pos.coords.heading ?? 0
|
|
796
|
+
},
|
|
797
|
+
timestamp: pos.timestamp,
|
|
798
|
+
accessLocation: "FINE"
|
|
799
|
+
});
|
|
800
|
+
}, (err) => onError(err));
|
|
801
|
+
return () => navigator.geolocation.clearWatch(watchId);
|
|
1000
802
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
803
|
+
function startUpdateLocationPrompt(eventParams) {
|
|
804
|
+
const { onEvent } = eventParams;
|
|
805
|
+
const handler = (e) => {
|
|
806
|
+
onEvent(e.detail);
|
|
807
|
+
};
|
|
808
|
+
window.addEventListener("__ait:prompt-response:location-update", handler);
|
|
809
|
+
window.dispatchEvent(new CustomEvent("__ait:prompt-request", { detail: { type: "location-update" } }));
|
|
810
|
+
return () => window.removeEventListener("__ait:prompt-response:location-update", handler);
|
|
1007
811
|
}
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if (mode === "web") return fetchAlbumItemsWeb(maxCount, types);
|
|
1014
|
-
if (mode === "prompt") return fetchAlbumItemsPrompt(maxCount);
|
|
1015
|
-
return fetchAlbumItemsMock(maxCount, types);
|
|
812
|
+
const _startUpdateLocation = (eventParams) => {
|
|
813
|
+
const mode = aitState.state.deviceModes.location;
|
|
814
|
+
if (mode === "web") return startUpdateLocationWeb(eventParams);
|
|
815
|
+
if (mode === "prompt") return startUpdateLocationPrompt(eventParams);
|
|
816
|
+
return startUpdateLocationMock(eventParams);
|
|
1016
817
|
};
|
|
1017
|
-
const
|
|
818
|
+
const startUpdateLocation = withPermission(_startUpdateLocation, "geolocation");
|
|
1018
819
|
//#endregion
|
|
1019
|
-
//#region src/mock/device/
|
|
820
|
+
//#region src/mock/device/network.ts
|
|
1020
821
|
/**
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
822
|
+
* Network Status mock (mode-aware helper)
|
|
823
|
+
* navigation 모듈에서 사용. circular dep 방지를 위해 device에 위치.
|
|
1023
824
|
*/
|
|
1024
|
-
const _getClipboardText = async () => {
|
|
1025
|
-
checkPermission("clipboard", "getClipboardText");
|
|
1026
|
-
if (aitState.state.deviceModes.clipboard === "mock") return aitState.state.mockData.clipboardText;
|
|
1027
|
-
try {
|
|
1028
|
-
return await navigator.clipboard.readText();
|
|
1029
|
-
} catch {
|
|
1030
|
-
return "";
|
|
1031
|
-
}
|
|
1032
|
-
};
|
|
1033
|
-
const getClipboardText = withPermission(_getClipboardText, "clipboard");
|
|
1034
|
-
const _setClipboardText = async (text) => {
|
|
1035
|
-
checkPermission("clipboard", "setClipboardText");
|
|
1036
|
-
if (aitState.state.deviceModes.clipboard === "mock") {
|
|
1037
|
-
aitState.patch("mockData", { clipboardText: text });
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
await navigator.clipboard.writeText(text);
|
|
1041
|
-
};
|
|
1042
|
-
const setClipboardText = withPermission(_setClipboardText, "clipboard");
|
|
1043
|
-
//#endregion
|
|
1044
|
-
//#region src/mock/device/contacts.ts
|
|
1045
825
|
/**
|
|
1046
|
-
*
|
|
826
|
+
* Web mode: uses navigator.connection.effectiveType (4g/3g/2g) and navigator.onLine.
|
|
827
|
+
* Limitations: WIFI, 5G, WWAN cannot be detected via the Network Information API.
|
|
828
|
+
* Falls back to state-based value when effectiveType is unavailable.
|
|
1047
829
|
*/
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
830
|
+
function getNetworkStatusByMode() {
|
|
831
|
+
const mode = aitState.state.deviceModes.network;
|
|
832
|
+
if (mode === "mock") return null;
|
|
833
|
+
if (mode === "web") {
|
|
834
|
+
if (!navigator.onLine) return "OFFLINE";
|
|
835
|
+
const conn = navigator.connection;
|
|
836
|
+
if (conn?.effectiveType) return {
|
|
837
|
+
"4g": "4G",
|
|
838
|
+
"3g": "3G",
|
|
839
|
+
"2g": "2G",
|
|
840
|
+
"slow-2g": "2G"
|
|
841
|
+
}[conn.effectiveType] ?? "UNKNOWN";
|
|
842
|
+
return aitState.state.networkStatus;
|
|
1054
843
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
return {
|
|
1058
|
-
result: sliced,
|
|
1059
|
-
nextOffset: nextOffset < contacts.length ? nextOffset : null,
|
|
1060
|
-
done: nextOffset >= contacts.length
|
|
1061
|
-
};
|
|
1062
|
-
};
|
|
1063
|
-
const fetchContacts = withPermission(_fetchContacts, "contacts");
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
1064
846
|
//#endregion
|
|
1065
|
-
//#region src/mock/device/
|
|
847
|
+
//#region src/mock/device/pdf.ts
|
|
1066
848
|
/**
|
|
1067
|
-
*
|
|
849
|
+
* Base64로 인코딩된 PDF 데이터를 네이티브 PDF 뷰어로 여는 mock.
|
|
850
|
+
* mock 환경에서는 즉시 `'CLOSE'`를 반환한다.
|
|
851
|
+
*/
|
|
852
|
+
async function openPDFViewer(_params) {
|
|
853
|
+
await Promise.resolve();
|
|
854
|
+
return "CLOSE";
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/mock/proxy.ts
|
|
858
|
+
/**
|
|
859
|
+
* 미구현 API용 Proxy 트립와이어.
|
|
1068
860
|
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
861
|
+
* 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
|
|
862
|
+
* 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
|
|
863
|
+
* mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
|
|
864
|
+
* 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
|
|
865
|
+
* 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
|
|
866
|
+
*
|
|
867
|
+
* ## KNOWN_UNIMPLEMENTED 정책
|
|
868
|
+
* SDK에 존재하는 것으로 알려져 있으나 현재 mock이 없는 API 이름만 이 집합에 둔다.
|
|
869
|
+
* 이 경우에만 throw 대신 🔴 inert no-op을 반환하고 sdkCallLog에 기록한다.
|
|
870
|
+
* 완전히 미지의 이름은 여전히 throw — "잘 되는 척" 방지.
|
|
1074
871
|
*/
|
|
872
|
+
const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
|
|
1075
873
|
/**
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
874
|
+
* SDK에 존재하나 mock이 아직 없는 것으로 확인된 이름 목록.
|
|
875
|
+
* 새 API가 SDK에 추가되면 여기에 추가하고 별도 PR에서 mock 구현으로 이동한다.
|
|
876
|
+
* 확인되지 않은 이름은 절대 여기에 추가하지 않는다 — throw가 더 안전하다.
|
|
1078
877
|
*/
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
20,
|
|
1099
|
-
20,
|
|
1100
|
-
20,
|
|
1101
|
-
20
|
|
1102
|
-
],
|
|
1103
|
-
confetti: [
|
|
1104
|
-
10,
|
|
1105
|
-
20,
|
|
1106
|
-
10,
|
|
1107
|
-
20,
|
|
1108
|
-
10,
|
|
1109
|
-
20,
|
|
1110
|
-
10
|
|
1111
|
-
]
|
|
1112
|
-
};
|
|
1113
|
-
async function generateHapticFeedback(options) {
|
|
1114
|
-
const timestamp = Date.now();
|
|
1115
|
-
aitState.logAnalytics({
|
|
1116
|
-
type: "haptic",
|
|
1117
|
-
params: { hapticType: options.type }
|
|
1118
|
-
});
|
|
1119
|
-
const pattern = HAPTIC_VIBRATE_PATTERN[options.type] ?? 30;
|
|
1120
|
-
const vibrated = typeof navigator.vibrate === "function" ? navigator.vibrate(pattern) : false;
|
|
1121
|
-
aitState.logSdkCall({
|
|
1122
|
-
method: "generateHapticFeedback",
|
|
1123
|
-
args: [{ type: options.type }],
|
|
1124
|
-
timestamp,
|
|
1125
|
-
status: "resolved",
|
|
1126
|
-
result: {
|
|
1127
|
-
hapticType: options.type,
|
|
1128
|
-
vibrated
|
|
1129
|
-
},
|
|
1130
|
-
fidelity: "partial"
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
async function saveBase64Data(params) {
|
|
1134
|
-
const a = document.createElement("a");
|
|
1135
|
-
a.href = `data:${params.mimeType};base64,${params.data}`;
|
|
1136
|
-
a.download = params.fileName;
|
|
1137
|
-
a.click();
|
|
878
|
+
const KNOWN_UNIMPLEMENTED = /* @__PURE__ */ new Set([]);
|
|
879
|
+
function createMockProxy(moduleName, implementations) {
|
|
880
|
+
return new Proxy(implementations, { get(target, prop) {
|
|
881
|
+
if (typeof prop === "symbol") return void 0;
|
|
882
|
+
if (prop in target) return target[prop];
|
|
883
|
+
const name = String(prop);
|
|
884
|
+
if (KNOWN_UNIMPLEMENTED.has(name)) return (...args) => {
|
|
885
|
+
console.warn(`[@ait-co/devtools] ${moduleName}.${name} is known-unimplemented (🔴 inert). Returning undefined. Please file or upvote an issue: ${ISSUES_URL}`);
|
|
886
|
+
aitState.logSdkCall({
|
|
887
|
+
method: `${moduleName}.${name}`,
|
|
888
|
+
args,
|
|
889
|
+
timestamp: Date.now(),
|
|
890
|
+
status: "resolved",
|
|
891
|
+
result: void 0,
|
|
892
|
+
fidelity: "inert"
|
|
893
|
+
});
|
|
894
|
+
};
|
|
895
|
+
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}`);
|
|
896
|
+
} });
|
|
1138
897
|
}
|
|
1139
898
|
//#endregion
|
|
1140
|
-
//#region src/mock/device/
|
|
899
|
+
//#region src/mock/device/storage.ts
|
|
1141
900
|
/**
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
901
|
+
* Storage mock
|
|
902
|
+
* localStorage에 `__ait_storage:` prefix로 저장하여 앱 자체 localStorage와 분리
|
|
1144
903
|
*/
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
904
|
+
const Storage = createMockProxy("Storage", {
|
|
905
|
+
getItem: async (key) => {
|
|
906
|
+
return localStorage.getItem(`__ait_storage:${key}`);
|
|
907
|
+
},
|
|
908
|
+
setItem: async (key, value) => {
|
|
909
|
+
localStorage.setItem(`__ait_storage:${key}`, value);
|
|
910
|
+
},
|
|
911
|
+
removeItem: async (key) => {
|
|
912
|
+
localStorage.removeItem(`__ait_storage:${key}`);
|
|
913
|
+
},
|
|
914
|
+
clearItems: async () => {
|
|
915
|
+
const keys = Object.keys(localStorage).filter((k) => k.startsWith("__ait_storage:"));
|
|
916
|
+
for (const k of keys) localStorage.removeItem(k);
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/mock/observe.ts
|
|
921
|
+
/**
|
|
922
|
+
* fn을 observe로 감싼다.
|
|
923
|
+
*
|
|
924
|
+
* @param apiName - 로그에 기록할 SDK 메서드 이름 (예: `'setScreenAwakeMode'`)
|
|
925
|
+
* @param fidelity - 이 mock의 fidelity grade ('faithful' | 'partial' | 'inert')
|
|
926
|
+
* @param fn - 실제 mock 구현체. 시그니처를 그대로 통과시킨다.
|
|
927
|
+
* @returns fn과 동일한 타입의 래퍼 함수
|
|
928
|
+
*/
|
|
929
|
+
function observe(apiName, fidelity, fn) {
|
|
930
|
+
return (...args) => {
|
|
931
|
+
const timestamp = Date.now();
|
|
932
|
+
const safeArgs = args.map((a) => safeSerialize(a));
|
|
933
|
+
const result = fn(...args);
|
|
934
|
+
if (result instanceof Promise) {
|
|
935
|
+
aitState.logSdkCall({
|
|
936
|
+
method: apiName,
|
|
937
|
+
args: safeArgs,
|
|
938
|
+
timestamp,
|
|
939
|
+
status: "pending",
|
|
940
|
+
fidelity
|
|
941
|
+
});
|
|
942
|
+
result.then((value) => {
|
|
943
|
+
aitState.logSdkCall({
|
|
944
|
+
method: apiName,
|
|
945
|
+
args: safeArgs,
|
|
946
|
+
timestamp,
|
|
947
|
+
status: "resolved",
|
|
948
|
+
result: safeSerialize(value),
|
|
949
|
+
fidelity
|
|
950
|
+
});
|
|
951
|
+
}, (err) => {
|
|
952
|
+
aitState.logSdkCall({
|
|
953
|
+
method: apiName,
|
|
954
|
+
args: safeArgs,
|
|
955
|
+
timestamp,
|
|
956
|
+
status: "rejected",
|
|
957
|
+
error: err instanceof Error ? err.message : String(err),
|
|
958
|
+
fidelity
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
963
|
+
aitState.logSdkCall({
|
|
964
|
+
method: apiName,
|
|
965
|
+
args: safeArgs,
|
|
966
|
+
timestamp,
|
|
967
|
+
status: "resolved",
|
|
968
|
+
result: safeSerialize(result),
|
|
969
|
+
fidelity
|
|
970
|
+
});
|
|
971
|
+
return result;
|
|
1159
972
|
};
|
|
1160
973
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
974
|
+
/**
|
|
975
|
+
* 값을 JSON-safe한 형태로 변환한다.
|
|
976
|
+
* - null / primitive — 그대로.
|
|
977
|
+
* - 함수 — `'[Function: name]'` 문자열.
|
|
978
|
+
* - 기타 객체 — JSON.stringify 실패 시 `'[unserializable]'`.
|
|
979
|
+
*/
|
|
980
|
+
function safeSerialize(value) {
|
|
981
|
+
if (value === null || value === void 0) return value;
|
|
982
|
+
if (typeof value === "function") return `[Function: ${value.name || "anonymous"}]`;
|
|
983
|
+
if (typeof value !== "object") return value;
|
|
984
|
+
try {
|
|
985
|
+
return JSON.parse(JSON.stringify(value));
|
|
986
|
+
} catch {
|
|
987
|
+
return "[unserializable]";
|
|
988
|
+
}
|
|
1163
989
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
resolve({
|
|
1173
|
-
coords: {
|
|
1174
|
-
latitude: pos.coords.latitude,
|
|
1175
|
-
longitude: pos.coords.longitude,
|
|
1176
|
-
altitude: pos.coords.altitude ?? 0,
|
|
1177
|
-
accuracy: pos.coords.accuracy,
|
|
1178
|
-
altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
|
|
1179
|
-
heading: pos.coords.heading ?? 0
|
|
1180
|
-
},
|
|
1181
|
-
timestamp: pos.timestamp,
|
|
1182
|
-
accessLocation: "FINE"
|
|
1183
|
-
});
|
|
1184
|
-
}, () => {
|
|
1185
|
-
console.warn("[@ait-co/devtools] Geolocation failed, falling back to mock");
|
|
1186
|
-
resolve(buildLocation());
|
|
1187
|
-
});
|
|
1188
|
-
});
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/mock/navigation/index.ts
|
|
992
|
+
/**
|
|
993
|
+
* 화면/네비게이션/이벤트 mock
|
|
994
|
+
*/
|
|
995
|
+
async function closeView() {
|
|
996
|
+
console.log("[@ait-co/devtools] closeView called");
|
|
997
|
+
window.history.back();
|
|
1189
998
|
}
|
|
1190
|
-
async function
|
|
1191
|
-
|
|
999
|
+
async function openURL(url) {
|
|
1000
|
+
console.log("[@ait-co/devtools] openURL:", url);
|
|
1001
|
+
window.open(url, "_blank");
|
|
1192
1002
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
};
|
|
1200
|
-
const getCurrentLocation = withPermission(_getCurrentLocation, "geolocation");
|
|
1201
|
-
function startUpdateLocationMock(eventParams) {
|
|
1202
|
-
const { onEvent, options } = eventParams;
|
|
1203
|
-
const interval = Math.max(options.timeInterval, 500);
|
|
1204
|
-
const id = setInterval(() => {
|
|
1205
|
-
const loc = buildLocation();
|
|
1206
|
-
loc.coords.latitude += (Math.random() - .5) * 1e-4;
|
|
1207
|
-
loc.coords.longitude += (Math.random() - .5) * 1e-4;
|
|
1208
|
-
onEvent(loc);
|
|
1209
|
-
}, interval);
|
|
1210
|
-
return () => clearInterval(id);
|
|
1003
|
+
async function share(message) {
|
|
1004
|
+
if (navigator.share) {
|
|
1005
|
+
await navigator.share({ text: message.message });
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
console.log("[@ait-co/devtools] share:", message.message);
|
|
1211
1009
|
}
|
|
1212
|
-
function
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1010
|
+
async function getTossShareLink(path, _ogImageUrl) {
|
|
1011
|
+
return `https://toss.im/share/mock${path}`;
|
|
1012
|
+
}
|
|
1013
|
+
async function setIosSwipeGestureEnabled(options) {
|
|
1014
|
+
console.log("[@ait-co/devtools] setIosSwipeGestureEnabled:", options.isEnabled);
|
|
1015
|
+
aitState.patch("navigation", { iosSwipeGestureEnabled: options.isEnabled });
|
|
1016
|
+
}
|
|
1017
|
+
async function setDeviceOrientation(options) {
|
|
1018
|
+
const current = aitState.state.viewport.orientation;
|
|
1019
|
+
if (current === "auto") {
|
|
1020
|
+
console.log("[@ait-co/devtools] setDeviceOrientation:", options.type);
|
|
1021
|
+
aitState.patch("viewport", { appOrientation: options.type });
|
|
1022
|
+
return;
|
|
1217
1023
|
}
|
|
1218
|
-
|
|
1219
|
-
onEvent({
|
|
1220
|
-
coords: {
|
|
1221
|
-
latitude: pos.coords.latitude,
|
|
1222
|
-
longitude: pos.coords.longitude,
|
|
1223
|
-
altitude: pos.coords.altitude ?? 0,
|
|
1224
|
-
accuracy: pos.coords.accuracy,
|
|
1225
|
-
altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
|
|
1226
|
-
heading: pos.coords.heading ?? 0
|
|
1227
|
-
},
|
|
1228
|
-
timestamp: pos.timestamp,
|
|
1229
|
-
accessLocation: "FINE"
|
|
1230
|
-
});
|
|
1231
|
-
}, (err) => onError(err));
|
|
1232
|
-
return () => navigator.geolocation.clearWatch(watchId);
|
|
1024
|
+
console.warn(`[@ait-co/devtools] setDeviceOrientation(${options.type}) ignored — Panel is forcing "${current}". Change the Viewport tab's orientation to "auto" to let the app control rotation.`);
|
|
1233
1025
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1026
|
+
const setScreenAwakeMode = observe("setScreenAwakeMode", "inert", async (options) => {
|
|
1027
|
+
console.log("[@ait-co/devtools] setScreenAwakeMode:", options.enabled);
|
|
1028
|
+
return { enabled: options.enabled };
|
|
1029
|
+
});
|
|
1030
|
+
const setSecureScreen = observe("setSecureScreen", "inert", async (options) => {
|
|
1031
|
+
console.log("[@ait-co/devtools] setSecureScreen:", options.enabled);
|
|
1032
|
+
return { enabled: options.enabled };
|
|
1033
|
+
});
|
|
1034
|
+
const requestReview = observe("requestReview", "inert", async () => {
|
|
1035
|
+
console.log("[@ait-co/devtools] requestReview called");
|
|
1036
|
+
});
|
|
1037
|
+
requestReview.isSupported = () => true;
|
|
1038
|
+
function getPlatformOS() {
|
|
1039
|
+
return aitState.state.platform;
|
|
1040
|
+
}
|
|
1041
|
+
function getOperationalEnvironment() {
|
|
1042
|
+
return aitState.state.environment;
|
|
1043
|
+
}
|
|
1044
|
+
function getTossAppVersion() {
|
|
1045
|
+
return aitState.state.appVersion;
|
|
1046
|
+
}
|
|
1047
|
+
function isMinVersionSupported(minVersions) {
|
|
1048
|
+
const required = aitState.state.platform === "ios" ? minVersions.ios : minVersions.android;
|
|
1049
|
+
if (required === "always") return true;
|
|
1050
|
+
if (required === "never") return false;
|
|
1051
|
+
const current = aitState.state.appVersion.split(".").map(Number);
|
|
1052
|
+
const min = required.split(".").map(Number);
|
|
1053
|
+
for (let i = 0; i < 3; i++) {
|
|
1054
|
+
if ((current[i] ?? 0) > (min[i] ?? 0)) return true;
|
|
1055
|
+
if ((current[i] ?? 0) < (min[i] ?? 0)) return false;
|
|
1056
|
+
}
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
function getSchemeUri() {
|
|
1060
|
+
return aitState.state.schemeUri || window.location.pathname;
|
|
1061
|
+
}
|
|
1062
|
+
function getLocale() {
|
|
1063
|
+
return aitState.state.locale;
|
|
1064
|
+
}
|
|
1065
|
+
function getDeviceId() {
|
|
1066
|
+
return aitState.state.deviceId;
|
|
1067
|
+
}
|
|
1068
|
+
function getGroupId() {
|
|
1069
|
+
return aitState.state.groupId;
|
|
1070
|
+
}
|
|
1071
|
+
async function getNetworkStatus() {
|
|
1072
|
+
const modeResult = getNetworkStatusByMode();
|
|
1073
|
+
if (modeResult) return modeResult;
|
|
1074
|
+
return aitState.state.networkStatus;
|
|
1075
|
+
}
|
|
1076
|
+
async function getServerTime() {
|
|
1077
|
+
return Date.now();
|
|
1078
|
+
}
|
|
1079
|
+
getServerTime.isSupported = () => true;
|
|
1080
|
+
/**
|
|
1081
|
+
* 현재 backEvent 구독자 수. graniteEvent.addEventListener('backEvent', …)가
|
|
1082
|
+
* 증가시키고, 반환된 cleanup이 감소시킨다. 호스트 back 메시지 처리 시 인터셉트
|
|
1083
|
+
* 여부를 판단하는 데 쓰인다.
|
|
1084
|
+
*
|
|
1085
|
+
* @internal 테스트 및 safe-area-bridge에서만 사용.
|
|
1086
|
+
*/
|
|
1087
|
+
let _backEventSubscriberCount = 0;
|
|
1088
|
+
const graniteEvent = { addEventListener(event, { onEvent, onError }) {
|
|
1089
|
+
const handler = () => {
|
|
1090
|
+
try {
|
|
1091
|
+
onEvent();
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
window.addEventListener(`__ait:${event}`, handler);
|
|
1097
|
+
if (event === "backEvent") _backEventSubscriberCount++;
|
|
1098
|
+
let cleaned = false;
|
|
1099
|
+
return () => {
|
|
1100
|
+
if (cleaned) return;
|
|
1101
|
+
cleaned = true;
|
|
1102
|
+
window.removeEventListener(`__ait:${event}`, handler);
|
|
1103
|
+
if (event === "backEvent") _backEventSubscriberCount--;
|
|
1104
|
+
};
|
|
1105
|
+
} };
|
|
1106
|
+
/**
|
|
1107
|
+
* 호스트 back 내비게이션을 처리한다.
|
|
1108
|
+
*
|
|
1109
|
+
* backEvent 구독자가 1명 이상이면 `window.dispatchEvent(new CustomEvent('__ait:backEvent'))`만
|
|
1110
|
+
* 발사한다 — 미니앱이 back을 가로채는(intercept) 채널이고 실제 토스 호스트와 동일한 시맨틱.
|
|
1111
|
+
* 구독자가 없으면 `history.back()`을 호출해 기본 브라우저 뒤로가기를 수행한다.
|
|
1112
|
+
*
|
|
1113
|
+
* env 1 패널의 back 버튼(`src/panel/viewport.ts` `aitState.trigger('backEvent')`)과
|
|
1114
|
+
* 동일한 경로를 거쳐 back 시맨틱의 단일 소유처를 navigation 모듈에 유지한다.
|
|
1115
|
+
*/
|
|
1116
|
+
function dispatchHostBackNavigation() {
|
|
1117
|
+
if (_backEventSubscriberCount > 0) window.dispatchEvent(new CustomEvent("__ait:backEvent"));
|
|
1118
|
+
else history.back();
|
|
1119
|
+
}
|
|
1120
|
+
const appsInTossEvent = { addEventListener(_event, _handlers) {
|
|
1121
|
+
return () => {};
|
|
1122
|
+
} };
|
|
1123
|
+
const tdsEvent = { addEventListener(event, { onEvent }) {
|
|
1236
1124
|
const handler = (e) => {
|
|
1237
|
-
|
|
1125
|
+
const detail = e.detail;
|
|
1126
|
+
onEvent(detail);
|
|
1238
1127
|
};
|
|
1239
|
-
window.addEventListener(
|
|
1240
|
-
window.
|
|
1241
|
-
|
|
1128
|
+
window.addEventListener(`__ait:${event}`, handler);
|
|
1129
|
+
return () => window.removeEventListener(`__ait:${event}`, handler);
|
|
1130
|
+
} };
|
|
1131
|
+
/**
|
|
1132
|
+
* @deprecated web-framework 3.0 에서 제거됨. 2.x 소비자 back-compat용으로 유지.
|
|
1133
|
+
*/
|
|
1134
|
+
function onVisibilityChangedByTransparentServiceWeb(eventParams) {
|
|
1135
|
+
const handler = () => eventParams.onEvent(!document.hidden);
|
|
1136
|
+
document.addEventListener("visibilitychange", handler);
|
|
1137
|
+
return () => document.removeEventListener("visibilitychange", handler);
|
|
1242
1138
|
}
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1139
|
+
const env = { getDeploymentId: () => aitState.state.deploymentId };
|
|
1140
|
+
function getAppsInTossGlobals() {
|
|
1141
|
+
return {
|
|
1142
|
+
deploymentId: aitState.state.deploymentId,
|
|
1143
|
+
brandDisplayName: aitState.state.brand.displayName,
|
|
1144
|
+
brandIcon: aitState.state.brand.icon,
|
|
1145
|
+
brandPrimaryColor: aitState.state.brand.primaryColor
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
const SafeAreaInsets = {
|
|
1149
|
+
get: () => ({ ...aitState.state.safeAreaInsets }),
|
|
1150
|
+
subscribe: ({ onEvent }) => {
|
|
1151
|
+
return aitState.subscribe(() => onEvent({ ...aitState.state.safeAreaInsets }));
|
|
1152
|
+
}
|
|
1248
1153
|
};
|
|
1249
|
-
|
|
1154
|
+
/** @deprecated */
|
|
1155
|
+
function getSafeAreaInsets() {
|
|
1156
|
+
return aitState.state.safeAreaInsets.top;
|
|
1157
|
+
}
|
|
1158
|
+
//#endregion
|
|
1159
|
+
//#region src/mock/safe-area-bridge.ts
|
|
1160
|
+
/**
|
|
1161
|
+
* env-2 postMessage bridges (#484, #510).
|
|
1162
|
+
*
|
|
1163
|
+
* In the AITC Sandbox PWA (env 2) the dev app runs inside the launcher's
|
|
1164
|
+
* full-viewport `<iframe>`. The launcher is the top-level document, so its
|
|
1165
|
+
* `env(safe-area-inset-*)` measurement is the ground truth for the real device
|
|
1166
|
+
* geometry. The framed page's mock would otherwise report a synthetic preset
|
|
1167
|
+
* value (e.g. top=54), which sdk-example then double-pads on top of a viewport
|
|
1168
|
+
* that already starts below the status bar — the env-2 "dead band" defect.
|
|
1169
|
+
*
|
|
1170
|
+
* This module installs receive-half listeners for two message types:
|
|
1171
|
+
*
|
|
1172
|
+
* 1. `ait:safe-area-insets` (#484): the launcher forwards its real env() insets
|
|
1173
|
+
* to the framed page on iframe load and resize/orientationchange. Validates the
|
|
1174
|
+
* envelope and writes real insets into the mock SafeAreaInsets state, firing the
|
|
1175
|
+
* subscribe path (see navigation/index.ts) so apps that subscribe re-read the
|
|
1176
|
+
* corrected values.
|
|
1177
|
+
*
|
|
1178
|
+
* 2. `ait:navigate-back` (#510): the launcher partner bar's `←` button posts this
|
|
1179
|
+
* command to the framed page. The receive half calls `dispatchHostBackNavigation()`
|
|
1180
|
+
* (navigation/index.ts): if backEvent subscribers are present, a `__ait:backEvent`
|
|
1181
|
+
* CustomEvent is dispatched (the mini-app intercept channel, matching the env-1
|
|
1182
|
+
* panel path); otherwise `history.back()` is called. No data other than `type` is
|
|
1183
|
+
* read from or written to the message — shape validation rejects anything that
|
|
1184
|
+
* carries extra fields with the wrong type. Apps that do not install this mock
|
|
1185
|
+
* (older builds) silently ignore the message (natural no-op).
|
|
1186
|
+
*
|
|
1187
|
+
* Origin policy: neither message type carries sensitive data, so we do NOT
|
|
1188
|
+
* restrict by origin — the launcher posts cross-origin from a *.trycloudflare.com
|
|
1189
|
+
* tunnel with targetOrigin '*'. Shape validation is still mandatory: a malformed
|
|
1190
|
+
* or out-of-range message is silently ignored so a stray postMessage can never
|
|
1191
|
+
* corrupt the mock state or trigger spurious navigation.
|
|
1192
|
+
*
|
|
1193
|
+
* Message-driven by design: env 1 (desktop browser, no launcher) never receives
|
|
1194
|
+
* these messages, so the panel preset stays authoritative there with zero special
|
|
1195
|
+
* casing here.
|
|
1196
|
+
*/
|
|
1197
|
+
/** The postMessage envelope the launcher posts to the framed dev app (inset forward). */
|
|
1198
|
+
const SAFE_AREA_INSETS_MESSAGE_TYPE = "ait:safe-area-insets";
|
|
1199
|
+
/**
|
|
1200
|
+
* The postMessage command the launcher partner bar's `←` button sends to the
|
|
1201
|
+
* framed dev app (#510). The framed page calls `history.back()` in response.
|
|
1202
|
+
*
|
|
1203
|
+
* Protocol: only `{ type: 'ait:navigate-back' }` is valid. No other fields are
|
|
1204
|
+
* read or acted on — extra fields are silently ignored by the shape guard.
|
|
1205
|
+
* Game variant never sends this message (back button is partner-bar-only).
|
|
1206
|
+
*/
|
|
1207
|
+
const NAVIGATE_BACK_MESSAGE_TYPE = "ait:navigate-back";
|
|
1208
|
+
const MAX_INSET_PX = 200;
|
|
1209
|
+
function isValidInset(value) {
|
|
1210
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= MAX_INSET_PX;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Parse + validate a raw postMessage payload into a `SafeAreaInsets`, or return
|
|
1214
|
+
* null when it is not a well-formed `ait:safe-area-insets` message. Pure — unit
|
|
1215
|
+
* tested without a real MessageEvent.
|
|
1216
|
+
*/
|
|
1217
|
+
function parseSafeAreaInsetsMessage(data) {
|
|
1218
|
+
if (typeof data !== "object" || data === null) return null;
|
|
1219
|
+
if (data.type !== "ait:safe-area-insets") return null;
|
|
1220
|
+
const insets = data.insets;
|
|
1221
|
+
if (typeof insets !== "object" || insets === null) return null;
|
|
1222
|
+
const { top, bottom, left, right } = insets;
|
|
1223
|
+
if (!isValidInset(top) || !isValidInset(bottom) || !isValidInset(left) || !isValidInset(right)) return null;
|
|
1224
|
+
return {
|
|
1225
|
+
top,
|
|
1226
|
+
bottom,
|
|
1227
|
+
left,
|
|
1228
|
+
right
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Apply forwarded insets to the mock state. Skips the write (and the resulting
|
|
1233
|
+
* subscribe notify) when nothing changed, so repeated identical messages from a
|
|
1234
|
+
* resize storm don't churn subscribers.
|
|
1235
|
+
*/
|
|
1236
|
+
function applyForwardedSafeAreaInsets(insets) {
|
|
1237
|
+
const current = aitState.state.safeAreaInsets;
|
|
1238
|
+
if (current.top === insets.top && current.bottom === insets.bottom && current.left === insets.left && current.right === insets.right) return;
|
|
1239
|
+
aitState.update({ safeAreaInsets: insets });
|
|
1240
|
+
}
|
|
1241
|
+
let installed = false;
|
|
1242
|
+
/**
|
|
1243
|
+
* Install the window `message` listener that receives forwarded insets. Safe to
|
|
1244
|
+
* call multiple times (idempotent) and a no-op outside a browser (SSR/jsdom
|
|
1245
|
+
* without a window). Imported for its side effect by the mock barrel so any
|
|
1246
|
+
* consumer that aliases `@apps-in-toss/web-framework` to the mock gets it wired.
|
|
1247
|
+
*/
|
|
1248
|
+
function installSafeAreaInsetsBridge() {
|
|
1249
|
+
if (installed || typeof window === "undefined") return;
|
|
1250
|
+
installed = true;
|
|
1251
|
+
window.addEventListener("message", (event) => {
|
|
1252
|
+
const insets = parseSafeAreaInsetsMessage(event.data);
|
|
1253
|
+
if (insets) applyForwardedSafeAreaInsets(insets);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Parse a raw postMessage payload as an `ait:navigate-back` command.
|
|
1258
|
+
*
|
|
1259
|
+
* Returns true when the payload is a well-formed navigate-back command
|
|
1260
|
+
* (`{ type: 'ait:navigate-back' }`), false otherwise. Pure — unit tested
|
|
1261
|
+
* without a real MessageEvent.
|
|
1262
|
+
*
|
|
1263
|
+
* Shape guard: only the `type` field is inspected; any extra fields are
|
|
1264
|
+
* ignored so future extensions do not break older receivers. The function
|
|
1265
|
+
* does NOT read any data field beyond `type` — no sensitive values, no host
|
|
1266
|
+
* disclosure (same principle as the insets bridge).
|
|
1267
|
+
*/
|
|
1268
|
+
function isNavigateBackMessage(data) {
|
|
1269
|
+
if (typeof data !== "object" || data === null) return false;
|
|
1270
|
+
return data.type === NAVIGATE_BACK_MESSAGE_TYPE;
|
|
1271
|
+
}
|
|
1272
|
+
let navigateBackInstalled = false;
|
|
1273
|
+
/**
|
|
1274
|
+
* Install the window `message` listener that handles `ait:navigate-back`
|
|
1275
|
+
* commands (#510). When the launcher partner bar's `←` button is clicked it
|
|
1276
|
+
* posts `{ type: 'ait:navigate-back' }` to the framed dev app; this listener
|
|
1277
|
+
* calls `dispatchHostBackNavigation()` from the navigation module.
|
|
1278
|
+
*
|
|
1279
|
+
* Dispatch semantics: if there are any `graniteEvent.addEventListener('backEvent', …)`
|
|
1280
|
+
* subscribers the CustomEvent `__ait:backEvent` is fired (same path as the env-1
|
|
1281
|
+
* panel back button — the mini-app intercept channel). When there are no
|
|
1282
|
+
* subscribers `history.back()` is called as the fallback. Back semantics are
|
|
1283
|
+
* owned entirely by the navigation module; this bridge only delegates.
|
|
1284
|
+
*
|
|
1285
|
+
* Safe to call multiple times (idempotent) and a no-op outside a browser.
|
|
1286
|
+
* Installed together with the inset bridge by `installBridges()` so any consumer
|
|
1287
|
+
* of the mock barrel gets both wired automatically.
|
|
1288
|
+
*
|
|
1289
|
+
* No-op on apps that predate this bridge — the launcher posts the message but
|
|
1290
|
+
* older mocks simply have no listener (harmless).
|
|
1291
|
+
*/
|
|
1292
|
+
function installNavigateBackBridge() {
|
|
1293
|
+
if (navigateBackInstalled || typeof window === "undefined") return;
|
|
1294
|
+
navigateBackInstalled = true;
|
|
1295
|
+
window.addEventListener("message", (event) => {
|
|
1296
|
+
if (isNavigateBackMessage(event.data)) dispatchHostBackNavigation();
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Install both env-2 postMessage bridges in one call (#484 insets + #510
|
|
1301
|
+
* navigate-back). The mock barrel calls this at import time so consumers get
|
|
1302
|
+
* all bridges wired without any explicit setup.
|
|
1303
|
+
*/
|
|
1304
|
+
function installBridges() {
|
|
1305
|
+
installSafeAreaInsetsBridge();
|
|
1306
|
+
installNavigateBackBridge();
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1309
|
+
//#region src/mock/ads/index.ts
|
|
1310
|
+
/**
|
|
1311
|
+
* 광고 mock (GoogleAdMob, TossAds, FullScreenAd)
|
|
1312
|
+
*
|
|
1313
|
+
* 변경 이력 (#196):
|
|
1314
|
+
* - slot 레지스트리로 TossAds destroy/destroyAll 누수 수정 (🟡→🟢)
|
|
1315
|
+
* - attachBanner BannerSlotCallbacks 발화 (onAdRendered/onAdImpression/onNoFill 등)
|
|
1316
|
+
* - initialize onInitialized/onInitializationFailed 발화
|
|
1317
|
+
* - AdMob reward 파라미터화 (state.ads.rewardUnitType/rewardAmount)
|
|
1318
|
+
* - 모든 호출 observe()로 sdkCallLog에 기록
|
|
1319
|
+
*/
|
|
1320
|
+
function withIsSupported(fn) {
|
|
1321
|
+
fn.isSupported = () => true;
|
|
1322
|
+
return fn;
|
|
1323
|
+
}
|
|
1324
|
+
const _slotRegistry = /* @__PURE__ */ new Map();
|
|
1325
|
+
let _slotCounter = 0;
|
|
1326
|
+
function _nextSlotId(adGroupId) {
|
|
1327
|
+
_slotCounter += 1;
|
|
1328
|
+
return `mock-slot-${adGroupId}-${_slotCounter}`;
|
|
1329
|
+
}
|
|
1330
|
+
const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
1331
|
+
loadAppsInTossAdMob: withIsSupported(observe("GoogleAdMob.loadAppsInTossAdMob", "faithful", (args) => {
|
|
1332
|
+
setTimeout(() => {
|
|
1333
|
+
if (aitState.state.ads.forceNoFill) {
|
|
1334
|
+
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
aitState.patch("ads", { isLoaded: true });
|
|
1338
|
+
args.onEvent({
|
|
1339
|
+
type: "loaded",
|
|
1340
|
+
data: { adGroupId: args.options?.adGroupId }
|
|
1341
|
+
});
|
|
1342
|
+
}, 200);
|
|
1343
|
+
return () => {};
|
|
1344
|
+
})),
|
|
1345
|
+
showAppsInTossAdMob: withIsSupported(observe("GoogleAdMob.showAppsInTossAdMob", "faithful", (args) => {
|
|
1346
|
+
if (!aitState.state.ads.isLoaded) {
|
|
1347
|
+
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
1348
|
+
return () => {};
|
|
1349
|
+
}
|
|
1350
|
+
setTimeout(() => args.onEvent({ type: "requested" }), 50);
|
|
1351
|
+
setTimeout(() => args.onEvent({ type: "show" }), 100);
|
|
1352
|
+
setTimeout(() => args.onEvent({ type: "impression" }), 150);
|
|
1353
|
+
setTimeout(() => {
|
|
1354
|
+
const { rewardUnitType, rewardAmount } = aitState.state.ads;
|
|
1355
|
+
args.onEvent({
|
|
1356
|
+
type: "userEarnedReward",
|
|
1357
|
+
data: {
|
|
1358
|
+
unitType: rewardUnitType,
|
|
1359
|
+
unitAmount: rewardAmount
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
}, 1e3);
|
|
1363
|
+
setTimeout(() => {
|
|
1364
|
+
args.onEvent({ type: "dismissed" });
|
|
1365
|
+
aitState.patch("ads", { isLoaded: false });
|
|
1366
|
+
}, 1500);
|
|
1367
|
+
return () => {};
|
|
1368
|
+
})),
|
|
1369
|
+
isAppsInTossAdMobLoaded: withIsSupported(observe("GoogleAdMob.isAppsInTossAdMobLoaded", "faithful", async (_options) => aitState.state.ads.isLoaded))
|
|
1370
|
+
});
|
|
1371
|
+
const TossAds = createMockProxy("TossAds", {
|
|
1372
|
+
initialize: withIsSupported(observe("TossAds.initialize", "partial", (options) => {
|
|
1373
|
+
if (aitState.state.ads.forceNoFill) {
|
|
1374
|
+
options.callbacks?.onInitializationFailed?.(/* @__PURE__ */ new Error("No fill"));
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
options.callbacks?.onInitialized?.();
|
|
1378
|
+
})),
|
|
1379
|
+
attach: withIsSupported(observe("TossAds.attach", "partial", (_adGroupId, target, _options) => {
|
|
1380
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
1381
|
+
if (el) {
|
|
1382
|
+
const placeholder = document.createElement("div");
|
|
1383
|
+
placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:16px;text-align:center;color:#666;font-size:14px;";
|
|
1384
|
+
placeholder.textContent = "[@ait-co/devtools] TossAds Placeholder";
|
|
1385
|
+
el.appendChild(placeholder);
|
|
1386
|
+
}
|
|
1387
|
+
})),
|
|
1388
|
+
attachBanner: withIsSupported(observe("TossAds.attachBanner", "faithful", (adGroupId, target, options) => {
|
|
1389
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
1390
|
+
const slotId = _nextSlotId(adGroupId);
|
|
1391
|
+
const placeholder = document.createElement("div");
|
|
1392
|
+
const theme = options?.theme ?? "auto";
|
|
1393
|
+
const variant = options?.variant ?? "card";
|
|
1394
|
+
const isDark = theme === "dark" || theme === "auto" && typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
1395
|
+
const bg = isDark ? "#1a1a1a" : "#f0f0f0";
|
|
1396
|
+
const textColor = isDark ? "#aaa" : "#666";
|
|
1397
|
+
const borderColor = isDark ? "#555" : "#999";
|
|
1398
|
+
const height = variant === "expanded" ? "120px" : "60px";
|
|
1399
|
+
placeholder.dataset.aitSlotId = slotId;
|
|
1400
|
+
placeholder.style.cssText = `background:${bg};border:1px dashed ${borderColor};padding:8px 12px;text-align:center;color:${textColor};font-size:12px;min-height:${height};display:flex;align-items:center;justify-content:center;`;
|
|
1401
|
+
placeholder.textContent = `[@ait-co/devtools] Banner Ad (${variant})`;
|
|
1402
|
+
if (el) {
|
|
1403
|
+
el.appendChild(placeholder);
|
|
1404
|
+
_slotRegistry.set(slotId, placeholder);
|
|
1405
|
+
}
|
|
1406
|
+
const destroySlot = () => {
|
|
1407
|
+
const registered = _slotRegistry.get(slotId);
|
|
1408
|
+
if (registered) {
|
|
1409
|
+
registered.remove();
|
|
1410
|
+
_slotRegistry.delete(slotId);
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
setTimeout(() => {
|
|
1414
|
+
if (aitState.state.ads.forceNoFill) {
|
|
1415
|
+
options?.callbacks?.onNoFill?.({
|
|
1416
|
+
slotId,
|
|
1417
|
+
adGroupId,
|
|
1418
|
+
adMetadata: {}
|
|
1419
|
+
});
|
|
1420
|
+
options?.callbacks?.onAdFailedToRender?.({
|
|
1421
|
+
slotId,
|
|
1422
|
+
adGroupId,
|
|
1423
|
+
adMetadata: {},
|
|
1424
|
+
error: {
|
|
1425
|
+
code: 0,
|
|
1426
|
+
message: "No fill"
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const eventPayload = {
|
|
1432
|
+
slotId,
|
|
1433
|
+
adGroupId,
|
|
1434
|
+
adMetadata: {
|
|
1435
|
+
creativeId: `mock-creative-${slotId}`,
|
|
1436
|
+
requestId: `mock-req-${slotId}`
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
options?.callbacks?.onAdRendered?.(eventPayload);
|
|
1440
|
+
options?.callbacks?.onAdImpression?.(eventPayload);
|
|
1441
|
+
}, 100);
|
|
1442
|
+
return { destroy: destroySlot };
|
|
1443
|
+
})),
|
|
1444
|
+
destroy: withIsSupported(observe("TossAds.destroy", "faithful", (slotId) => {
|
|
1445
|
+
const el = _slotRegistry.get(slotId);
|
|
1446
|
+
if (el) {
|
|
1447
|
+
el.remove();
|
|
1448
|
+
_slotRegistry.delete(slotId);
|
|
1449
|
+
}
|
|
1450
|
+
})),
|
|
1451
|
+
destroyAll: withIsSupported(observe("TossAds.destroyAll", "faithful", () => {
|
|
1452
|
+
for (const el of _slotRegistry.values()) el.remove();
|
|
1453
|
+
_slotRegistry.clear();
|
|
1454
|
+
}))
|
|
1455
|
+
});
|
|
1456
|
+
const loadFullScreenAd = withIsSupported(observe("loadFullScreenAd", "faithful", (args) => {
|
|
1457
|
+
setTimeout(() => {
|
|
1458
|
+
if (aitState.state.ads.forceNoFill) {
|
|
1459
|
+
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
aitState.patch("ads", { isLoaded: true });
|
|
1463
|
+
args.onEvent({
|
|
1464
|
+
type: "loaded",
|
|
1465
|
+
data: { adGroupId: args.options?.adGroupId }
|
|
1466
|
+
});
|
|
1467
|
+
}, 200);
|
|
1468
|
+
return () => {};
|
|
1469
|
+
}));
|
|
1470
|
+
const showFullScreenAd = withIsSupported(observe("showFullScreenAd", "faithful", (args) => {
|
|
1471
|
+
if (!aitState.state.ads.isLoaded) {
|
|
1472
|
+
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
1473
|
+
return () => {};
|
|
1474
|
+
}
|
|
1475
|
+
setTimeout(() => args.onEvent({ type: "show" }), 100);
|
|
1476
|
+
setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
|
|
1477
|
+
return () => {};
|
|
1478
|
+
}));
|
|
1250
1479
|
//#endregion
|
|
1251
|
-
//#region src/mock/
|
|
1252
|
-
/**
|
|
1253
|
-
* Network Status mock (mode-aware helper)
|
|
1254
|
-
* navigation 모듈에서 사용. circular dep 방지를 위해 device에 위치.
|
|
1255
|
-
*/
|
|
1480
|
+
//#region src/mock/analytics/index.ts
|
|
1256
1481
|
/**
|
|
1257
|
-
*
|
|
1258
|
-
* Limitations: WIFI, 5G, WWAN cannot be detected via the Network Information API.
|
|
1259
|
-
* Falls back to state-based value when effectiveType is unavailable.
|
|
1482
|
+
* Analytics mock
|
|
1260
1483
|
*/
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1484
|
+
const Analytics = {
|
|
1485
|
+
screen: (params) => {
|
|
1486
|
+
aitState.logAnalytics({
|
|
1487
|
+
type: "screen",
|
|
1488
|
+
params: params ?? {}
|
|
1489
|
+
});
|
|
1490
|
+
return Promise.resolve();
|
|
1491
|
+
},
|
|
1492
|
+
impression: (params) => {
|
|
1493
|
+
aitState.logAnalytics({
|
|
1494
|
+
type: "impression",
|
|
1495
|
+
params: params ?? {}
|
|
1496
|
+
});
|
|
1497
|
+
return Promise.resolve();
|
|
1498
|
+
},
|
|
1499
|
+
click: (params) => {
|
|
1500
|
+
aitState.logAnalytics({
|
|
1501
|
+
type: "click",
|
|
1502
|
+
params: params ?? {}
|
|
1503
|
+
});
|
|
1504
|
+
return Promise.resolve();
|
|
1274
1505
|
}
|
|
1275
|
-
|
|
1506
|
+
};
|
|
1507
|
+
async function eventLog(params) {
|
|
1508
|
+
aitState.logAnalytics({
|
|
1509
|
+
type: params.log_type,
|
|
1510
|
+
params: {
|
|
1511
|
+
log_name: params.log_name,
|
|
1512
|
+
...params.params
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1276
1515
|
}
|
|
1277
1516
|
//#endregion
|
|
1278
|
-
//#region src/mock/
|
|
1517
|
+
//#region src/mock/auth/index.ts
|
|
1279
1518
|
/**
|
|
1280
|
-
*
|
|
1281
|
-
* mock 환경에서는 즉시 `'CLOSE'`를 반환한다.
|
|
1519
|
+
* 인증/로그인 mock
|
|
1282
1520
|
*/
|
|
1283
|
-
async function
|
|
1284
|
-
|
|
1285
|
-
|
|
1521
|
+
async function appLogin() {
|
|
1522
|
+
return {
|
|
1523
|
+
authorizationCode: `mock-auth-${crypto.randomUUID()}`,
|
|
1524
|
+
referrer: aitState.state.environment === "toss" ? "DEFAULT" : "SANDBOX"
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
async function getIsTossLoginIntegratedService() {
|
|
1528
|
+
return aitState.state.auth.isTossLoginIntegrated;
|
|
1529
|
+
}
|
|
1530
|
+
async function getUserKeyForGame() {
|
|
1531
|
+
if (!aitState.state.auth.userKeyHash) return void 0;
|
|
1532
|
+
return {
|
|
1533
|
+
hash: aitState.state.auth.userKeyHash,
|
|
1534
|
+
type: "HASH"
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
async function getAnonymousKey() {
|
|
1538
|
+
if (!aitState.state.auth.anonymousKeyHash) return void 0;
|
|
1539
|
+
return {
|
|
1540
|
+
hash: aitState.state.auth.anonymousKeyHash,
|
|
1541
|
+
type: "HASH"
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
async function appsInTossSignTossCert(_params) {
|
|
1545
|
+
console.log("[@ait-co/devtools] appsInTossSignTossCert called (no-op in mock)");
|
|
1286
1546
|
}
|
|
1287
|
-
//#endregion
|
|
1288
|
-
//#region src/mock/device/storage.ts
|
|
1289
|
-
/**
|
|
1290
|
-
* Storage mock
|
|
1291
|
-
* localStorage에 `__ait_storage:` prefix로 저장하여 앱 자체 localStorage와 분리
|
|
1292
|
-
*/
|
|
1293
|
-
const Storage = createMockProxy("Storage", {
|
|
1294
|
-
getItem: async (key) => {
|
|
1295
|
-
return localStorage.getItem(`__ait_storage:${key}`);
|
|
1296
|
-
},
|
|
1297
|
-
setItem: async (key, value) => {
|
|
1298
|
-
localStorage.setItem(`__ait_storage:${key}`, value);
|
|
1299
|
-
},
|
|
1300
|
-
removeItem: async (key) => {
|
|
1301
|
-
localStorage.removeItem(`__ait_storage:${key}`);
|
|
1302
|
-
},
|
|
1303
|
-
clearItems: async () => {
|
|
1304
|
-
const keys = Object.keys(localStorage).filter((k) => k.startsWith("__ait_storage:"));
|
|
1305
|
-
for (const k of keys) localStorage.removeItem(k);
|
|
1306
|
-
}
|
|
1307
|
-
});
|
|
1308
1547
|
//#endregion
|
|
1309
1548
|
//#region src/mock/game/index.ts
|
|
1310
1549
|
/**
|
|
@@ -1474,145 +1713,6 @@ const requestTossPayPaysBilling = Object.assign(async function requestTossPayPay
|
|
|
1474
1713
|
};
|
|
1475
1714
|
}, { isSupported: () => true });
|
|
1476
1715
|
//#endregion
|
|
1477
|
-
//#region src/mock/navigation/index.ts
|
|
1478
|
-
/**
|
|
1479
|
-
* 화면/네비게이션/이벤트 mock
|
|
1480
|
-
*/
|
|
1481
|
-
async function closeView() {
|
|
1482
|
-
console.log("[@ait-co/devtools] closeView called");
|
|
1483
|
-
window.history.back();
|
|
1484
|
-
}
|
|
1485
|
-
async function openURL(url) {
|
|
1486
|
-
console.log("[@ait-co/devtools] openURL:", url);
|
|
1487
|
-
window.open(url, "_blank");
|
|
1488
|
-
}
|
|
1489
|
-
async function share(message) {
|
|
1490
|
-
if (navigator.share) {
|
|
1491
|
-
await navigator.share({ text: message.message });
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
console.log("[@ait-co/devtools] share:", message.message);
|
|
1495
|
-
}
|
|
1496
|
-
async function getTossShareLink(path, _ogImageUrl) {
|
|
1497
|
-
return `https://toss.im/share/mock${path}`;
|
|
1498
|
-
}
|
|
1499
|
-
async function setIosSwipeGestureEnabled(options) {
|
|
1500
|
-
console.log("[@ait-co/devtools] setIosSwipeGestureEnabled:", options.isEnabled);
|
|
1501
|
-
aitState.patch("navigation", { iosSwipeGestureEnabled: options.isEnabled });
|
|
1502
|
-
}
|
|
1503
|
-
async function setDeviceOrientation(options) {
|
|
1504
|
-
const current = aitState.state.viewport.orientation;
|
|
1505
|
-
if (current === "auto") {
|
|
1506
|
-
console.log("[@ait-co/devtools] setDeviceOrientation:", options.type);
|
|
1507
|
-
aitState.patch("viewport", { appOrientation: options.type });
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
console.warn(`[@ait-co/devtools] setDeviceOrientation(${options.type}) ignored — Panel is forcing "${current}". Change the Viewport tab's orientation to "auto" to let the app control rotation.`);
|
|
1511
|
-
}
|
|
1512
|
-
const setScreenAwakeMode = observe("setScreenAwakeMode", "inert", async (options) => {
|
|
1513
|
-
console.log("[@ait-co/devtools] setScreenAwakeMode:", options.enabled);
|
|
1514
|
-
return { enabled: options.enabled };
|
|
1515
|
-
});
|
|
1516
|
-
const setSecureScreen = observe("setSecureScreen", "inert", async (options) => {
|
|
1517
|
-
console.log("[@ait-co/devtools] setSecureScreen:", options.enabled);
|
|
1518
|
-
return { enabled: options.enabled };
|
|
1519
|
-
});
|
|
1520
|
-
const requestReview = observe("requestReview", "inert", async () => {
|
|
1521
|
-
console.log("[@ait-co/devtools] requestReview called");
|
|
1522
|
-
});
|
|
1523
|
-
requestReview.isSupported = () => true;
|
|
1524
|
-
function getPlatformOS() {
|
|
1525
|
-
return aitState.state.platform;
|
|
1526
|
-
}
|
|
1527
|
-
function getOperationalEnvironment() {
|
|
1528
|
-
return aitState.state.environment;
|
|
1529
|
-
}
|
|
1530
|
-
function getTossAppVersion() {
|
|
1531
|
-
return aitState.state.appVersion;
|
|
1532
|
-
}
|
|
1533
|
-
function isMinVersionSupported(minVersions) {
|
|
1534
|
-
const required = aitState.state.platform === "ios" ? minVersions.ios : minVersions.android;
|
|
1535
|
-
if (required === "always") return true;
|
|
1536
|
-
if (required === "never") return false;
|
|
1537
|
-
const current = aitState.state.appVersion.split(".").map(Number);
|
|
1538
|
-
const min = required.split(".").map(Number);
|
|
1539
|
-
for (let i = 0; i < 3; i++) {
|
|
1540
|
-
if ((current[i] ?? 0) > (min[i] ?? 0)) return true;
|
|
1541
|
-
if ((current[i] ?? 0) < (min[i] ?? 0)) return false;
|
|
1542
|
-
}
|
|
1543
|
-
return true;
|
|
1544
|
-
}
|
|
1545
|
-
function getSchemeUri() {
|
|
1546
|
-
return aitState.state.schemeUri || window.location.pathname;
|
|
1547
|
-
}
|
|
1548
|
-
function getLocale() {
|
|
1549
|
-
return aitState.state.locale;
|
|
1550
|
-
}
|
|
1551
|
-
function getDeviceId() {
|
|
1552
|
-
return aitState.state.deviceId;
|
|
1553
|
-
}
|
|
1554
|
-
function getGroupId() {
|
|
1555
|
-
return aitState.state.groupId;
|
|
1556
|
-
}
|
|
1557
|
-
async function getNetworkStatus() {
|
|
1558
|
-
const modeResult = getNetworkStatusByMode();
|
|
1559
|
-
if (modeResult) return modeResult;
|
|
1560
|
-
return aitState.state.networkStatus;
|
|
1561
|
-
}
|
|
1562
|
-
async function getServerTime() {
|
|
1563
|
-
return Date.now();
|
|
1564
|
-
}
|
|
1565
|
-
getServerTime.isSupported = () => true;
|
|
1566
|
-
const graniteEvent = { addEventListener(event, { onEvent, onError }) {
|
|
1567
|
-
const handler = () => {
|
|
1568
|
-
try {
|
|
1569
|
-
onEvent();
|
|
1570
|
-
} catch (e) {
|
|
1571
|
-
onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
1572
|
-
}
|
|
1573
|
-
};
|
|
1574
|
-
window.addEventListener(`__ait:${event}`, handler);
|
|
1575
|
-
return () => window.removeEventListener(`__ait:${event}`, handler);
|
|
1576
|
-
} };
|
|
1577
|
-
const appsInTossEvent = { addEventListener(_event, _handlers) {
|
|
1578
|
-
return () => {};
|
|
1579
|
-
} };
|
|
1580
|
-
const tdsEvent = { addEventListener(event, { onEvent }) {
|
|
1581
|
-
const handler = (e) => {
|
|
1582
|
-
const detail = e.detail;
|
|
1583
|
-
onEvent(detail);
|
|
1584
|
-
};
|
|
1585
|
-
window.addEventListener(`__ait:${event}`, handler);
|
|
1586
|
-
return () => window.removeEventListener(`__ait:${event}`, handler);
|
|
1587
|
-
} };
|
|
1588
|
-
/**
|
|
1589
|
-
* @deprecated web-framework 3.0 에서 제거됨. 2.x 소비자 back-compat용으로 유지.
|
|
1590
|
-
*/
|
|
1591
|
-
function onVisibilityChangedByTransparentServiceWeb(eventParams) {
|
|
1592
|
-
const handler = () => eventParams.onEvent(!document.hidden);
|
|
1593
|
-
document.addEventListener("visibilitychange", handler);
|
|
1594
|
-
return () => document.removeEventListener("visibilitychange", handler);
|
|
1595
|
-
}
|
|
1596
|
-
const env = { getDeploymentId: () => aitState.state.deploymentId };
|
|
1597
|
-
function getAppsInTossGlobals() {
|
|
1598
|
-
return {
|
|
1599
|
-
deploymentId: aitState.state.deploymentId,
|
|
1600
|
-
brandDisplayName: aitState.state.brand.displayName,
|
|
1601
|
-
brandIcon: aitState.state.brand.icon,
|
|
1602
|
-
brandPrimaryColor: aitState.state.brand.primaryColor
|
|
1603
|
-
};
|
|
1604
|
-
}
|
|
1605
|
-
const SafeAreaInsets = {
|
|
1606
|
-
get: () => ({ ...aitState.state.safeAreaInsets }),
|
|
1607
|
-
subscribe: ({ onEvent }) => {
|
|
1608
|
-
return aitState.subscribe(() => onEvent({ ...aitState.state.safeAreaInsets }));
|
|
1609
|
-
}
|
|
1610
|
-
};
|
|
1611
|
-
/** @deprecated */
|
|
1612
|
-
function getSafeAreaInsets() {
|
|
1613
|
-
return aitState.state.safeAreaInsets.top;
|
|
1614
|
-
}
|
|
1615
|
-
//#endregion
|
|
1616
1716
|
//#region src/mock/notification.ts
|
|
1617
1717
|
/**
|
|
1618
1718
|
* 알림 동의 mock
|
|
@@ -1935,8 +2035,8 @@ function captureCurrentState(snapshot) {
|
|
|
1935
2035
|
* @apps-in-toss/web-framework의 모든 export를 mock으로 대체한다.
|
|
1936
2036
|
* 번들러 alias로 원본 대신 이 모듈이 resolve된다.
|
|
1937
2037
|
*/
|
|
1938
|
-
|
|
2038
|
+
installBridges();
|
|
1939
2039
|
//#endregion
|
|
1940
|
-
export { Accuracy, Analytics, FetchAlbumPhotosPermissionError, FetchContactsPermissionError, GetClipboardTextPermissionError, GetCurrentLocationPermissionError, GoogleAdMob, IAP, OpenCameraPermissionError, PermissionError, SAFE_AREA_INSETS_MESSAGE_TYPE, SafeAreaInsets, SetClipboardTextPermissionError, StartUpdateLocationPermissionError, Storage, TossAds, aitState, appLogin, applyForwardedSafeAreaInsets, applyPreset, appsInTossEvent, appsInTossSignTossCert, builtInPresets, captureCurrentState, checkoutPayment, closeView, contactsViral, deleteUserPreset, env, eventLog, fetchAlbumItems, fetchAlbumPhotos, fetchContacts, generateHapticFeedback, getAnonymousKey, getAppsInTossGlobals, getClipboardText, getCurrentLocation, getDefaultPlaceholderImages, getDeviceId, getGameCenterGameProfile, getGroupId, getIsTossLoginIntegratedService, getLocale, getNetworkStatus, getOperationalEnvironment, getPermission, getPlatformOS, getSafeAreaInsets, getSchemeUri, getServerTime, getTossAppVersion, getTossShareLink, getUserKeyForGame, graniteEvent, grantPromotionReward, grantPromotionRewardForGame, installSafeAreaInsetsBridge, isMinVersionSupported, listUserPresets, loadFullScreenAd, matchesPreset, onVisibilityChangedByTransparentServiceWeb, openCamera, openGameCenterLeaderboard, openPDFViewer, openPermissionDialog, openURL, parseSafeAreaInsetsMessage, partner, requestNotificationAgreement, requestPermission, requestReview, requestTossPayPaysBilling, saveBase64Data, saveUserPreset, setClipboardText, setDeviceOrientation, setIosSwipeGestureEnabled, setScreenAwakeMode, setSecureScreen, share, showFullScreenAd, startUpdateLocation, submitGameCenterLeaderBoardScore, tdsEvent };
|
|
2040
|
+
export { Accuracy, Analytics, FetchAlbumPhotosPermissionError, FetchContactsPermissionError, GetClipboardTextPermissionError, GetCurrentLocationPermissionError, GoogleAdMob, IAP, NAVIGATE_BACK_MESSAGE_TYPE, OpenCameraPermissionError, PermissionError, SAFE_AREA_INSETS_MESSAGE_TYPE, SafeAreaInsets, SetClipboardTextPermissionError, StartUpdateLocationPermissionError, Storage, TossAds, aitState, appLogin, applyForwardedSafeAreaInsets, applyPreset, appsInTossEvent, appsInTossSignTossCert, builtInPresets, captureCurrentState, checkoutPayment, closeView, contactsViral, deleteUserPreset, env, eventLog, fetchAlbumItems, fetchAlbumPhotos, fetchContacts, generateHapticFeedback, getAnonymousKey, getAppsInTossGlobals, getClipboardText, getCurrentLocation, getDefaultPlaceholderImages, getDeviceId, getGameCenterGameProfile, getGroupId, getIsTossLoginIntegratedService, getLocale, getNetworkStatus, getOperationalEnvironment, getPermission, getPlatformOS, getSafeAreaInsets, getSchemeUri, getServerTime, getTossAppVersion, getTossShareLink, getUserKeyForGame, graniteEvent, grantPromotionReward, grantPromotionRewardForGame, installBridges, installNavigateBackBridge, installSafeAreaInsetsBridge, isMinVersionSupported, isNavigateBackMessage, listUserPresets, loadFullScreenAd, matchesPreset, onVisibilityChangedByTransparentServiceWeb, openCamera, openGameCenterLeaderboard, openPDFViewer, openPermissionDialog, openURL, parseSafeAreaInsetsMessage, partner, requestNotificationAgreement, requestPermission, requestReview, requestTossPayPaysBilling, saveBase64Data, saveUserPreset, setClipboardText, setDeviceOrientation, setIosSwipeGestureEnabled, setScreenAwakeMode, setSecureScreen, share, showFullScreenAd, startUpdateLocation, submitGameCenterLeaderBoardScore, tdsEvent };
|
|
1941
2041
|
|
|
1942
2042
|
//# sourceMappingURL=index.js.map
|