@apps-in-toss/devtools 3.1.0-beta.1 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +201 -28
- package/README.md +237 -871
- package/dist/mock/2x.d.ts +3 -155
- package/dist/mock/2x.js +1305 -1559
- package/dist/mock/3x.d.ts +27 -123
- package/dist/mock/3x.js +1275 -1521
- package/dist/mock/index.d.ts +27 -123
- package/dist/mock/index.js +1275 -1521
- package/dist/panel/index.js +154 -106
- package/dist/unplugin/index.cjs +12 -147
- package/dist/unplugin/index.d.cts +408 -78
- package/dist/unplugin/index.d.ts +408 -78
- package/dist/unplugin/index.js +13 -147
- package/package.json +5 -8
- package/dist/tunnel-BvEf1qGV.js +0 -186
- package/dist/tunnel-DtCTOUlp.cjs +0 -187
- package/dist/unplugin/tunnel.cjs +0 -191
- package/dist/unplugin/tunnel.d.cts +0 -140
- package/dist/unplugin/tunnel.d.ts +0 -140
- package/dist/unplugin/tunnel.js +0 -186
package/dist/panel/index.js
CHANGED
|
@@ -25515,6 +25515,16 @@ const en = {
|
|
|
25515
25515
|
"viewport.status.aitNavBar": "AIT nav bar",
|
|
25516
25516
|
"viewport.status.aitNavBarValue": "{height}px → SafeArea top · {type}",
|
|
25517
25517
|
"viewport.orientation.autoSuffix": "{orient} (auto)",
|
|
25518
|
+
"iap.section.products": "Product Catalog ({count})",
|
|
25519
|
+
"iap.empty.products": "(no products)",
|
|
25520
|
+
"iap.btn.addProduct": "+ Add Product",
|
|
25521
|
+
"iap.btn.removeProduct": "Remove",
|
|
25522
|
+
"iap.label.untitledProduct": "(untitled)",
|
|
25523
|
+
"iap.row.sku": "SKU",
|
|
25524
|
+
"iap.row.type": "Type",
|
|
25525
|
+
"iap.row.displayName": "Display Name",
|
|
25526
|
+
"iap.row.displayAmount": "Display Amount",
|
|
25527
|
+
"iap.row.renewalCycle": "Renewal Cycle",
|
|
25518
25528
|
"iap.section.simulator": "IAP Simulator",
|
|
25519
25529
|
"iap.row.nextResult": "Next Purchase Result",
|
|
25520
25530
|
"iap.section.tossPay": "TossPay",
|
|
@@ -25570,40 +25580,7 @@ const en = {
|
|
|
25570
25580
|
"notifications.section.title": "requestNotificationAgreement",
|
|
25571
25581
|
"notifications.option.newAgreement": "newAgreement (first-time agree)",
|
|
25572
25582
|
"notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
|
|
25573
|
-
"notifications.option.agreementRejected": "agreementRejected (user declined)"
|
|
25574
|
-
"launcher.title": "AITC DevTools Launcher",
|
|
25575
|
-
"launcher.description": "Scan the terminal QR code or paste the tunnel URL.",
|
|
25576
|
-
"launcher.installCta": "Install launcher to your phone",
|
|
25577
|
-
"launcher.urlPlaceholder": "https://example.trycloudflare.com",
|
|
25578
|
-
"launcher.openBtn": "Open",
|
|
25579
|
-
"launcher.scanBtn": "Scan QR with camera",
|
|
25580
|
-
"launcher.noCamera": "No camera available — paste the URL instead.",
|
|
25581
|
-
"launcher.cameraError": "Could not access the camera — paste the URL instead.",
|
|
25582
|
-
"launcher.invalidUrlHttps": "Enter a valid https:// URL (the tunnel URL from your terminal).",
|
|
25583
|
-
"launcher.invalidUrl": "Enter a valid http(s):// URL.",
|
|
25584
|
-
"launcher.debugAuthFailed": "Debug connection authentication failed",
|
|
25585
|
-
"launcher.debugAuthFailedHint": "The QR code may have expired. Scan a fresh QR code.",
|
|
25586
|
-
"launcher.debugAuthExpiredHint": "The debug session has expired. Scan a fresh QR from the attach page on your Mac.",
|
|
25587
|
-
"launcher.debugAuthRescanCta": "Scan a new QR",
|
|
25588
|
-
"launcher.diagTitle": "Viewport diagnostics",
|
|
25589
|
-
"launcher.diagYes": "yes",
|
|
25590
|
-
"launcher.diagNo": "no",
|
|
25591
|
-
"launcher.letterboxDetected": "An iOS viewport constraint may clip the bottom {pt}pt — rotating to landscape and back to portrait may resolve it.",
|
|
25592
|
-
"launcher.letterboxClipped": "An iOS viewport bug makes the bottom {pt}pt unusable — rotating to landscape and back to portrait may recover it.",
|
|
25593
|
-
"launcher.diagVerdictLabel": "Verdict reason",
|
|
25594
|
-
"launcher.diagSafeAreaTrace": "top re-measure trace",
|
|
25595
|
-
"launcher.diagVerdict.detected": "✓ letterbox correction",
|
|
25596
|
-
"launcher.diagVerdict.notStandalone": "not standalone",
|
|
25597
|
-
"launcher.diagVerdict.landscape": "landscape",
|
|
25598
|
-
"launcher.diagVerdict.shortfallTooSmall": "shortfall too small",
|
|
25599
|
-
"launcher.diagVerdict.safeAreaTopZero": "top=0 (env() stale?)",
|
|
25600
|
-
"launcher.navbar.defaultTitle": "Mini App",
|
|
25601
|
-
"launcher.navbar.back": "Back",
|
|
25602
|
-
"launcher.navbar.menu": "Menu",
|
|
25603
|
-
"launcher.navbar.close": "Close",
|
|
25604
|
-
"launcher.navbar.menuRescan": "Rescan",
|
|
25605
|
-
"launcher.navbar.menuDiag": "Viewport diagnostics",
|
|
25606
|
-
"launcher.navbar.menuLanguage": "Language"
|
|
25583
|
+
"notifications.option.agreementRejected": "agreementRejected (user declined)"
|
|
25607
25584
|
};
|
|
25608
25585
|
//#endregion
|
|
25609
25586
|
//#region src/i18n/ko.ts
|
|
@@ -25693,6 +25670,16 @@ const ko = {
|
|
|
25693
25670
|
"viewport.status.aitNavBar": "AIT nav bar",
|
|
25694
25671
|
"viewport.status.aitNavBarValue": "{height}px → SafeArea top · {type}",
|
|
25695
25672
|
"viewport.orientation.autoSuffix": "{orient} (auto)",
|
|
25673
|
+
"iap.section.products": "Product Catalog ({count})",
|
|
25674
|
+
"iap.empty.products": "(등록된 상품 없음)",
|
|
25675
|
+
"iap.btn.addProduct": "+ Add Product",
|
|
25676
|
+
"iap.btn.removeProduct": "Remove",
|
|
25677
|
+
"iap.label.untitledProduct": "(제목 없음)",
|
|
25678
|
+
"iap.row.sku": "SKU",
|
|
25679
|
+
"iap.row.type": "Type",
|
|
25680
|
+
"iap.row.displayName": "Display Name",
|
|
25681
|
+
"iap.row.displayAmount": "Display Amount",
|
|
25682
|
+
"iap.row.renewalCycle": "Renewal Cycle",
|
|
25696
25683
|
"iap.section.simulator": "IAP Simulator",
|
|
25697
25684
|
"iap.row.nextResult": "Next Purchase Result",
|
|
25698
25685
|
"iap.section.tossPay": "TossPay",
|
|
@@ -25748,40 +25735,7 @@ const ko = {
|
|
|
25748
25735
|
"notifications.section.title": "requestNotificationAgreement",
|
|
25749
25736
|
"notifications.option.newAgreement": "newAgreement (최초 동의)",
|
|
25750
25737
|
"notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
|
|
25751
|
-
"notifications.option.agreementRejected": "agreementRejected (사용자 거절)"
|
|
25752
|
-
"launcher.title": "AITC DevTools Launcher",
|
|
25753
|
-
"launcher.description": "터미널 QR을 스캔하거나 URL을 입력하세요.",
|
|
25754
|
-
"launcher.installCta": "폰에 런처 설치하기",
|
|
25755
|
-
"launcher.urlPlaceholder": "https://example.trycloudflare.com",
|
|
25756
|
-
"launcher.openBtn": "Open",
|
|
25757
|
-
"launcher.scanBtn": "QR 카메라로 스캔",
|
|
25758
|
-
"launcher.noCamera": "카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.",
|
|
25759
|
-
"launcher.cameraError": "카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.",
|
|
25760
|
-
"launcher.invalidUrlHttps": "올바른 https:// URL을 입력하세요 (터미널의 터널 URL).",
|
|
25761
|
-
"launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요.",
|
|
25762
|
-
"launcher.debugAuthFailed": "디버그 연결 인증 실패",
|
|
25763
|
-
"launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
|
|
25764
|
-
"launcher.debugAuthExpiredHint": "디버그 세션이 만료됐어요. Mac의 attach 페이지에서 새 QR을 스캔하세요.",
|
|
25765
|
-
"launcher.debugAuthRescanCta": "새 QR 스캔하기",
|
|
25766
|
-
"launcher.diagTitle": "뷰포트 진단",
|
|
25767
|
-
"launcher.diagYes": "예",
|
|
25768
|
-
"launcher.diagNo": "아니요",
|
|
25769
|
-
"launcher.letterboxDetected": "iOS 뷰포트 제약으로 화면 아래 {pt}pt가 잘릴 수 있습니다 — 기기를 가로로 돌렸다 세로로 복귀하면 해소될 수 있어요.",
|
|
25770
|
-
"launcher.letterboxClipped": "iOS 뷰포트 버그로 화면 아래 {pt}pt를 쓸 수 없습니다 — 기기를 가로로 돌렸다 세로로 돌리면 복구될 수 있어요.",
|
|
25771
|
-
"launcher.diagVerdictLabel": "판정 사유",
|
|
25772
|
-
"launcher.diagSafeAreaTrace": "top 재측정 추이",
|
|
25773
|
-
"launcher.diagVerdict.detected": "✓ letterbox 보정",
|
|
25774
|
-
"launcher.diagVerdict.notStandalone": "홈 화면 앱 아님",
|
|
25775
|
-
"launcher.diagVerdict.landscape": "가로 모드",
|
|
25776
|
-
"launcher.diagVerdict.shortfallTooSmall": "높이 차이 미달",
|
|
25777
|
-
"launcher.diagVerdict.safeAreaTopZero": "top=0 (env() stale?)",
|
|
25778
|
-
"launcher.navbar.defaultTitle": "미니앱",
|
|
25779
|
-
"launcher.navbar.back": "뒤로가기",
|
|
25780
|
-
"launcher.navbar.menu": "메뉴",
|
|
25781
|
-
"launcher.navbar.close": "닫기",
|
|
25782
|
-
"launcher.navbar.menuRescan": "다시 스캔",
|
|
25783
|
-
"launcher.navbar.menuDiag": "뷰포트 진단",
|
|
25784
|
-
"launcher.navbar.menuLanguage": "언어"
|
|
25738
|
+
"notifications.option.agreementRejected": "agreementRejected (사용자 거절)"
|
|
25785
25739
|
};
|
|
25786
25740
|
//#endregion
|
|
25787
25741
|
//#region src/i18n/index.ts
|
|
@@ -25876,7 +25830,7 @@ function t(key, vars) {
|
|
|
25876
25830
|
*
|
|
25877
25831
|
* This adds NO second i18n system — it is a thin `useSyncExternalStore` wrapper
|
|
25878
25832
|
* over the existing `getLocale()` / `setLocale()` / `LOCALE_CHANGE_EVENT` so that
|
|
25879
|
-
* React surfaces (panel, fixture
|
|
25833
|
+
* React surfaces (panel, fixture) re-render their subtree when the
|
|
25880
25834
|
* locale changes, instead of the vanilla panel's old `disposePanel()` → `mount()`
|
|
25881
25835
|
* full-remount dance.
|
|
25882
25836
|
*
|
|
@@ -25917,6 +25871,33 @@ function useT() {
|
|
|
25917
25871
|
return t;
|
|
25918
25872
|
}
|
|
25919
25873
|
//#endregion
|
|
25874
|
+
//#region src/mock/deep-merge.ts
|
|
25875
|
+
function isPlainObject(value) {
|
|
25876
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25877
|
+
}
|
|
25878
|
+
function mergeValue(base, override) {
|
|
25879
|
+
if (override === void 0) return base;
|
|
25880
|
+
if (Array.isArray(override)) return override;
|
|
25881
|
+
if (isPlainObject(base) && isPlainObject(override)) {
|
|
25882
|
+
const result = { ...base };
|
|
25883
|
+
for (const key of Object.keys(override)) {
|
|
25884
|
+
if (!(key in base)) continue;
|
|
25885
|
+
result[key] = mergeValue(base[key], override[key]);
|
|
25886
|
+
}
|
|
25887
|
+
return result;
|
|
25888
|
+
}
|
|
25889
|
+
return override;
|
|
25890
|
+
}
|
|
25891
|
+
/**
|
|
25892
|
+
* `base`(보통 `DEFAULT_STATE`)에 `override`를 재귀 병합한 새 객체를
|
|
25893
|
+
* 반환한다. `base`는 변형하지 않는다(불변). `override`가 `undefined`이면
|
|
25894
|
+
* `base`를 그대로 반환한다.
|
|
25895
|
+
*/
|
|
25896
|
+
function deepMergeState(base, override) {
|
|
25897
|
+
if (override === void 0) return base;
|
|
25898
|
+
return mergeValue(base, override);
|
|
25899
|
+
}
|
|
25900
|
+
//#endregion
|
|
25920
25901
|
//#region src/mock/throttle-registry.ts
|
|
25921
25902
|
/**
|
|
25922
25903
|
* throttle 레지스트리 (devtools#834).
|
|
@@ -25946,6 +25927,10 @@ function resetThrottleRegistry() {
|
|
|
25946
25927
|
}
|
|
25947
25928
|
//#endregion
|
|
25948
25929
|
//#region src/mock/state.ts
|
|
25930
|
+
/**
|
|
25931
|
+
* @apps-in-toss/devtools 중앙 상태 관리
|
|
25932
|
+
* DevTools Panel과 mock 구현체가 이 상태를 공유한다.
|
|
25933
|
+
*/
|
|
25949
25934
|
/** SDK 호출 로그 ring buffer 상한 */
|
|
25950
25935
|
const SDK_CALL_LOG_MAX = 200;
|
|
25951
25936
|
const DEFAULT_STATE = {
|
|
@@ -26005,6 +25990,14 @@ const DEFAULT_STATE = {
|
|
|
26005
25990
|
displayAmount: "1,000원",
|
|
26006
25991
|
iconUrl: "",
|
|
26007
25992
|
description: "게임에서 사용할 수 있는 보석 100개"
|
|
25993
|
+
}, {
|
|
25994
|
+
sku: "mock-sub-monthly",
|
|
25995
|
+
type: "SUBSCRIPTION",
|
|
25996
|
+
displayName: "월간 구독",
|
|
25997
|
+
displayAmount: "4,900원",
|
|
25998
|
+
iconUrl: "",
|
|
25999
|
+
description: "매달 자동으로 갱신되는 구독 상품",
|
|
26000
|
+
renewalCycle: "MONTHLY"
|
|
26008
26001
|
}],
|
|
26009
26002
|
nextResult: "success",
|
|
26010
26003
|
pendingOrders: [],
|
|
@@ -26067,6 +26060,22 @@ const DEFAULT_STATE = {
|
|
|
26067
26060
|
},
|
|
26068
26061
|
failureModes: { sdkLine: "2.x" }
|
|
26069
26062
|
};
|
|
26063
|
+
/**
|
|
26064
|
+
* `initialState` unplugin 옵션(Vite `define`로 주입된 `__AIT_INITIAL_STATE__`)이
|
|
26065
|
+
* 있으면 `DEFAULT_STATE`에 재귀 병합한 결과를, 없으면 `DEFAULT_STATE`를 그대로
|
|
26066
|
+
* 반환한다. devtools 자체 빌드/테스트에는 이 define이 없으므로
|
|
26067
|
+
* `typeof` 가드로 ReferenceError를 막는다(env.d.ts 참고).
|
|
26068
|
+
*
|
|
26069
|
+
* 모듈 로드 시 한 번만 계산해 생성자·`reset()` 양쪽에서 재사용한다 — `reset()`도
|
|
26070
|
+
* 이 병합된 값으로 되돌아가야, 소비자가 선언한 커스텀 카탈로그가 패널의
|
|
26071
|
+
* "초기화" 이후에도 사라지지 않는다(초기화는 "SDK 기본값"이 아니라 "이
|
|
26072
|
+
* 프로젝트가 선언한 기본값"으로 돌아가는 것이 사용자 기대에 맞다).
|
|
26073
|
+
*/
|
|
26074
|
+
function resolveDefaultState() {
|
|
26075
|
+
const override = typeof __AIT_INITIAL_STATE__ !== "undefined" ? __AIT_INITIAL_STATE__ : void 0;
|
|
26076
|
+
return override ? deepMergeState(DEFAULT_STATE, override) : DEFAULT_STATE;
|
|
26077
|
+
}
|
|
26078
|
+
const RESOLVED_DEFAULT_STATE = resolveDefaultState();
|
|
26070
26079
|
function generateDeviceId() {
|
|
26071
26080
|
const stored = localStorage.getItem("__ait_device_id");
|
|
26072
26081
|
if (stored) return stored;
|
|
@@ -26079,7 +26088,7 @@ var AitStateManager = class {
|
|
|
26079
26088
|
_listeners = /* @__PURE__ */ new Set();
|
|
26080
26089
|
_inTransaction = false;
|
|
26081
26090
|
constructor() {
|
|
26082
|
-
this._state = structuredClone(
|
|
26091
|
+
this._state = structuredClone(RESOLVED_DEFAULT_STATE);
|
|
26083
26092
|
try {
|
|
26084
26093
|
this._state.deviceId = generateDeviceId();
|
|
26085
26094
|
} catch {
|
|
@@ -26175,7 +26184,7 @@ var AitStateManager = class {
|
|
|
26175
26184
|
reset() {
|
|
26176
26185
|
const deviceId = this._state.deviceId;
|
|
26177
26186
|
this._state = {
|
|
26178
|
-
...structuredClone(
|
|
26187
|
+
...structuredClone(RESOLVED_DEFAULT_STATE),
|
|
26179
26188
|
deviceId
|
|
26180
26189
|
};
|
|
26181
26190
|
resetThrottleRegistry();
|
|
@@ -27839,39 +27848,6 @@ const showFullScreenAd = withIsSupported(observe("showFullScreenAd", "faithful",
|
|
|
27839
27848
|
setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
|
|
27840
27849
|
return () => {};
|
|
27841
27850
|
}));
|
|
27842
|
-
createMockProxy("Ads", {
|
|
27843
|
-
showTossAdOrShoppingAds: withIsSupported(observe("Ads.showTossAdOrShoppingAds", "faithful", (args) => {
|
|
27844
|
-
if (!aitState.state.ads.isLoaded) {
|
|
27845
|
-
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
27846
|
-
return () => {};
|
|
27847
|
-
}
|
|
27848
|
-
const { rewardUnitType, rewardAmount } = aitState.state.ads;
|
|
27849
|
-
setTimeout(() => args.onEvent({
|
|
27850
|
-
type: "userEarnedReward",
|
|
27851
|
-
data: {
|
|
27852
|
-
unitType: rewardUnitType,
|
|
27853
|
-
unitAmount: rewardAmount
|
|
27854
|
-
}
|
|
27855
|
-
}), 1e3);
|
|
27856
|
-
setTimeout(() => {
|
|
27857
|
-
args.onEvent({ type: "dismissed" });
|
|
27858
|
-
aitState.patch("ads", { isLoaded: false });
|
|
27859
|
-
}, 1500);
|
|
27860
|
-
return () => {};
|
|
27861
|
-
})),
|
|
27862
|
-
showTossShoppingAds: withIsSupported(observe("Ads.showTossShoppingAds", "faithful", (args) => {
|
|
27863
|
-
const { rewardUnitType, rewardAmount } = aitState.state.ads;
|
|
27864
|
-
setTimeout(() => args.onEvent({
|
|
27865
|
-
type: "userEarnedReward",
|
|
27866
|
-
data: {
|
|
27867
|
-
unitType: rewardUnitType,
|
|
27868
|
-
unitAmount: rewardAmount
|
|
27869
|
-
}
|
|
27870
|
-
}), 1e3);
|
|
27871
|
-
setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
|
|
27872
|
-
return () => {};
|
|
27873
|
-
}))
|
|
27874
|
-
});
|
|
27875
27851
|
//#endregion
|
|
27876
27852
|
//#region src/panel/tabs/ads.ts
|
|
27877
27853
|
function recordEvent(type) {
|
|
@@ -28243,6 +28219,55 @@ Object.assign(async function requestTossPayPaysBilling(options) {
|
|
|
28243
28219
|
}, { isSupported: () => true });
|
|
28244
28220
|
//#endregion
|
|
28245
28221
|
//#region src/panel/tabs/iap.ts
|
|
28222
|
+
const PRODUCT_TYPES = [
|
|
28223
|
+
"CONSUMABLE",
|
|
28224
|
+
"NON_CONSUMABLE",
|
|
28225
|
+
"SUBSCRIPTION"
|
|
28226
|
+
];
|
|
28227
|
+
const RENEWAL_CYCLES = [
|
|
28228
|
+
"WEEKLY",
|
|
28229
|
+
"MONTHLY",
|
|
28230
|
+
"YEARLY"
|
|
28231
|
+
];
|
|
28232
|
+
function newDefaultProduct() {
|
|
28233
|
+
return {
|
|
28234
|
+
sku: `mock-product-${Date.now()}`,
|
|
28235
|
+
type: "CONSUMABLE",
|
|
28236
|
+
displayName: "새 상품",
|
|
28237
|
+
displayAmount: "0원",
|
|
28238
|
+
iconUrl: "",
|
|
28239
|
+
description: ""
|
|
28240
|
+
};
|
|
28241
|
+
}
|
|
28242
|
+
/**
|
|
28243
|
+
* products[idx]를 partial 병합해 `aitState.patch('iap', { products })`로
|
|
28244
|
+
* 반영한다. `aitState.state`를 매번 다시 읽어(호출 시점 최신 스냅샷)
|
|
28245
|
+
* 여러 필드를 연속으로 고칠 때도 이전 입력이 유실되지 않게 한다.
|
|
28246
|
+
*/
|
|
28247
|
+
function updateProduct(index, partial) {
|
|
28248
|
+
const next = [...aitState.state.iap.products];
|
|
28249
|
+
const current = next[index];
|
|
28250
|
+
if (!current) return;
|
|
28251
|
+
next[index] = {
|
|
28252
|
+
...current,
|
|
28253
|
+
...partial
|
|
28254
|
+
};
|
|
28255
|
+
aitState.patch("iap", { products: next });
|
|
28256
|
+
}
|
|
28257
|
+
function removeProduct(index) {
|
|
28258
|
+
const next = [...aitState.state.iap.products];
|
|
28259
|
+
next.splice(index, 1);
|
|
28260
|
+
aitState.patch("iap", { products: next });
|
|
28261
|
+
}
|
|
28262
|
+
function renderProductCard(product, index, disabled) {
|
|
28263
|
+
const card = h("div", { className: "ait-product-card" });
|
|
28264
|
+
const removeBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("iap.btn.removeProduct"));
|
|
28265
|
+
if (disabled) removeBtn.disabled = true;
|
|
28266
|
+
removeBtn.addEventListener("click", () => removeProduct(index));
|
|
28267
|
+
card.append(h("div", { className: "ait-row" }, h("strong", {}, product.sku || t("iap.label.untitledProduct")), removeBtn), inputRow(t("iap.row.sku"), product.sku, (v) => updateProduct(index, { sku: v }), disabled), selectRow(t("iap.row.type"), PRODUCT_TYPES, product.type, (v) => updateProduct(index, { type: v }), disabled), inputRow(t("iap.row.displayName"), product.displayName, (v) => updateProduct(index, { displayName: v }), disabled), inputRow(t("iap.row.displayAmount"), product.displayAmount, (v) => updateProduct(index, { displayAmount: v }), disabled));
|
|
28268
|
+
if (product.type === "SUBSCRIPTION") card.appendChild(selectRow(t("iap.row.renewalCycle"), RENEWAL_CYCLES, product.renewalCycle ?? "MONTHLY", (v) => updateProduct(index, { renewalCycle: v }), disabled));
|
|
28269
|
+
return card;
|
|
28270
|
+
}
|
|
28246
28271
|
function formatTimestamp(iso) {
|
|
28247
28272
|
const d = new Date(iso);
|
|
28248
28273
|
if (Number.isNaN(d.getTime())) return iso;
|
|
@@ -28265,6 +28290,18 @@ function renderIapTab() {
|
|
|
28265
28290
|
"INTERNAL_ERROR"
|
|
28266
28291
|
];
|
|
28267
28292
|
if (disabled) container.appendChild(monitoringNotice());
|
|
28293
|
+
const products = s.iap.products;
|
|
28294
|
+
const productsSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.products", { count: products.length })));
|
|
28295
|
+
if (products.length === 0) productsSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.products")));
|
|
28296
|
+
else products.forEach((product, index) => {
|
|
28297
|
+
productsSection.appendChild(renderProductCard(product, index, disabled));
|
|
28298
|
+
});
|
|
28299
|
+
const addProductBtn = h("button", { className: "ait-btn-secondary" }, t("iap.btn.addProduct"));
|
|
28300
|
+
if (disabled) addProductBtn.disabled = true;
|
|
28301
|
+
addProductBtn.addEventListener("click", () => {
|
|
28302
|
+
aitState.patch("iap", { products: [...aitState.state.iap.products, newDefaultProduct()] });
|
|
28303
|
+
});
|
|
28304
|
+
productsSection.appendChild(h("div", { className: "ait-btn-row" }, addProductBtn));
|
|
28268
28305
|
const pendingOrders = s.iap.pendingOrders;
|
|
28269
28306
|
const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.pending", { count: pendingOrders.length })));
|
|
28270
28307
|
if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.pending")));
|
|
@@ -28280,7 +28317,7 @@ function renderIapTab() {
|
|
|
28280
28317
|
const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.completed", { count: completedOrders.length })));
|
|
28281
28318
|
if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.completed")));
|
|
28282
28319
|
else for (const o of completedOrders) completedSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, o.status), `${o.sku} (${shortOrderId(o.orderId)}) · ${formatTimestamp(o.date)}`));
|
|
28283
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.simulator")), selectRow(t("iap.row.nextResult"), results, s.iap.nextResult, (v) => {
|
|
28320
|
+
container.append(productsSection, h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.simulator")), selectRow(t("iap.row.nextResult"), results, s.iap.nextResult, (v) => {
|
|
28284
28321
|
aitState.patch("iap", { nextResult: v });
|
|
28285
28322
|
}, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.tossPay")), selectRow(t("iap.row.tossPayResult"), ["success", "fail"], s.payment.nextResult, (v) => {
|
|
28286
28323
|
aitState.patch("payment", { nextResult: v });
|
|
@@ -29100,6 +29137,17 @@ const PANEL_STYLES = `
|
|
|
29100
29137
|
background: #3a3a5a;
|
|
29101
29138
|
}
|
|
29102
29139
|
|
|
29140
|
+
/* IAP 상품 카탈로그 편집 카드 (한 상품 = 카드 1개) */
|
|
29141
|
+
.ait-product-card {
|
|
29142
|
+
border: 1px solid #3a3a5a;
|
|
29143
|
+
border-radius: 6px;
|
|
29144
|
+
padding: 8px;
|
|
29145
|
+
margin-bottom: 8px;
|
|
29146
|
+
}
|
|
29147
|
+
.ait-product-card .ait-row:last-child {
|
|
29148
|
+
margin-bottom: 0;
|
|
29149
|
+
}
|
|
29150
|
+
|
|
29103
29151
|
/* Prompt notification */
|
|
29104
29152
|
.ait-prompt-banner {
|
|
29105
29153
|
background: #2d1b69;
|
|
@@ -30553,7 +30601,7 @@ function Panel() {
|
|
|
30553
30601
|
color: "#666",
|
|
30554
30602
|
fontWeight: 400
|
|
30555
30603
|
},
|
|
30556
|
-
children: ["v", "3.1.
|
|
30604
|
+
children: ["v", "3.1.1"]
|
|
30557
30605
|
}),
|
|
30558
30606
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
30559
30607
|
type: "button",
|
package/dist/unplugin/index.cjs
CHANGED
|
@@ -6,71 +6,6 @@ let node_fs = require("node:fs");
|
|
|
6
6
|
let node_module = require("node:module");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
let unplugin = require("unplugin");
|
|
9
|
-
//#region src/shared/parent-watcher.ts
|
|
10
|
-
/**
|
|
11
|
-
* Shared parent-PID watcher — used by both the MCP debug daemon and the
|
|
12
|
-
* unplugin tunnel path to self-terminate when the parent process (e.g. Claude
|
|
13
|
-
* Code, vite) has died or been reparented without sending SIGTERM/SIGHUP.
|
|
14
|
-
*
|
|
15
|
-
* Intentionally react-free and Node-stdlib-only so this module is safe to
|
|
16
|
-
* import from the MCP daemon bundle (`dist/mcp/cli.js`) without violating the
|
|
17
|
-
* install-graph invariant.
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Returns `true` when the given PID refers to a running process.
|
|
21
|
-
*
|
|
22
|
-
* Uses `process.kill(pid, 0)` — a no-op signal that succeeds when the process
|
|
23
|
-
* exists and we have permission to signal it; throws ESRCH when it doesn't exist.
|
|
24
|
-
*/
|
|
25
|
-
function isPidAlive(pid) {
|
|
26
|
-
try {
|
|
27
|
-
process.kill(pid, 0);
|
|
28
|
-
return true;
|
|
29
|
-
} catch (err) {
|
|
30
|
-
if (err.code === "EPERM") return true;
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Starts a periodic watcher that detects when the parent process (e.g. Claude
|
|
36
|
-
* Code) has died without sending SIGTERM/SIGHUP, and calls `onOrphaned` so the
|
|
37
|
-
* daemon can self-terminate rather than running as a zombie.
|
|
38
|
-
*
|
|
39
|
-
* Mirrors the `startAttachWatcher` pattern: `setInterval`-based, returns
|
|
40
|
-
* `{ stop(): void }`, injectable deps for testability.
|
|
41
|
-
*
|
|
42
|
-
* @param onOrphaned - Called once when the parent is gone.
|
|
43
|
-
* @param opts.intervalMs - Poll interval in milliseconds (default 5 000).
|
|
44
|
-
* @param opts.initialPpid - Parent PID to watch (default `process.ppid`).
|
|
45
|
-
* @param opts.isAlive - Predicate to test if a PID is running (default `isPidAlive`).
|
|
46
|
-
* @param opts.getPpid - Supplier of current ppid (default `() => process.ppid`).
|
|
47
|
-
* Detects ppid changes as well as death.
|
|
48
|
-
* @param opts.log - Logger (default `process.stderr.write`).
|
|
49
|
-
*
|
|
50
|
-
* @returns `stop` — call during shutdown to clear the interval.
|
|
51
|
-
*/
|
|
52
|
-
function startParentWatcher(onOrphaned, opts) {
|
|
53
|
-
const { intervalMs = 5e3, initialPpid = process.ppid, isAlive = isPidAlive, getPpid = () => process.ppid, log = (msg) => process.stderr.write(msg) } = opts ?? {};
|
|
54
|
-
if (initialPpid <= 1) {
|
|
55
|
-
log("[ait-debug] parent-pid watcher: no parent to watch (ppid<=1), skipping\n");
|
|
56
|
-
return { stop() {} };
|
|
57
|
-
}
|
|
58
|
-
let fired = false;
|
|
59
|
-
const handle = setInterval(() => {
|
|
60
|
-
if (fired) return;
|
|
61
|
-
const currentPpid = getPpid();
|
|
62
|
-
if (currentPpid !== initialPpid || !isAlive(initialPpid)) {
|
|
63
|
-
fired = true;
|
|
64
|
-
clearInterval(handle);
|
|
65
|
-
log(`[ait-debug] parent-pid watcher: parent PID ${initialPpid} is gone (currentPpid=${currentPpid}) — shutting down\n`);
|
|
66
|
-
onOrphaned();
|
|
67
|
-
}
|
|
68
|
-
}, intervalMs);
|
|
69
|
-
return { stop() {
|
|
70
|
-
clearInterval(handle);
|
|
71
|
-
} };
|
|
72
|
-
}
|
|
73
|
-
//#endregion
|
|
74
9
|
//#region src/unplugin/index.ts
|
|
75
10
|
/**
|
|
76
11
|
* @apps-in-toss/devtools unplugin
|
|
@@ -124,6 +59,14 @@ function detectInstalledSdkMajor(cwd = process.cwd()) {
|
|
|
124
59
|
} catch {}
|
|
125
60
|
return null;
|
|
126
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* 패널 자동 주입 대상 진입점 파일명 기본 패턴. `entryPattern` 옵션 미지정 시
|
|
64
|
+
* 사용된다. `id.test()`는 상태를 갖지 않아야 하므로(매 파일마다 호출) 이
|
|
65
|
+
* 패턴과 사용자 지정 `entryPattern` 모두 `g`/`y` 플래그를 붙이지 않는다는
|
|
66
|
+
* 전제다 — `RegExp.prototype.test`는 global/sticky 플래그가 있으면 호출마다
|
|
67
|
+
* `lastIndex`가 전진해 다음 파일 매칭이 간헐적으로 실패한다.
|
|
68
|
+
*/
|
|
69
|
+
const DEFAULT_ENTRY_PATTERN = /\/(main|index|entry|app)\.[tj]sx?$/i;
|
|
127
70
|
const FRAMEWORK_ID = "@apps-in-toss/web-framework";
|
|
128
71
|
const BRIDGE_ID = "@apps-in-toss/web-bridge";
|
|
129
72
|
const ANALYTICS_ID = "@apps-in-toss/web-analytics";
|
|
@@ -132,40 +75,14 @@ const WEBVIEW_BRIDGE_ID = "@apps-in-toss/webview-bridge";
|
|
|
132
75
|
const MCP_STATE_PATH = "/api/ait-devtools/state";
|
|
133
76
|
/** Browser runtime opt-in consumed by the panel's state-sync helper. */
|
|
134
77
|
const MCP_ENABLE_SNIPPET = "globalThis.__AIT_DEVTOOLS_MCP_ENABLED__ = true;";
|
|
135
|
-
/**
|
|
136
|
-
* Resolves the effective tunnel option (#425).
|
|
137
|
-
*
|
|
138
|
-
* An explicit `tunnel` value (including `false`) always takes priority over
|
|
139
|
-
* env vars — the `??` operator means `undefined` (= omitted) falls through,
|
|
140
|
-
* but `false` / `true` / an object are preserved as-is (non-breaking).
|
|
141
|
-
*
|
|
142
|
-
* When the option is omitted:
|
|
143
|
-
* - `AIT_TUNNEL=1` enables the screen-preview tunnel.
|
|
144
|
-
* - Not set → `false` (disabled).
|
|
145
|
-
*
|
|
146
|
-
* Extracted as a pure function so it can be unit-tested without standing up
|
|
147
|
-
* a full Vite dev server.
|
|
148
|
-
*
|
|
149
|
-
* @param explicit - The `tunnel` option as passed by the consumer (or `undefined` when omitted).
|
|
150
|
-
* @param env - The process environment (injectable for testing).
|
|
151
|
-
*/
|
|
152
|
-
function resolveTunnelOption(explicit, env) {
|
|
153
|
-
return explicit ?? !!env.AIT_TUNNEL;
|
|
154
|
-
}
|
|
155
78
|
const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
|
|
156
79
|
const isDev = process.env.NODE_ENV !== "production";
|
|
157
|
-
const shouldEnable = isDev;
|
|
80
|
+
const shouldEnable = isDev || !!options?.forceEnable;
|
|
158
81
|
const shouldMock = shouldEnable && (options?.mock ?? isDev);
|
|
159
82
|
const sdkMajor = options?.sdkVersion === "2" || options?.sdkVersion === "3" ? options.sdkVersion : detectInstalledSdkMajor() ?? "3";
|
|
160
83
|
const shouldPanel = shouldEnable && (options?.panel ?? true);
|
|
161
84
|
const shouldMcp = shouldEnable && (options?.mcp ?? false);
|
|
162
85
|
let lastState = null;
|
|
163
|
-
const tunnelOpt = resolveTunnelOption(options?.tunnel, process.env);
|
|
164
|
-
const shouldTunnel = isDev && !!tunnelOpt;
|
|
165
|
-
const webViewType = options?.webViewType ?? "partner";
|
|
166
|
-
const navBarTransparent = options?.navBarTransparent;
|
|
167
|
-
const navBarTheme = options?.navBarTheme;
|
|
168
|
-
const tunnelConfig = typeof tunnelOpt === "object" ? tunnelOpt : {};
|
|
169
86
|
return {
|
|
170
87
|
name: "ait-co-devtools",
|
|
171
88
|
enforce: "pre",
|
|
@@ -180,7 +97,7 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
|
|
|
180
97
|
},
|
|
181
98
|
transformInclude(id) {
|
|
182
99
|
if (!shouldPanel && !shouldMcp) return false;
|
|
183
|
-
return /\.(tsx?|jsx?)$/.test(id) &&
|
|
100
|
+
return /\.(tsx?|jsx?)$/.test(id) && (options?.entryPattern ?? DEFAULT_ENTRY_PATTERN).test(id) && !id.includes("node_modules");
|
|
184
101
|
},
|
|
185
102
|
transform(code) {
|
|
186
103
|
let result = code;
|
|
@@ -197,12 +114,8 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
|
|
|
197
114
|
},
|
|
198
115
|
vite: {
|
|
199
116
|
config() {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
define,
|
|
204
|
-
server: { allowedHosts: [".trycloudflare.com"] }
|
|
205
|
-
};
|
|
117
|
+
if (options?.initialState === void 0) return {};
|
|
118
|
+
return { define: { __AIT_INITIAL_STATE__: JSON.stringify(options.initialState) } };
|
|
206
119
|
},
|
|
207
120
|
configureServer(server) {
|
|
208
121
|
if (shouldMcp) server.middlewares.use(MCP_STATE_PATH, (req, res) => {
|
|
@@ -244,53 +157,6 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
|
|
|
244
157
|
res.writeHead(405, { "Content-Type": "application/json" });
|
|
245
158
|
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
246
159
|
});
|
|
247
|
-
if (shouldTunnel) {
|
|
248
|
-
let tunnel = null;
|
|
249
|
-
let parentWatcher = null;
|
|
250
|
-
const httpServer = server.httpServer;
|
|
251
|
-
httpServer?.once("listening", () => {
|
|
252
|
-
const address = httpServer?.address();
|
|
253
|
-
const port = tunnelConfig.port ?? (address && typeof address === "object" ? address.port : void 0);
|
|
254
|
-
if (!port) {
|
|
255
|
-
console.warn("[@apps-in-toss/devtools] tunnel: could not determine the dev server port; skipping.");
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
Promise.resolve().then(() => require("../tunnel-DtCTOUlp.cjs")).then(async ({ startQuickTunnel, printTunnelBanner }) => {
|
|
259
|
-
const t = await startQuickTunnel(port);
|
|
260
|
-
tunnel = t;
|
|
261
|
-
let tunnelAppName;
|
|
262
|
-
try {
|
|
263
|
-
const { readFileSync } = await import("node:fs");
|
|
264
|
-
const pkgRaw = readFileSync(`${server.config.root}/package.json`, "utf8");
|
|
265
|
-
const pkg = JSON.parse(pkgRaw);
|
|
266
|
-
const rawName = typeof pkg.name === "string" ? pkg.name : "";
|
|
267
|
-
tunnelAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
|
|
268
|
-
} catch {}
|
|
269
|
-
await printTunnelBanner(t.url, {
|
|
270
|
-
qr: tunnelConfig.qr,
|
|
271
|
-
name: tunnelAppName,
|
|
272
|
-
webViewType,
|
|
273
|
-
navBarTransparent,
|
|
274
|
-
navBarTheme
|
|
275
|
-
});
|
|
276
|
-
parentWatcher = startParentWatcher(() => {
|
|
277
|
-
cleanup();
|
|
278
|
-
process.exit(0);
|
|
279
|
-
});
|
|
280
|
-
}).catch((err) => {
|
|
281
|
-
console.warn(`[@apps-in-toss/devtools] tunnel failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
const cleanup = () => {
|
|
285
|
-
parentWatcher?.stop();
|
|
286
|
-
tunnel?.stop();
|
|
287
|
-
};
|
|
288
|
-
httpServer?.once("close", cleanup);
|
|
289
|
-
process.once("SIGINT", cleanup);
|
|
290
|
-
process.once("SIGTERM", cleanup);
|
|
291
|
-
process.once("SIGHUP", cleanup);
|
|
292
|
-
process.once("exit", cleanup);
|
|
293
|
-
}
|
|
294
160
|
}
|
|
295
161
|
}
|
|
296
162
|
};
|
|
@@ -305,7 +171,6 @@ const aitDevtools = aitDevtoolsPlugin;
|
|
|
305
171
|
exports.default = aitDevtools;
|
|
306
172
|
exports.detectInstalledSdkMajor = detectInstalledSdkMajor;
|
|
307
173
|
exports.esbuild = esbuild;
|
|
308
|
-
exports.resolveTunnelOption = resolveTunnelOption;
|
|
309
174
|
exports.rollup = rollup;
|
|
310
175
|
exports.rspack = rspack;
|
|
311
176
|
exports.vite = vite;
|