@ait-co/devtools 0.1.5 → 0.1.7

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.
@@ -101,7 +101,8 @@ const DEFAULT_STATE = {
101
101
  customWidth: 402,
102
102
  customHeight: 874,
103
103
  frame: false,
104
- aitNavBar: true
104
+ aitNavBar: true,
105
+ aitNavBarType: "partner"
105
106
  }
106
107
  };
107
108
  function generateDeviceId() {
@@ -707,6 +708,22 @@ const PANEL_STYLES = `
707
708
  .ait-navbar-back { padding: 0 8px; font-size: 24px; }
708
709
  .ait-navbar-divider { width: 1px; height: 16px; background: rgba(0, 0, 0, 0.15); }
709
710
 
711
+ /* Game variant: 투명 배경, 우측 actions만 — 풀스크린 게임 캔버스를 가리지 않는다 */
712
+ .ait-navbar.ait-navbar-game {
713
+ background: transparent;
714
+ backdrop-filter: none;
715
+ justify-content: flex-end;
716
+ color: #fff;
717
+ }
718
+ .ait-navbar.ait-navbar-game .ait-navbar-actions {
719
+ background: rgba(0, 0, 0, 0.35);
720
+ color: #fff;
721
+ }
722
+ .ait-navbar.ait-navbar-game .ait-navbar-divider {
723
+ background: rgba(255, 255, 255, 0.3);
724
+ }
725
+ .ait-navbar.ait-navbar-game .ait-navbar-btn:hover { color: #8ab4ff; }
726
+
710
727
  @media (max-width: 720px) {
711
728
  .ait-panel.open {
712
729
  position: fixed;
@@ -1373,7 +1390,119 @@ function renderEventsTab() {
1373
1390
  return container;
1374
1391
  }
1375
1392
  //#endregion
1393
+ //#region src/mock/iap/index.ts
1394
+ /**
1395
+ * IAP (인앱결제) mock
1396
+ */
1397
+ let orderCounter = 0;
1398
+ function generateOrderId() {
1399
+ return `mock-order-${++orderCounter}-${Date.now()}`;
1400
+ }
1401
+ function buildOrderResult(sku) {
1402
+ const product = aitState.state.iap.products.find((p) => p.sku === sku);
1403
+ const amountStr = product?.displayAmount?.replace(/[^0-9]/g, "") ?? "1000";
1404
+ return {
1405
+ orderId: generateOrderId(),
1406
+ displayName: product?.displayName ?? "Mock Product",
1407
+ displayAmount: product?.displayAmount ?? "1,000원",
1408
+ amount: parseInt(amountStr, 10) || 1e3,
1409
+ currency: "KRW",
1410
+ fraction: 0,
1411
+ miniAppIconUrl: product?.iconUrl || null
1412
+ };
1413
+ }
1414
+ async function handlePurchase(sku, processProductGrant, onEvent, onError) {
1415
+ const nextResult = aitState.state.iap.nextResult;
1416
+ await new Promise((r) => setTimeout(r, 300));
1417
+ if (nextResult !== "success") {
1418
+ onError({ code: nextResult });
1419
+ return;
1420
+ }
1421
+ const result = buildOrderResult(sku);
1422
+ try {
1423
+ if (!await processProductGrant({ orderId: result.orderId })) {
1424
+ onError({ code: "PRODUCT_NOT_GRANTED_BY_PARTNER" });
1425
+ return;
1426
+ }
1427
+ } catch (e) {
1428
+ onError(e);
1429
+ return;
1430
+ }
1431
+ aitState.patch("iap", { completedOrders: [...aitState.state.iap.completedOrders, {
1432
+ orderId: result.orderId,
1433
+ sku,
1434
+ status: "COMPLETED",
1435
+ date: (/* @__PURE__ */ new Date()).toISOString()
1436
+ }] });
1437
+ await onEvent({
1438
+ type: "success",
1439
+ data: result
1440
+ });
1441
+ }
1442
+ const IAP = createMockProxy("IAP", {
1443
+ createOneTimePurchaseOrder(params) {
1444
+ handlePurchase(params.options.sku ?? params.options.productId ?? "", params.options.processProductGrant, params.onEvent, params.onError).catch((e) => console.error("[@ait-co/devtools] IAP unexpected error:", e));
1445
+ return () => {};
1446
+ },
1447
+ createSubscriptionPurchaseOrder(params) {
1448
+ handlePurchase(params.options.sku, params.options.processProductGrant, params.onEvent, params.onError).catch((e) => console.error("[@ait-co/devtools] IAP unexpected error:", e));
1449
+ return () => {};
1450
+ },
1451
+ async getProductItemList() {
1452
+ return { products: aitState.state.iap.products.map((p) => ({
1453
+ ...p,
1454
+ ...p.type === "SUBSCRIPTION" ? { renewalCycle: p.renewalCycle ?? "MONTHLY" } : {}
1455
+ })) };
1456
+ },
1457
+ async getPendingOrders() {
1458
+ return { orders: [...aitState.state.iap.pendingOrders] };
1459
+ },
1460
+ async getCompletedOrRefundedOrders() {
1461
+ return {
1462
+ hasNext: false,
1463
+ nextKey: null,
1464
+ orders: [...aitState.state.iap.completedOrders]
1465
+ };
1466
+ },
1467
+ async completeProductGrant(args) {
1468
+ const idx = aitState.state.iap.pendingOrders.findIndex((o) => o.orderId === args.params.orderId);
1469
+ if (idx !== -1) {
1470
+ const order = aitState.state.iap.pendingOrders[idx];
1471
+ const pendingOrders = aitState.state.iap.pendingOrders.filter((_, i) => i !== idx);
1472
+ const completedOrders = [...aitState.state.iap.completedOrders, {
1473
+ orderId: order.orderId,
1474
+ sku: order.sku,
1475
+ status: "COMPLETED",
1476
+ date: (/* @__PURE__ */ new Date()).toISOString()
1477
+ }];
1478
+ aitState.patch("iap", {
1479
+ pendingOrders,
1480
+ completedOrders
1481
+ });
1482
+ }
1483
+ return true;
1484
+ },
1485
+ async getSubscriptionInfo(_args) {
1486
+ return { subscription: {
1487
+ catalogId: 1,
1488
+ status: "ACTIVE",
1489
+ expiresAt: new Date(Date.now() + 720 * 60 * 60 * 1e3).toISOString(),
1490
+ isAutoRenew: true,
1491
+ gracePeriodExpiresAt: null,
1492
+ isAccessible: true
1493
+ } };
1494
+ }
1495
+ });
1496
+ //#endregion
1376
1497
  //#region src/panel/tabs/iap.ts
1498
+ function formatTimestamp(iso) {
1499
+ const d = new Date(iso);
1500
+ if (Number.isNaN(d.getTime())) return iso;
1501
+ return d.toLocaleTimeString();
1502
+ }
1503
+ function shortOrderId(orderId) {
1504
+ return orderId.length > 12 ? `…${orderId.slice(-10)}` : orderId;
1505
+ }
1377
1506
  function renderIapTab() {
1378
1507
  const s = aitState.state;
1379
1508
  const disabled = !s.panelEditable;
@@ -1388,11 +1517,26 @@ function renderIapTab() {
1388
1517
  "INTERNAL_ERROR"
1389
1518
  ];
1390
1519
  if (disabled) container.appendChild(monitoringNotice());
1520
+ const pendingOrders = s.iap.pendingOrders;
1521
+ const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Pending Orders (${pendingOrders.length})`));
1522
+ if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, "(no pending orders)"));
1523
+ else for (const o of pendingOrders) {
1524
+ const completeBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Complete");
1525
+ if (disabled) completeBtn.disabled = true;
1526
+ completeBtn.addEventListener("click", () => {
1527
+ IAP.completeProductGrant({ params: { orderId: o.orderId } }).catch((err) => console.error("[@ait-co/devtools] completeProductGrant error:", err));
1528
+ });
1529
+ pendingSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, "PENDING"), `${o.sku} (${shortOrderId(o.orderId)}) · ${formatTimestamp(o.paymentCompletedDate)} `, completeBtn));
1530
+ }
1531
+ const completedOrders = s.iap.completedOrders;
1532
+ const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Completed Orders (${completedOrders.length})`));
1533
+ if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, "(no completed orders)"));
1534
+ 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)}`));
1391
1535
  container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "IAP Simulator"), selectRow("Next Purchase Result", results, s.iap.nextResult, (v) => {
1392
1536
  aitState.patch("iap", { nextResult: v });
1393
1537
  }, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "TossPay"), selectRow("Next Payment Result", ["success", "fail"], s.payment.nextResult, (v) => {
1394
1538
  aitState.patch("payment", { nextResult: v });
1395
- }, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Completed Orders (${s.iap.completedOrders.length})`), ...s.iap.completedOrders.slice(-5).map((o) => h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, o.status), `${o.sku} (${o.orderId.slice(-8)})`))));
1539
+ }, disabled)), pendingSection, completedSection);
1396
1540
  return container;
1397
1541
  }
1398
1542
  //#endregion
@@ -1755,7 +1899,11 @@ function removeNavBarElement() {
1755
1899
  }
1756
1900
  /**
1757
1901
  * Apps in Toss host nav bar 렌더. OS status bar 아래에 48px 높이로 쌓인다.
1758
- * 구성: 좌측 뒤로가기(‹), 앱 아이콘 + 이름(`brand.displayName`), 우측 `⋯` + 구분선 + `×`.
1902
+ *
1903
+ * 변형(SDK `webViewProps.type`과 의미 일치):
1904
+ * - `partner` (기본): 흰 배경, 좌측 뒤로가기(‹), 앱 아이콘 + 이름(`brand.displayName`),
1905
+ * 우측 `⋯` + 구분선 + `×`.
1906
+ * - `game`: 투명 배경, 게임 캔버스를 가리지 않도록 우측 `⋯` + 구분선 + `×`만.
1759
1907
  *
1760
1908
  * `env(safe-area-inset-top)`에는 이 높이가 포함되지 않으므로 (공식 SDK 확인),
1761
1909
  * 오버레이는 preset.safeAreaTop만큼 아래로 내려서 그린다.
@@ -1763,23 +1911,14 @@ function removeNavBarElement() {
1763
1911
  * 뒤로가기 버튼은 `__ait:backEvent`를 트리거하고, X 버튼은 `closeView()`를 호출한다.
1764
1912
  * 실제 SDK 이벤트 플러밍을 한 곳에서 검증할 수 있다.
1765
1913
  */
1766
- function renderNavBar(preset, displayName) {
1914
+ function renderNavBar(preset, displayName, type) {
1767
1915
  removeNavBarElement();
1768
1916
  const el = h("div", {
1769
1917
  id: NAV_BAR_ELEMENT_ID,
1770
- className: "ait-navbar",
1918
+ className: `ait-navbar ait-navbar-${type}`,
1771
1919
  "aria-hidden": "true"
1772
1920
  });
1773
1921
  el.style.top = `${preset.safeAreaTop}px`;
1774
- const backBtn = h("button", {
1775
- className: "ait-navbar-btn ait-navbar-back",
1776
- type: "button",
1777
- "aria-label": "Back"
1778
- });
1779
- backBtn.textContent = "‹";
1780
- backBtn.addEventListener("click", () => {
1781
- aitState.trigger("backEvent");
1782
- });
1783
1922
  const moreBtn = h("button", {
1784
1923
  className: "ait-navbar-btn",
1785
1924
  type: "button",
@@ -1795,9 +1934,22 @@ function renderNavBar(preset, displayName) {
1795
1934
  closeBtn.addEventListener("click", () => {
1796
1935
  closeView().catch((err) => console.error("[@ait-co/devtools] navbar close failed:", err));
1797
1936
  });
1798
- const nameSpan = h("span", { className: "ait-navbar-name" });
1799
- nameSpan.textContent = displayName;
1800
- el.append(backBtn, h("div", { className: "ait-navbar-title" }, h("span", { className: "ait-navbar-icon" }), nameSpan), h("div", { className: "ait-navbar-actions" }, moreBtn, h("span", { className: "ait-navbar-divider" }), closeBtn));
1937
+ const actions = h("div", { className: "ait-navbar-actions" }, moreBtn, h("span", { className: "ait-navbar-divider" }), closeBtn);
1938
+ if (type === "game") el.append(actions);
1939
+ else {
1940
+ const backBtn = h("button", {
1941
+ className: "ait-navbar-btn ait-navbar-back",
1942
+ type: "button",
1943
+ "aria-label": "Back"
1944
+ });
1945
+ backBtn.textContent = "‹";
1946
+ backBtn.addEventListener("click", () => {
1947
+ aitState.trigger("backEvent");
1948
+ });
1949
+ const nameSpan = h("span", { className: "ait-navbar-name" });
1950
+ nameSpan.textContent = displayName;
1951
+ el.append(backBtn, h("div", { className: "ait-navbar-title" }, h("span", { className: "ait-navbar-icon" }), nameSpan), actions);
1952
+ }
1801
1953
  document.body.appendChild(el);
1802
1954
  }
1803
1955
  /**
@@ -1869,7 +2021,7 @@ function applyViewport(state) {
1869
2021
  else removeNotchElement();
1870
2022
  if (preset && state.frame && !landscape && preset.safeAreaBottom > 0) renderHomeIndicator();
1871
2023
  else removeHomeIndicator();
1872
- if (preset && state.aitNavBar && !landscape) renderNavBar(preset, aitState.state.brand.displayName);
2024
+ if (preset && state.aitNavBar && !landscape) renderNavBar(preset, aitState.state.brand.displayName, state.aitNavBarType);
1873
2025
  else removeNavBarElement();
1874
2026
  }
1875
2027
  function isViewportPresetId(v) {
@@ -1916,6 +2068,7 @@ function loadViewportFromStorage() {
1916
2068
  if (isValidCustomDimension(obj.customHeight)) next.customHeight = obj.customHeight;
1917
2069
  if (typeof obj.frame === "boolean") next.frame = obj.frame;
1918
2070
  if (typeof obj.aitNavBar === "boolean") next.aitNavBar = obj.aitNavBar;
2071
+ if (obj.aitNavBarType === "partner" || obj.aitNavBarType === "game") next.aitNavBarType = obj.aitNavBarType;
1919
2072
  return next;
1920
2073
  } catch {
1921
2074
  return null;
@@ -2063,6 +2216,16 @@ function renderViewportTab() {
2063
2216
  navBarCheckbox.addEventListener("change", () => {
2064
2217
  aitState.patch("viewport", { aitNavBar: navBarCheckbox.checked });
2065
2218
  });
2219
+ const navBarTypeSelect = h("select", { className: "ait-select" });
2220
+ if (disabled || !vp.aitNavBar) navBarTypeSelect.disabled = true;
2221
+ for (const opt of ["partner", "game"]) {
2222
+ const option = h("option", { value: opt }, opt);
2223
+ if (opt === vp.aitNavBarType) option.selected = true;
2224
+ navBarTypeSelect.appendChild(option);
2225
+ }
2226
+ navBarTypeSelect.addEventListener("change", () => {
2227
+ aitState.patch("viewport", { aitNavBarType: navBarTypeSelect.value });
2228
+ });
2066
2229
  const size = resolveViewportSize(vp);
2067
2230
  const statusEl = h("div", { className: "ait-section" });
2068
2231
  if (vp.preset === "none" || size.width === 0) statusEl.appendChild(h("div", { style: "color:#888;font-size:11px" }, "No viewport constraint — body fills the window."));
@@ -2080,7 +2243,7 @@ function renderViewportTab() {
2080
2243
  const insets = computeSafeAreaInsets(preset, landscape, vp.landscapeSide);
2081
2244
  rows.push(h("div", { className: "ait-status-row" }, h("span", {}, "Safe area"), h("span", { className: "ait-status-value" }, `T${insets.top} R${insets.right} B${insets.bottom} L${insets.left}`)));
2082
2245
  }
2083
- if (vp.aitNavBar && !landscape) rows.push(h("div", { className: "ait-status-row" }, h("span", {}, "AIT nav bar"), h("span", { className: "ait-status-value" }, `48px (excl. SafeArea)`)));
2246
+ if (vp.aitNavBar && !landscape) rows.push(h("div", { className: "ait-status-row" }, h("span", {}, "AIT nav bar"), h("span", { className: "ait-status-value" }, `48px (excl. SafeArea) · ${vp.aitNavBarType}`)));
2084
2247
  for (const row of rows) statusEl.appendChild(row);
2085
2248
  }
2086
2249
  const deviceSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Device"), h("div", { className: "ait-row" }, h("label", {}, "Preset"), presetSelect), h("div", { className: "ait-row" }, h("label", {}, "Orientation"), orientationSelect));
@@ -2088,7 +2251,7 @@ function renderViewportTab() {
2088
2251
  const notch = getPreset(vp.preset).notch;
2089
2252
  if (notch === "notch" || notch === "dynamic-island") deviceSection.appendChild(h("div", { className: "ait-row" }, h("label", {}, "Notch side"), landscapeSideSelect));
2090
2253
  }
2091
- container.append(deviceSection, customRow, h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Appearance"), h("div", { className: "ait-row" }, h("label", {}, "Show frame"), frameCheckbox), h("div", { className: "ait-row" }, h("label", {}, "Show Apps in Toss nav bar"), navBarCheckbox)), statusEl);
2254
+ container.append(deviceSection, customRow, h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Appearance"), h("div", { className: "ait-row" }, h("label", {}, "Show frame"), frameCheckbox), h("div", { className: "ait-row" }, h("label", {}, "Show Apps in Toss nav bar"), navBarCheckbox), h("div", { className: "ait-row" }, h("label", {}, "Nav bar type"), navBarTypeSelect)), statusEl);
2092
2255
  return container;
2093
2256
  }
2094
2257
  //#endregion
@@ -2339,7 +2502,7 @@ function mount() {
2339
2502
  mockBadge.textContent = aitState.state.panelEditable ? "EDIT" : "READ-ONLY";
2340
2503
  refreshPanel();
2341
2504
  });
2342
- const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.5`), closeBtn);
2505
+ const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.7`), closeBtn);
2343
2506
  const header = h("div", { className: "ait-panel-header" }, h("span", {}, "AIT DevTools"), headerRight);
2344
2507
  tabsEl = h("div", { className: "ait-panel-tabs" });
2345
2508
  for (const tab of TABS) {
@@ -2378,7 +2541,7 @@ function mount() {
2378
2541
  });
2379
2542
  aitState.subscribe(() => {
2380
2543
  try {
2381
- if (isOpen && (currentTab === "analytics" || currentTab === "storage" || currentTab === "device" || currentTab === "viewport")) refreshPanel();
2544
+ if (isOpen && (currentTab === "analytics" || currentTab === "storage" || currentTab === "device" || currentTab === "viewport" || currentTab === "iap")) refreshPanel();
2382
2545
  } catch (err) {
2383
2546
  console.error("[@ait-co/devtools] Error in subscribe callback:", err);
2384
2547
  }