@ait-co/devtools 0.1.31 → 0.1.33
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/README.en.md +11 -7
- package/README.md +24 -9
- package/dist/in-app/index.d.ts +68 -14
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +7 -0
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +226 -26
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +53 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mock/index.d.ts +129 -22
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +298 -54
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.d.ts.map +1 -1
- package/dist/panel/index.js +746 -115
- package/dist/panel/index.js.map +1 -1
- package/package.json +1 -1
package/dist/mock/index.js
CHANGED
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
//#region src/mock/proxy.ts
|
|
2
|
-
/**
|
|
3
|
-
* 미구현 API용 Proxy 트립와이어.
|
|
4
|
-
*
|
|
5
|
-
* 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
|
|
6
|
-
* 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
|
|
7
|
-
* mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
|
|
8
|
-
* 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
|
|
9
|
-
* 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
|
|
10
|
-
*/
|
|
11
|
-
const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
|
|
12
|
-
function createMockProxy(moduleName, implementations) {
|
|
13
|
-
return new Proxy(implementations, { get(target, prop) {
|
|
14
|
-
if (typeof prop === "symbol") return void 0;
|
|
15
|
-
if (prop in target) return target[prop];
|
|
16
|
-
throw new Error(`[@ait-co/devtools] ${moduleName}.${prop} is not mocked. This API may exist in @apps-in-toss/web-framework, but devtools' mock does not cover it yet. Please file an issue: ${ISSUES_URL}`);
|
|
17
|
-
} });
|
|
18
|
-
}
|
|
19
|
-
//#endregion
|
|
20
1
|
//#region src/mock/state.ts
|
|
2
|
+
/** SDK 호출 로그 ring buffer 상한 */
|
|
3
|
+
const SDK_CALL_LOG_MAX = 200;
|
|
21
4
|
const DEFAULT_STATE = {
|
|
22
5
|
platform: "ios",
|
|
23
6
|
environment: "sandbox",
|
|
@@ -33,6 +16,7 @@ const DEFAULT_STATE = {
|
|
|
33
16
|
primaryColor: "#3182F6"
|
|
34
17
|
},
|
|
35
18
|
networkStatus: "WIFI",
|
|
19
|
+
navigation: { iosSwipeGestureEnabled: null },
|
|
36
20
|
permissions: {
|
|
37
21
|
clipboard: "allowed",
|
|
38
22
|
contacts: "allowed",
|
|
@@ -54,7 +38,7 @@ const DEFAULT_STATE = {
|
|
|
54
38
|
accessLocation: "FINE"
|
|
55
39
|
},
|
|
56
40
|
safeAreaInsets: {
|
|
57
|
-
top:
|
|
41
|
+
top: 54,
|
|
58
42
|
bottom: 34,
|
|
59
43
|
left: 0,
|
|
60
44
|
right: 0
|
|
@@ -94,7 +78,9 @@ const DEFAULT_STATE = {
|
|
|
94
78
|
isLoaded: false,
|
|
95
79
|
nextEvent: "loaded",
|
|
96
80
|
forceNoFill: false,
|
|
97
|
-
lastEvent: null
|
|
81
|
+
lastEvent: null,
|
|
82
|
+
rewardUnitType: "coins",
|
|
83
|
+
rewardAmount: 10
|
|
98
84
|
},
|
|
99
85
|
game: {
|
|
100
86
|
profile: {
|
|
@@ -104,6 +90,7 @@ const DEFAULT_STATE = {
|
|
|
104
90
|
leaderboardScores: []
|
|
105
91
|
},
|
|
106
92
|
analyticsLog: [],
|
|
93
|
+
sdkCallLog: [],
|
|
107
94
|
deviceModes: {
|
|
108
95
|
camera: "mock",
|
|
109
96
|
photos: "mock",
|
|
@@ -216,6 +203,19 @@ var AitStateManager = class {
|
|
|
216
203
|
};
|
|
217
204
|
this._notify();
|
|
218
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* SDK 호출 로그 추가 (ring buffer, 상한 SDK_CALL_LOG_MAX).
|
|
208
|
+
* `observe()`가 호출하고, proxy의 KNOWN_UNIMPLEMENTED 경로도 직접 호출한다.
|
|
209
|
+
*/
|
|
210
|
+
logSdkCall(entry) {
|
|
211
|
+
const log = this._state.sdkCallLog;
|
|
212
|
+
const next = log.length >= SDK_CALL_LOG_MAX ? log.slice(1 - SDK_CALL_LOG_MAX) : log;
|
|
213
|
+
this._state = {
|
|
214
|
+
...this._state,
|
|
215
|
+
sdkCallLog: [...next, entry]
|
|
216
|
+
};
|
|
217
|
+
this._notify();
|
|
218
|
+
}
|
|
219
219
|
/** 이벤트 트리거 (backEvent, homeEvent 등) */
|
|
220
220
|
trigger(event) {
|
|
221
221
|
window.dispatchEvent(new CustomEvent(`__ait:${event}`));
|
|
@@ -239,16 +239,142 @@ if (!globalRef[SINGLETON_KEY]) globalRef[SINGLETON_KEY] = new AitStateManager();
|
|
|
239
239
|
const aitState = globalRef[SINGLETON_KEY];
|
|
240
240
|
if (typeof window !== "undefined") window.__ait = aitState;
|
|
241
241
|
//#endregion
|
|
242
|
+
//#region src/mock/observe.ts
|
|
243
|
+
/**
|
|
244
|
+
* fn을 observe로 감싼다.
|
|
245
|
+
*
|
|
246
|
+
* @param apiName - 로그에 기록할 SDK 메서드 이름 (예: `'setScreenAwakeMode'`)
|
|
247
|
+
* @param fidelity - 이 mock의 fidelity grade ('faithful' | 'partial' | 'inert')
|
|
248
|
+
* @param fn - 실제 mock 구현체. 시그니처를 그대로 통과시킨다.
|
|
249
|
+
* @returns fn과 동일한 타입의 래퍼 함수
|
|
250
|
+
*/
|
|
251
|
+
function observe(apiName, fidelity, fn) {
|
|
252
|
+
return (...args) => {
|
|
253
|
+
const timestamp = Date.now();
|
|
254
|
+
const safeArgs = args.map((a) => safeSerialize(a));
|
|
255
|
+
const result = fn(...args);
|
|
256
|
+
if (result instanceof Promise) {
|
|
257
|
+
aitState.logSdkCall({
|
|
258
|
+
method: apiName,
|
|
259
|
+
args: safeArgs,
|
|
260
|
+
timestamp,
|
|
261
|
+
status: "pending",
|
|
262
|
+
fidelity
|
|
263
|
+
});
|
|
264
|
+
result.then((value) => {
|
|
265
|
+
aitState.logSdkCall({
|
|
266
|
+
method: apiName,
|
|
267
|
+
args: safeArgs,
|
|
268
|
+
timestamp,
|
|
269
|
+
status: "resolved",
|
|
270
|
+
result: safeSerialize(value),
|
|
271
|
+
fidelity
|
|
272
|
+
});
|
|
273
|
+
}, (err) => {
|
|
274
|
+
aitState.logSdkCall({
|
|
275
|
+
method: apiName,
|
|
276
|
+
args: safeArgs,
|
|
277
|
+
timestamp,
|
|
278
|
+
status: "rejected",
|
|
279
|
+
error: err instanceof Error ? err.message : String(err),
|
|
280
|
+
fidelity
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
aitState.logSdkCall({
|
|
286
|
+
method: apiName,
|
|
287
|
+
args: safeArgs,
|
|
288
|
+
timestamp,
|
|
289
|
+
status: "resolved",
|
|
290
|
+
result: safeSerialize(result),
|
|
291
|
+
fidelity
|
|
292
|
+
});
|
|
293
|
+
return result;
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 값을 JSON-safe한 형태로 변환한다.
|
|
298
|
+
* - null / primitive — 그대로.
|
|
299
|
+
* - 함수 — `'[Function: name]'` 문자열.
|
|
300
|
+
* - 기타 객체 — JSON.stringify 실패 시 `'[unserializable]'`.
|
|
301
|
+
*/
|
|
302
|
+
function safeSerialize(value) {
|
|
303
|
+
if (value === null || value === void 0) return value;
|
|
304
|
+
if (typeof value === "function") return `[Function: ${value.name || "anonymous"}]`;
|
|
305
|
+
if (typeof value !== "object") return value;
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(JSON.stringify(value));
|
|
308
|
+
} catch {
|
|
309
|
+
return "[unserializable]";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/mock/proxy.ts
|
|
314
|
+
/**
|
|
315
|
+
* 미구현 API용 Proxy 트립와이어.
|
|
316
|
+
*
|
|
317
|
+
* 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
|
|
318
|
+
* 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
|
|
319
|
+
* mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
|
|
320
|
+
* 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
|
|
321
|
+
* 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
|
|
322
|
+
*
|
|
323
|
+
* ## KNOWN_UNIMPLEMENTED 정책
|
|
324
|
+
* SDK에 존재하는 것으로 알려져 있으나 현재 mock이 없는 API 이름만 이 집합에 둔다.
|
|
325
|
+
* 이 경우에만 throw 대신 🔴 inert no-op을 반환하고 sdkCallLog에 기록한다.
|
|
326
|
+
* 완전히 미지의 이름은 여전히 throw — "잘 되는 척" 방지.
|
|
327
|
+
*/
|
|
328
|
+
const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
|
|
329
|
+
/**
|
|
330
|
+
* SDK에 존재하나 mock이 아직 없는 것으로 확인된 이름 목록.
|
|
331
|
+
* 새 API가 SDK에 추가되면 여기에 추가하고 별도 PR에서 mock 구현으로 이동한다.
|
|
332
|
+
* 확인되지 않은 이름은 절대 여기에 추가하지 않는다 — throw가 더 안전하다.
|
|
333
|
+
*/
|
|
334
|
+
const KNOWN_UNIMPLEMENTED = /* @__PURE__ */ new Set([]);
|
|
335
|
+
function createMockProxy(moduleName, implementations) {
|
|
336
|
+
return new Proxy(implementations, { get(target, prop) {
|
|
337
|
+
if (typeof prop === "symbol") return void 0;
|
|
338
|
+
if (prop in target) return target[prop];
|
|
339
|
+
const name = String(prop);
|
|
340
|
+
if (KNOWN_UNIMPLEMENTED.has(name)) return (...args) => {
|
|
341
|
+
console.warn(`[@ait-co/devtools] ${moduleName}.${name} is known-unimplemented (🔴 inert). Returning undefined. Please file or upvote an issue: ${ISSUES_URL}`);
|
|
342
|
+
aitState.logSdkCall({
|
|
343
|
+
method: `${moduleName}.${name}`,
|
|
344
|
+
args,
|
|
345
|
+
timestamp: Date.now(),
|
|
346
|
+
status: "resolved",
|
|
347
|
+
result: void 0,
|
|
348
|
+
fidelity: "inert"
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
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}`);
|
|
352
|
+
} });
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
242
355
|
//#region src/mock/ads/index.ts
|
|
243
356
|
/**
|
|
244
357
|
* 광고 mock (GoogleAdMob, TossAds, FullScreenAd)
|
|
358
|
+
*
|
|
359
|
+
* 변경 이력 (#196):
|
|
360
|
+
* - slot 레지스트리로 TossAds destroy/destroyAll 누수 수정 (🟡→🟢)
|
|
361
|
+
* - attachBanner BannerSlotCallbacks 발화 (onAdRendered/onAdImpression/onNoFill 등)
|
|
362
|
+
* - initialize onInitialized/onInitializationFailed 발화
|
|
363
|
+
* - AdMob reward 파라미터화 (state.ads.rewardUnitType/rewardAmount)
|
|
364
|
+
* - 모든 호출 observe()로 sdkCallLog에 기록
|
|
245
365
|
*/
|
|
246
366
|
function withIsSupported(fn) {
|
|
247
367
|
fn.isSupported = () => true;
|
|
248
368
|
return fn;
|
|
249
369
|
}
|
|
370
|
+
const _slotRegistry = /* @__PURE__ */ new Map();
|
|
371
|
+
let _slotCounter = 0;
|
|
372
|
+
function _nextSlotId(adGroupId) {
|
|
373
|
+
_slotCounter += 1;
|
|
374
|
+
return `mock-slot-${adGroupId}-${_slotCounter}`;
|
|
375
|
+
}
|
|
250
376
|
const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
251
|
-
loadAppsInTossAdMob: withIsSupported((args) => {
|
|
377
|
+
loadAppsInTossAdMob: withIsSupported(observe("GoogleAdMob.loadAppsInTossAdMob", "faithful", (args) => {
|
|
252
378
|
setTimeout(() => {
|
|
253
379
|
if (aitState.state.ads.forceNoFill) {
|
|
254
380
|
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
@@ -261,8 +387,8 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
261
387
|
});
|
|
262
388
|
}, 200);
|
|
263
389
|
return () => {};
|
|
264
|
-
}),
|
|
265
|
-
showAppsInTossAdMob: withIsSupported((args) => {
|
|
390
|
+
})),
|
|
391
|
+
showAppsInTossAdMob: withIsSupported(observe("GoogleAdMob.showAppsInTossAdMob", "faithful", (args) => {
|
|
266
392
|
if (!aitState.state.ads.isLoaded) {
|
|
267
393
|
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
268
394
|
return () => {};
|
|
@@ -271,11 +397,12 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
271
397
|
setTimeout(() => args.onEvent({ type: "show" }), 100);
|
|
272
398
|
setTimeout(() => args.onEvent({ type: "impression" }), 150);
|
|
273
399
|
setTimeout(() => {
|
|
400
|
+
const { rewardUnitType, rewardAmount } = aitState.state.ads;
|
|
274
401
|
args.onEvent({
|
|
275
402
|
type: "userEarnedReward",
|
|
276
403
|
data: {
|
|
277
|
-
unitType:
|
|
278
|
-
unitAmount:
|
|
404
|
+
unitType: rewardUnitType,
|
|
405
|
+
unitAmount: rewardAmount
|
|
279
406
|
}
|
|
280
407
|
});
|
|
281
408
|
}, 1e3);
|
|
@@ -284,14 +411,18 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
284
411
|
aitState.patch("ads", { isLoaded: false });
|
|
285
412
|
}, 1500);
|
|
286
413
|
return () => {};
|
|
287
|
-
}),
|
|
288
|
-
isAppsInTossAdMobLoaded: withIsSupported(async (_options) => aitState.state.ads.isLoaded)
|
|
414
|
+
})),
|
|
415
|
+
isAppsInTossAdMobLoaded: withIsSupported(observe("GoogleAdMob.isAppsInTossAdMobLoaded", "faithful", async (_options) => aitState.state.ads.isLoaded))
|
|
289
416
|
});
|
|
290
417
|
const TossAds = createMockProxy("TossAds", {
|
|
291
|
-
initialize: withIsSupported((
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
418
|
+
initialize: withIsSupported(observe("TossAds.initialize", "partial", (options) => {
|
|
419
|
+
if (aitState.state.ads.forceNoFill) {
|
|
420
|
+
options.callbacks?.onInitializationFailed?.(/* @__PURE__ */ new Error("No fill"));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
options.callbacks?.onInitialized?.();
|
|
424
|
+
})),
|
|
425
|
+
attach: withIsSupported(observe("TossAds.attach", "partial", (_adGroupId, target, _options) => {
|
|
295
426
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
296
427
|
if (el) {
|
|
297
428
|
const placeholder = document.createElement("div");
|
|
@@ -299,21 +430,76 @@ const TossAds = createMockProxy("TossAds", {
|
|
|
299
430
|
placeholder.textContent = "[@ait-co/devtools] TossAds Placeholder";
|
|
300
431
|
el.appendChild(placeholder);
|
|
301
432
|
}
|
|
302
|
-
}),
|
|
303
|
-
attachBanner: withIsSupported((
|
|
433
|
+
})),
|
|
434
|
+
attachBanner: withIsSupported(observe("TossAds.attachBanner", "faithful", (adGroupId, target, options) => {
|
|
304
435
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
436
|
+
const slotId = _nextSlotId(adGroupId);
|
|
437
|
+
const placeholder = document.createElement("div");
|
|
438
|
+
const theme = options?.theme ?? "auto";
|
|
439
|
+
const variant = options?.variant ?? "card";
|
|
440
|
+
const isDark = theme === "dark" || theme === "auto" && typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
441
|
+
const bg = isDark ? "#1a1a1a" : "#f0f0f0";
|
|
442
|
+
const textColor = isDark ? "#aaa" : "#666";
|
|
443
|
+
const borderColor = isDark ? "#555" : "#999";
|
|
444
|
+
const height = variant === "expanded" ? "120px" : "60px";
|
|
445
|
+
placeholder.dataset.aitSlotId = slotId;
|
|
446
|
+
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;`;
|
|
447
|
+
placeholder.textContent = `[@ait-co/devtools] Banner Ad (${variant})`;
|
|
305
448
|
if (el) {
|
|
306
|
-
const placeholder = document.createElement("div");
|
|
307
|
-
placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:12px;text-align:center;color:#666;font-size:12px;";
|
|
308
|
-
placeholder.textContent = "[@ait-co/devtools] Banner Ad Placeholder";
|
|
309
449
|
el.appendChild(placeholder);
|
|
450
|
+
_slotRegistry.set(slotId, placeholder);
|
|
310
451
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
452
|
+
const destroySlot = () => {
|
|
453
|
+
const registered = _slotRegistry.get(slotId);
|
|
454
|
+
if (registered) {
|
|
455
|
+
registered.remove();
|
|
456
|
+
_slotRegistry.delete(slotId);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
setTimeout(() => {
|
|
460
|
+
if (aitState.state.ads.forceNoFill) {
|
|
461
|
+
options?.callbacks?.onNoFill?.({
|
|
462
|
+
slotId,
|
|
463
|
+
adGroupId,
|
|
464
|
+
adMetadata: {}
|
|
465
|
+
});
|
|
466
|
+
options?.callbacks?.onAdFailedToRender?.({
|
|
467
|
+
slotId,
|
|
468
|
+
adGroupId,
|
|
469
|
+
adMetadata: {},
|
|
470
|
+
error: {
|
|
471
|
+
code: 0,
|
|
472
|
+
message: "No fill"
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const eventPayload = {
|
|
478
|
+
slotId,
|
|
479
|
+
adGroupId,
|
|
480
|
+
adMetadata: {
|
|
481
|
+
creativeId: `mock-creative-${slotId}`,
|
|
482
|
+
requestId: `mock-req-${slotId}`
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
options?.callbacks?.onAdRendered?.(eventPayload);
|
|
486
|
+
options?.callbacks?.onAdImpression?.(eventPayload);
|
|
487
|
+
}, 100);
|
|
488
|
+
return { destroy: destroySlot };
|
|
489
|
+
})),
|
|
490
|
+
destroy: withIsSupported(observe("TossAds.destroy", "faithful", (slotId) => {
|
|
491
|
+
const el = _slotRegistry.get(slotId);
|
|
492
|
+
if (el) {
|
|
493
|
+
el.remove();
|
|
494
|
+
_slotRegistry.delete(slotId);
|
|
495
|
+
}
|
|
496
|
+
})),
|
|
497
|
+
destroyAll: withIsSupported(observe("TossAds.destroyAll", "faithful", () => {
|
|
498
|
+
for (const el of _slotRegistry.values()) el.remove();
|
|
499
|
+
_slotRegistry.clear();
|
|
500
|
+
}))
|
|
315
501
|
});
|
|
316
|
-
const loadFullScreenAd = withIsSupported((args) => {
|
|
502
|
+
const loadFullScreenAd = withIsSupported(observe("loadFullScreenAd", "faithful", (args) => {
|
|
317
503
|
setTimeout(() => {
|
|
318
504
|
if (aitState.state.ads.forceNoFill) {
|
|
319
505
|
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
@@ -326,8 +512,8 @@ const loadFullScreenAd = withIsSupported((args) => {
|
|
|
326
512
|
});
|
|
327
513
|
}, 200);
|
|
328
514
|
return () => {};
|
|
329
|
-
});
|
|
330
|
-
const showFullScreenAd = withIsSupported((args) => {
|
|
515
|
+
}));
|
|
516
|
+
const showFullScreenAd = withIsSupported(observe("showFullScreenAd", "faithful", (args) => {
|
|
331
517
|
if (!aitState.state.ads.isLoaded) {
|
|
332
518
|
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
333
519
|
return () => {};
|
|
@@ -335,7 +521,7 @@ const showFullScreenAd = withIsSupported((args) => {
|
|
|
335
521
|
setTimeout(() => args.onEvent({ type: "show" }), 100);
|
|
336
522
|
setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
|
|
337
523
|
return () => {};
|
|
338
|
-
});
|
|
524
|
+
}));
|
|
339
525
|
//#endregion
|
|
340
526
|
//#region src/mock/analytics/index.ts
|
|
341
527
|
/**
|
|
@@ -734,13 +920,70 @@ const fetchContacts = withPermission(_fetchContacts, "contacts");
|
|
|
734
920
|
//#region src/mock/device/haptic.ts
|
|
735
921
|
/**
|
|
736
922
|
* Haptic Feedback & saveBase64Data mock
|
|
923
|
+
*
|
|
924
|
+
* generateHapticFeedback — 영역 3 (하드웨어 API 관측):
|
|
925
|
+
* - 10종 HapticFeedbackType을 navigator.vibrate 패턴으로 매핑(근사, best-effort).
|
|
926
|
+
* - `typeof navigator.vibrate === 'function'` 가드 — API 없는 환경에서 throw 없이 skip.
|
|
927
|
+
* - sdkCallLog에 🟡(partial)로 기록. params: { hapticType, vibrated: boolean }.
|
|
928
|
+
* - 시그니처 불변 — __typecheck.ts의 Assert<Mock, Original> 통과.
|
|
929
|
+
*/
|
|
930
|
+
/**
|
|
931
|
+
* HapticFeedbackType 10종 → navigator.vibrate 패턴 매핑.
|
|
932
|
+
* 숫자: 진동 ms. 배열: [진동, 정지, 진동, …] 교대 패턴.
|
|
737
933
|
*/
|
|
934
|
+
const HAPTIC_VIBRATE_PATTERN = {
|
|
935
|
+
tickWeak: 10,
|
|
936
|
+
tap: 20,
|
|
937
|
+
tickMedium: 30,
|
|
938
|
+
softMedium: 40,
|
|
939
|
+
basicWeak: 15,
|
|
940
|
+
basicMedium: 50,
|
|
941
|
+
success: [
|
|
942
|
+
10,
|
|
943
|
+
40,
|
|
944
|
+
10
|
|
945
|
+
],
|
|
946
|
+
error: [
|
|
947
|
+
40,
|
|
948
|
+
30,
|
|
949
|
+
40
|
|
950
|
+
],
|
|
951
|
+
wiggle: [
|
|
952
|
+
20,
|
|
953
|
+
20,
|
|
954
|
+
20,
|
|
955
|
+
20,
|
|
956
|
+
20
|
|
957
|
+
],
|
|
958
|
+
confetti: [
|
|
959
|
+
10,
|
|
960
|
+
20,
|
|
961
|
+
10,
|
|
962
|
+
20,
|
|
963
|
+
10,
|
|
964
|
+
20,
|
|
965
|
+
10
|
|
966
|
+
]
|
|
967
|
+
};
|
|
738
968
|
async function generateHapticFeedback(options) {
|
|
739
|
-
|
|
969
|
+
const timestamp = Date.now();
|
|
740
970
|
aitState.logAnalytics({
|
|
741
971
|
type: "haptic",
|
|
742
972
|
params: { hapticType: options.type }
|
|
743
973
|
});
|
|
974
|
+
const pattern = HAPTIC_VIBRATE_PATTERN[options.type] ?? 30;
|
|
975
|
+
const vibrated = typeof navigator.vibrate === "function" ? navigator.vibrate(pattern) : false;
|
|
976
|
+
aitState.logSdkCall({
|
|
977
|
+
method: "generateHapticFeedback",
|
|
978
|
+
args: [{ type: options.type }],
|
|
979
|
+
timestamp,
|
|
980
|
+
status: "resolved",
|
|
981
|
+
result: {
|
|
982
|
+
hapticType: options.type,
|
|
983
|
+
vibrated
|
|
984
|
+
},
|
|
985
|
+
fidelity: "partial"
|
|
986
|
+
});
|
|
744
987
|
}
|
|
745
988
|
async function saveBase64Data(params) {
|
|
746
989
|
const a = document.createElement("a");
|
|
@@ -1108,8 +1351,9 @@ async function share(message) {
|
|
|
1108
1351
|
async function getTossShareLink(path, _ogImageUrl) {
|
|
1109
1352
|
return `https://toss.im/share/mock${path}`;
|
|
1110
1353
|
}
|
|
1111
|
-
async function setIosSwipeGestureEnabled(
|
|
1112
|
-
console.log("[@ait-co/devtools] setIosSwipeGestureEnabled:",
|
|
1354
|
+
async function setIosSwipeGestureEnabled(options) {
|
|
1355
|
+
console.log("[@ait-co/devtools] setIosSwipeGestureEnabled:", options.isEnabled);
|
|
1356
|
+
aitState.patch("navigation", { iosSwipeGestureEnabled: options.isEnabled });
|
|
1113
1357
|
}
|
|
1114
1358
|
async function setDeviceOrientation(options) {
|
|
1115
1359
|
const current = aitState.state.viewport.orientation;
|
|
@@ -1120,17 +1364,17 @@ async function setDeviceOrientation(options) {
|
|
|
1120
1364
|
}
|
|
1121
1365
|
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.`);
|
|
1122
1366
|
}
|
|
1123
|
-
|
|
1367
|
+
const setScreenAwakeMode = observe("setScreenAwakeMode", "inert", async (options) => {
|
|
1124
1368
|
console.log("[@ait-co/devtools] setScreenAwakeMode:", options.enabled);
|
|
1125
1369
|
return { enabled: options.enabled };
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1370
|
+
});
|
|
1371
|
+
const setSecureScreen = observe("setSecureScreen", "inert", async (options) => {
|
|
1128
1372
|
console.log("[@ait-co/devtools] setSecureScreen:", options.enabled);
|
|
1129
1373
|
return { enabled: options.enabled };
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1374
|
+
});
|
|
1375
|
+
const requestReview = observe("requestReview", "inert", async () => {
|
|
1132
1376
|
console.log("[@ait-co/devtools] requestReview called");
|
|
1133
|
-
}
|
|
1377
|
+
});
|
|
1134
1378
|
requestReview.isSupported = () => true;
|
|
1135
1379
|
function getPlatformOS() {
|
|
1136
1380
|
return aitState.state.platform;
|