@apps-in-toss/devtools 3.0.5 → 3.1.0-beta.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/LICENSE +28 -201
- package/README.md +871 -237
- package/dist/mock/2x.d.ts +155 -3
- package/dist/mock/2x.js +1560 -1306
- package/dist/mock/3x.d.ts +123 -3
- package/dist/mock/3x.js +1507 -1245
- package/dist/mock/index.d.ts +123 -3
- package/dist/mock/index.js +1507 -1245
- package/dist/panel/index.js +106 -154
- package/dist/tunnel-BvEf1qGV.js +186 -0
- package/dist/tunnel-DtCTOUlp.cjs +187 -0
- package/dist/unplugin/index.cjs +147 -12
- package/dist/unplugin/index.d.cts +78 -408
- package/dist/unplugin/index.d.ts +78 -408
- package/dist/unplugin/index.js +147 -13
- package/dist/unplugin/tunnel.cjs +191 -0
- package/dist/unplugin/tunnel.d.cts +140 -0
- package/dist/unplugin/tunnel.d.ts +140 -0
- package/dist/unplugin/tunnel.js +186 -0
- package/package.json +8 -5
- package/CHANGELOG.md +0 -32
package/dist/panel/index.js
CHANGED
|
@@ -25515,16 +25515,6 @@ 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",
|
|
25528
25518
|
"iap.section.simulator": "IAP Simulator",
|
|
25529
25519
|
"iap.row.nextResult": "Next Purchase Result",
|
|
25530
25520
|
"iap.section.tossPay": "TossPay",
|
|
@@ -25580,7 +25570,40 @@ const en = {
|
|
|
25580
25570
|
"notifications.section.title": "requestNotificationAgreement",
|
|
25581
25571
|
"notifications.option.newAgreement": "newAgreement (first-time agree)",
|
|
25582
25572
|
"notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
|
|
25583
|
-
"notifications.option.agreementRejected": "agreementRejected (user declined)"
|
|
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"
|
|
25584
25607
|
};
|
|
25585
25608
|
//#endregion
|
|
25586
25609
|
//#region src/i18n/ko.ts
|
|
@@ -25670,16 +25693,6 @@ const ko = {
|
|
|
25670
25693
|
"viewport.status.aitNavBar": "AIT nav bar",
|
|
25671
25694
|
"viewport.status.aitNavBarValue": "{height}px → SafeArea top · {type}",
|
|
25672
25695
|
"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",
|
|
25683
25696
|
"iap.section.simulator": "IAP Simulator",
|
|
25684
25697
|
"iap.row.nextResult": "Next Purchase Result",
|
|
25685
25698
|
"iap.section.tossPay": "TossPay",
|
|
@@ -25735,7 +25748,40 @@ const ko = {
|
|
|
25735
25748
|
"notifications.section.title": "requestNotificationAgreement",
|
|
25736
25749
|
"notifications.option.newAgreement": "newAgreement (최초 동의)",
|
|
25737
25750
|
"notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
|
|
25738
|
-
"notifications.option.agreementRejected": "agreementRejected (사용자 거절)"
|
|
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": "언어"
|
|
25739
25785
|
};
|
|
25740
25786
|
//#endregion
|
|
25741
25787
|
//#region src/i18n/index.ts
|
|
@@ -25830,7 +25876,7 @@ function t(key, vars) {
|
|
|
25830
25876
|
*
|
|
25831
25877
|
* This adds NO second i18n system — it is a thin `useSyncExternalStore` wrapper
|
|
25832
25878
|
* over the existing `getLocale()` / `setLocale()` / `LOCALE_CHANGE_EVENT` so that
|
|
25833
|
-
* React surfaces (panel, fixture) re-render their subtree when the
|
|
25879
|
+
* React surfaces (panel, fixture, launcher) re-render their subtree when the
|
|
25834
25880
|
* locale changes, instead of the vanilla panel's old `disposePanel()` → `mount()`
|
|
25835
25881
|
* full-remount dance.
|
|
25836
25882
|
*
|
|
@@ -25871,33 +25917,6 @@ function useT() {
|
|
|
25871
25917
|
return t;
|
|
25872
25918
|
}
|
|
25873
25919
|
//#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
|
|
25901
25920
|
//#region src/mock/throttle-registry.ts
|
|
25902
25921
|
/**
|
|
25903
25922
|
* throttle 레지스트리 (devtools#834).
|
|
@@ -25927,10 +25946,6 @@ function resetThrottleRegistry() {
|
|
|
25927
25946
|
}
|
|
25928
25947
|
//#endregion
|
|
25929
25948
|
//#region src/mock/state.ts
|
|
25930
|
-
/**
|
|
25931
|
-
* @apps-in-toss/devtools 중앙 상태 관리
|
|
25932
|
-
* DevTools Panel과 mock 구현체가 이 상태를 공유한다.
|
|
25933
|
-
*/
|
|
25934
25949
|
/** SDK 호출 로그 ring buffer 상한 */
|
|
25935
25950
|
const SDK_CALL_LOG_MAX = 200;
|
|
25936
25951
|
const DEFAULT_STATE = {
|
|
@@ -25990,14 +26005,6 @@ const DEFAULT_STATE = {
|
|
|
25990
26005
|
displayAmount: "1,000원",
|
|
25991
26006
|
iconUrl: "",
|
|
25992
26007
|
description: "게임에서 사용할 수 있는 보석 100개"
|
|
25993
|
-
}, {
|
|
25994
|
-
sku: "mock-sub-monthly",
|
|
25995
|
-
type: "SUBSCRIPTION",
|
|
25996
|
-
displayName: "월간 구독",
|
|
25997
|
-
displayAmount: "4,900원",
|
|
25998
|
-
iconUrl: "",
|
|
25999
|
-
description: "매달 자동으로 갱신되는 구독 상품",
|
|
26000
|
-
renewalCycle: "MONTHLY"
|
|
26001
26008
|
}],
|
|
26002
26009
|
nextResult: "success",
|
|
26003
26010
|
pendingOrders: [],
|
|
@@ -26060,22 +26067,6 @@ const DEFAULT_STATE = {
|
|
|
26060
26067
|
},
|
|
26061
26068
|
failureModes: { sdkLine: "2.x" }
|
|
26062
26069
|
};
|
|
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();
|
|
26079
26070
|
function generateDeviceId() {
|
|
26080
26071
|
const stored = localStorage.getItem("__ait_device_id");
|
|
26081
26072
|
if (stored) return stored;
|
|
@@ -26088,7 +26079,7 @@ var AitStateManager = class {
|
|
|
26088
26079
|
_listeners = /* @__PURE__ */ new Set();
|
|
26089
26080
|
_inTransaction = false;
|
|
26090
26081
|
constructor() {
|
|
26091
|
-
this._state = structuredClone(
|
|
26082
|
+
this._state = structuredClone(DEFAULT_STATE);
|
|
26092
26083
|
try {
|
|
26093
26084
|
this._state.deviceId = generateDeviceId();
|
|
26094
26085
|
} catch {
|
|
@@ -26184,7 +26175,7 @@ var AitStateManager = class {
|
|
|
26184
26175
|
reset() {
|
|
26185
26176
|
const deviceId = this._state.deviceId;
|
|
26186
26177
|
this._state = {
|
|
26187
|
-
...structuredClone(
|
|
26178
|
+
...structuredClone(DEFAULT_STATE),
|
|
26188
26179
|
deviceId
|
|
26189
26180
|
};
|
|
26190
26181
|
resetThrottleRegistry();
|
|
@@ -27848,6 +27839,39 @@ const showFullScreenAd = withIsSupported(observe("showFullScreenAd", "faithful",
|
|
|
27848
27839
|
setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
|
|
27849
27840
|
return () => {};
|
|
27850
27841
|
}));
|
|
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
|
+
});
|
|
27851
27875
|
//#endregion
|
|
27852
27876
|
//#region src/panel/tabs/ads.ts
|
|
27853
27877
|
function recordEvent(type) {
|
|
@@ -28219,55 +28243,6 @@ Object.assign(async function requestTossPayPaysBilling(options) {
|
|
|
28219
28243
|
}, { isSupported: () => true });
|
|
28220
28244
|
//#endregion
|
|
28221
28245
|
//#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
|
-
}
|
|
28271
28246
|
function formatTimestamp(iso) {
|
|
28272
28247
|
const d = new Date(iso);
|
|
28273
28248
|
if (Number.isNaN(d.getTime())) return iso;
|
|
@@ -28290,18 +28265,6 @@ function renderIapTab() {
|
|
|
28290
28265
|
"INTERNAL_ERROR"
|
|
28291
28266
|
];
|
|
28292
28267
|
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));
|
|
28305
28268
|
const pendingOrders = s.iap.pendingOrders;
|
|
28306
28269
|
const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.pending", { count: pendingOrders.length })));
|
|
28307
28270
|
if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.pending")));
|
|
@@ -28317,7 +28280,7 @@ function renderIapTab() {
|
|
|
28317
28280
|
const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.completed", { count: completedOrders.length })));
|
|
28318
28281
|
if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.completed")));
|
|
28319
28282
|
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)}`));
|
|
28320
|
-
container.append(
|
|
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) => {
|
|
28321
28284
|
aitState.patch("iap", { nextResult: v });
|
|
28322
28285
|
}, 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) => {
|
|
28323
28286
|
aitState.patch("payment", { nextResult: v });
|
|
@@ -29137,17 +29100,6 @@ const PANEL_STYLES = `
|
|
|
29137
29100
|
background: #3a3a5a;
|
|
29138
29101
|
}
|
|
29139
29102
|
|
|
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
|
-
|
|
29151
29103
|
/* Prompt notification */
|
|
29152
29104
|
.ait-prompt-banner {
|
|
29153
29105
|
background: #2d1b69;
|
|
@@ -30601,7 +30553,7 @@ function Panel() {
|
|
|
30601
30553
|
color: "#666",
|
|
30602
30554
|
fontWeight: 400
|
|
30603
30555
|
},
|
|
30604
|
-
children: ["v", "3.0.
|
|
30556
|
+
children: ["v", "3.1.0-beta.1"]
|
|
30605
30557
|
}),
|
|
30606
30558
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
30607
30559
|
type: "button",
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
//#region src/unplugin/tunnel.ts
|
|
5
|
+
/**
|
|
6
|
+
* Cloudflare quick-tunnel helper for the devtools unplugin.
|
|
7
|
+
*
|
|
8
|
+
* Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is
|
|
9
|
+
* on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common
|
|
10
|
+
* case. This is the one place in `@apps-in-toss/devtools` that depends on Node-only
|
|
11
|
+
* APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of
|
|
12
|
+
* jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the
|
|
13
|
+
* "web 모드는 e2e" rule in CLAUDE.md). The pure helpers below
|
|
14
|
+
* (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.
|
|
15
|
+
*/
|
|
16
|
+
/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */
|
|
17
|
+
const TRYCLOUDFLARE_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
18
|
+
/**
|
|
19
|
+
* Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared
|
|
20
|
+
* output, or `null` if the line doesn't contain one. Pulled out as a pure
|
|
21
|
+
* function so it can be unit-tested without spawning anything.
|
|
22
|
+
*/
|
|
23
|
+
function parseTrycloudflareUrl(line) {
|
|
24
|
+
const m = line.match(TRYCLOUDFLARE_RE);
|
|
25
|
+
return m ? m[0] : null;
|
|
26
|
+
}
|
|
27
|
+
const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
|
|
28
|
+
/**
|
|
29
|
+
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
30
|
+
* already on the phone's home screen, scanning this opens it directly into the
|
|
31
|
+
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
32
|
+
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
33
|
+
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
34
|
+
* land on a "please install" screen.
|
|
35
|
+
*
|
|
36
|
+
* When `opts.name` is given (non-blank), it is added as `&name=` so the launcher
|
|
37
|
+
* partner bar shows the app name instead of the generic default (#498).
|
|
38
|
+
*
|
|
39
|
+
* When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the
|
|
40
|
+
* launcher enters game nav chrome (floating capsule, no full bar) automatically
|
|
41
|
+
* on scan. `'partner'` is the launcher's implicit default and is not added to
|
|
42
|
+
* keep the URL clean (#584).
|
|
43
|
+
*
|
|
44
|
+
* When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended
|
|
45
|
+
* so the launcher partner bar renders with a transparent background (#587).
|
|
46
|
+
*
|
|
47
|
+
* When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is
|
|
48
|
+
* appended so the launcher partner bar uses the matching foreground colour (#587).
|
|
49
|
+
*/
|
|
50
|
+
function buildLauncherDeepLink(tunnelUrl, optsParam) {
|
|
51
|
+
const opts = optsParam ?? {};
|
|
52
|
+
let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;
|
|
53
|
+
if (opts.name !== void 0 && opts.name.trim() !== "") url += `&name=${encodeURIComponent(opts.name.trim())}`;
|
|
54
|
+
if (opts.webViewType === "game") url += "&navBarType=game";
|
|
55
|
+
if (opts.navBarTransparent === true) url += "&navBarTransparent=1";
|
|
56
|
+
if (opts.navBarTheme === "light" || opts.navBarTheme === "dark") url += `&navBarTheme=${opts.navBarTheme}`;
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
61
|
+
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
62
|
+
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
63
|
+
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
64
|
+
*/
|
|
65
|
+
async function printTunnelBanner(url, opts = {}) {
|
|
66
|
+
const log = opts.log ?? ((m) => console.log(m));
|
|
67
|
+
const deepLink = buildLauncherDeepLink(url, {
|
|
68
|
+
name: opts.name,
|
|
69
|
+
webViewType: opts.webViewType,
|
|
70
|
+
navBarTransparent: opts.navBarTransparent,
|
|
71
|
+
navBarTheme: opts.navBarTheme
|
|
72
|
+
});
|
|
73
|
+
log([
|
|
74
|
+
"",
|
|
75
|
+
" ┌─ @apps-in-toss/devtools · live tunnel ────────────────────────────",
|
|
76
|
+
` │ ${url}`,
|
|
77
|
+
" │",
|
|
78
|
+
` │ Install the launcher PWA once: ${LAUNCHER_URL}`,
|
|
79
|
+
" │ Then scan the QR below — it opens the launcher directly",
|
|
80
|
+
" │ into this tunnel URL (no manual paste needed).",
|
|
81
|
+
" │ Quick tunnels are unauthenticated, change every run, and are",
|
|
82
|
+
" │ not for production use.",
|
|
83
|
+
" └──────────────────────────────────────────────────────────────",
|
|
84
|
+
""
|
|
85
|
+
].join("\n"));
|
|
86
|
+
if (opts.qr !== false) {
|
|
87
|
+
const qrcode = (await import("qrcode-terminal")).default;
|
|
88
|
+
await new Promise((resolve) => {
|
|
89
|
+
qrcode.generate(deepLink, { small: true }, (out) => {
|
|
90
|
+
log(out);
|
|
91
|
+
resolve();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Sanitize cloudflared stderr output for error diagnostics (#421).
|
|
98
|
+
*
|
|
99
|
+
* Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs
|
|
100
|
+
* that carry those hostnames so tunnel host values never appear in error
|
|
101
|
+
* messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.
|
|
102
|
+
*
|
|
103
|
+
* SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only
|
|
104
|
+
* placeholder text is emitted.
|
|
105
|
+
*/
|
|
106
|
+
function sanitizeCloudflaredOutput(line) {
|
|
107
|
+
let s = line.replace(/(?:https?|wss?):\/\/[a-z0-9-]+\.trycloudflare\.com(?:\/[^\s]*)*/gi, (m) => m.replace(/[a-z0-9-]+\.trycloudflare\.com/i, "<HOST>.trycloudflare.com"));
|
|
108
|
+
s = s.replace(/[a-z0-9-]+\.trycloudflare\.com/gi, "<HOST>.trycloudflare.com");
|
|
109
|
+
return s;
|
|
110
|
+
}
|
|
111
|
+
const URL_TIMEOUT_MS = 2e4;
|
|
112
|
+
/**
|
|
113
|
+
* Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`
|
|
114
|
+
* and resolve once the public URL is known. Downloads the `cloudflared` binary
|
|
115
|
+
* on first use if it is not already installed. Rejects with a friendly error if
|
|
116
|
+
* no URL appears within {@link URL_TIMEOUT_MS}.
|
|
117
|
+
*/
|
|
118
|
+
async function startQuickTunnel(port) {
|
|
119
|
+
const { bin, install, Tunnel } = await import("cloudflared");
|
|
120
|
+
if (!existsSync(bin)) {
|
|
121
|
+
await mkdir(dirname(bin), { recursive: true });
|
|
122
|
+
await install(bin);
|
|
123
|
+
}
|
|
124
|
+
const tunnel = Tunnel.quick(`http://localhost:${port}`);
|
|
125
|
+
let stopped = false;
|
|
126
|
+
const stop = () => {
|
|
127
|
+
if (stopped) return;
|
|
128
|
+
stopped = true;
|
|
129
|
+
try {
|
|
130
|
+
tunnel.stop();
|
|
131
|
+
} catch {}
|
|
132
|
+
};
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const stderrLines = [];
|
|
135
|
+
/**
|
|
136
|
+
* Format the last `n` sanitized stderr lines as a diagnostic appendix.
|
|
137
|
+
* Returns an empty string when no lines have been collected.
|
|
138
|
+
*/
|
|
139
|
+
const stderrTail = (n = 15) => {
|
|
140
|
+
if (stderrLines.length === 0) return "";
|
|
141
|
+
const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join("");
|
|
142
|
+
return `\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\n${tail}`;
|
|
143
|
+
};
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
cleanup();
|
|
146
|
+
stop();
|
|
147
|
+
reject(/* @__PURE__ */ new Error(`[@apps-in-toss/devtools] cloudflared did not report a tunnel URL within ${URL_TIMEOUT_MS / 1e3}s. Check your network connection, or run \`cloudflared tunnel --url http://localhost:${port}\` manually.${stderrTail()}`));
|
|
148
|
+
}, URL_TIMEOUT_MS);
|
|
149
|
+
const onUrl = (line) => {
|
|
150
|
+
const found = parseTrycloudflareUrl(line);
|
|
151
|
+
if (!found) return;
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
cleanup();
|
|
154
|
+
resolve({
|
|
155
|
+
url: found,
|
|
156
|
+
stop
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const pushStderr = (line) => {
|
|
160
|
+
stderrLines.push(line);
|
|
161
|
+
};
|
|
162
|
+
const cleanup = () => {
|
|
163
|
+
tunnel.off("stdout", onUrl);
|
|
164
|
+
tunnel.off("stderr", onUrl);
|
|
165
|
+
tunnel.off("stderr", pushStderr);
|
|
166
|
+
};
|
|
167
|
+
tunnel.once("url", onUrl);
|
|
168
|
+
tunnel.on("stdout", onUrl);
|
|
169
|
+
tunnel.on("stderr", onUrl);
|
|
170
|
+
tunnel.on("stderr", pushStderr);
|
|
171
|
+
tunnel.once("error", (err) => {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
cleanup();
|
|
174
|
+
stop();
|
|
175
|
+
reject(err);
|
|
176
|
+
});
|
|
177
|
+
tunnel.once("exit", (code) => {
|
|
178
|
+
if (stopped) return;
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
cleanup();
|
|
181
|
+
reject(/* @__PURE__ */ new Error(`[@apps-in-toss/devtools] cloudflared exited (code ${code ?? "null"}) before reporting a tunnel URL.${stderrTail()}`));
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
export { printTunnelBanner, startQuickTunnel };
|