@ait-co/devtools 0.1.6 → 0.1.8

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.
@@ -71,7 +71,9 @@ const DEFAULT_STATE = {
71
71
  },
72
72
  ads: {
73
73
  isLoaded: false,
74
- nextEvent: "loaded"
74
+ nextEvent: "loaded",
75
+ forceNoFill: false,
76
+ lastEvent: null
75
77
  },
76
78
  game: {
77
79
  profile: {
@@ -1335,6 +1337,192 @@ function renderDeviceTab() {
1335
1337
  return container;
1336
1338
  }
1337
1339
  //#endregion
1340
+ //#region src/mock/ads/index.ts
1341
+ /**
1342
+ * 광고 mock (GoogleAdMob, TossAds, FullScreenAd)
1343
+ */
1344
+ function withIsSupported(fn) {
1345
+ fn.isSupported = () => true;
1346
+ return fn;
1347
+ }
1348
+ const GoogleAdMob = createMockProxy("GoogleAdMob", {
1349
+ loadAppsInTossAdMob: withIsSupported((args) => {
1350
+ setTimeout(() => {
1351
+ if (aitState.state.ads.forceNoFill) {
1352
+ args.onError(/* @__PURE__ */ new Error("No fill"));
1353
+ return;
1354
+ }
1355
+ aitState.patch("ads", { isLoaded: true });
1356
+ args.onEvent({
1357
+ type: "loaded",
1358
+ data: { adGroupId: args.options?.adGroupId }
1359
+ });
1360
+ }, 200);
1361
+ return () => {};
1362
+ }),
1363
+ showAppsInTossAdMob: withIsSupported((args) => {
1364
+ if (!aitState.state.ads.isLoaded) {
1365
+ args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
1366
+ return () => {};
1367
+ }
1368
+ setTimeout(() => args.onEvent({ type: "requested" }), 50);
1369
+ setTimeout(() => args.onEvent({ type: "show" }), 100);
1370
+ setTimeout(() => args.onEvent({ type: "impression" }), 150);
1371
+ setTimeout(() => {
1372
+ args.onEvent({
1373
+ type: "userEarnedReward",
1374
+ data: {
1375
+ unitType: "coins",
1376
+ unitAmount: 10
1377
+ }
1378
+ });
1379
+ }, 1e3);
1380
+ setTimeout(() => {
1381
+ args.onEvent({ type: "dismissed" });
1382
+ aitState.patch("ads", { isLoaded: false });
1383
+ }, 1500);
1384
+ return () => {};
1385
+ }),
1386
+ isAppsInTossAdMobLoaded: withIsSupported(async (_options) => aitState.state.ads.isLoaded)
1387
+ });
1388
+ createMockProxy("TossAds", {
1389
+ initialize: withIsSupported((_options) => {
1390
+ console.log("[@ait-co/devtools] TossAds.initialize (mock)");
1391
+ }),
1392
+ attach: withIsSupported((_adGroupId, target, _options) => {
1393
+ const el = typeof target === "string" ? document.querySelector(target) : target;
1394
+ if (el) {
1395
+ const placeholder = document.createElement("div");
1396
+ placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:16px;text-align:center;color:#666;font-size:14px;";
1397
+ placeholder.textContent = "[@ait-co/devtools] TossAds Placeholder";
1398
+ el.appendChild(placeholder);
1399
+ }
1400
+ }),
1401
+ attachBanner: withIsSupported((_adGroupId, target, _options) => {
1402
+ const el = typeof target === "string" ? document.querySelector(target) : target;
1403
+ if (el) {
1404
+ const placeholder = document.createElement("div");
1405
+ placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:12px;text-align:center;color:#666;font-size:12px;";
1406
+ placeholder.textContent = "[@ait-co/devtools] Banner Ad Placeholder";
1407
+ el.appendChild(placeholder);
1408
+ }
1409
+ return { destroy: () => {} };
1410
+ }),
1411
+ destroy: withIsSupported((_slotId) => {}),
1412
+ destroyAll: withIsSupported(() => {})
1413
+ });
1414
+ const loadFullScreenAd = withIsSupported((args) => {
1415
+ setTimeout(() => {
1416
+ if (aitState.state.ads.forceNoFill) {
1417
+ args.onError(/* @__PURE__ */ new Error("No fill"));
1418
+ return;
1419
+ }
1420
+ aitState.patch("ads", { isLoaded: true });
1421
+ args.onEvent({
1422
+ type: "loaded",
1423
+ data: { adGroupId: args.options?.adGroupId }
1424
+ });
1425
+ }, 200);
1426
+ return () => {};
1427
+ });
1428
+ const showFullScreenAd = withIsSupported((args) => {
1429
+ if (!aitState.state.ads.isLoaded) {
1430
+ args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
1431
+ return () => {};
1432
+ }
1433
+ setTimeout(() => args.onEvent({ type: "show" }), 100);
1434
+ setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
1435
+ return () => {};
1436
+ });
1437
+ //#endregion
1438
+ //#region src/panel/tabs/ads.ts
1439
+ function recordEvent(type) {
1440
+ aitState.patch("ads", { lastEvent: {
1441
+ type,
1442
+ timestamp: Date.now()
1443
+ } });
1444
+ }
1445
+ function recordError(message) {
1446
+ recordEvent(`error: ${message}`);
1447
+ }
1448
+ function statusRow(label, value) {
1449
+ return h("div", { className: "ait-row" }, h("label", {}, label), h("span", { style: "font-family:SF Mono,Menlo,monospace;font-size:11px;color:#aaa" }, value));
1450
+ }
1451
+ function lastEventLine() {
1452
+ const last = aitState.state.ads.lastEvent;
1453
+ if (!last) return h("div", { className: "ait-log-entry" }, h("span", { style: "color:#555" }, "No events yet"));
1454
+ const time = new Date(last.timestamp).toLocaleTimeString();
1455
+ return h("div", { className: "ait-log-entry" }, h("span", {
1456
+ className: "ait-log-type",
1457
+ style: last.type.startsWith("error:") ? "color:#e74c3c" : ""
1458
+ }, last.type), h("span", { className: "ait-log-time" }, time));
1459
+ }
1460
+ function adSection(title, onLoad, onShow, disabled) {
1461
+ const loadBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Load");
1462
+ const showBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Show");
1463
+ if (disabled) {
1464
+ loadBtn.disabled = true;
1465
+ showBtn.disabled = true;
1466
+ }
1467
+ loadBtn.addEventListener("click", onLoad);
1468
+ showBtn.addEventListener("click", onShow);
1469
+ return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, title), h("div", { className: "ait-btn-row" }, loadBtn, showBtn));
1470
+ }
1471
+ function renderAdsTab() {
1472
+ const s = aitState.state;
1473
+ const disabled = !s.panelEditable;
1474
+ const container = h("div");
1475
+ if (disabled) container.appendChild(monitoringNotice());
1476
+ const forceNoFillCb = h("input", {
1477
+ type: "checkbox",
1478
+ className: "ait-checkbox"
1479
+ });
1480
+ forceNoFillCb.checked = s.ads.forceNoFill;
1481
+ if (disabled) forceNoFillCb.disabled = true;
1482
+ forceNoFillCb.addEventListener("change", () => {
1483
+ aitState.patch("ads", { forceNoFill: forceNoFillCb.checked });
1484
+ });
1485
+ container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "Ads State"), statusRow("isLoaded", String(s.ads.isLoaded)), h("div", { className: "ait-row" }, h("label", {}, "Force \"no fill\""), forceNoFillCb), lastEventLine()), adSection("GoogleAdMob", () => {
1486
+ GoogleAdMob.loadAppsInTossAdMob({
1487
+ onEvent: (e) => recordEvent(e.type),
1488
+ onError: (err) => recordError(err.message)
1489
+ });
1490
+ }, () => {
1491
+ GoogleAdMob.showAppsInTossAdMob({
1492
+ onEvent: (e) => recordEvent(e.type),
1493
+ onError: (err) => recordError(err.message)
1494
+ });
1495
+ }, disabled), adSection("TossAds", () => {
1496
+ if (aitState.state.ads.forceNoFill) {
1497
+ recordError("No fill");
1498
+ return;
1499
+ }
1500
+ aitState.patch("ads", { isLoaded: true });
1501
+ recordEvent("loaded");
1502
+ }, () => {
1503
+ if (!aitState.state.ads.isLoaded) {
1504
+ recordError("Ad not loaded");
1505
+ return;
1506
+ }
1507
+ recordEvent("show");
1508
+ setTimeout(() => {
1509
+ recordEvent("dismissed");
1510
+ aitState.patch("ads", { isLoaded: false });
1511
+ }, 1500);
1512
+ }, disabled), adSection("FullScreenAd", () => {
1513
+ loadFullScreenAd({
1514
+ onEvent: (e) => recordEvent(e.type),
1515
+ onError: (err) => recordError(err.message)
1516
+ });
1517
+ }, () => {
1518
+ showFullScreenAd({
1519
+ onEvent: (e) => recordEvent(e.type),
1520
+ onError: (err) => recordError(err.message)
1521
+ });
1522
+ }, disabled));
1523
+ return container;
1524
+ }
1525
+ //#endregion
1338
1526
  //#region src/panel/tabs/analytics.ts
1339
1527
  function renderAnalyticsTab() {
1340
1528
  const disabled = !aitState.state.panelEditable;
@@ -1390,7 +1578,119 @@ function renderEventsTab() {
1390
1578
  return container;
1391
1579
  }
1392
1580
  //#endregion
1581
+ //#region src/mock/iap/index.ts
1582
+ /**
1583
+ * IAP (인앱결제) mock
1584
+ */
1585
+ let orderCounter = 0;
1586
+ function generateOrderId() {
1587
+ return `mock-order-${++orderCounter}-${Date.now()}`;
1588
+ }
1589
+ function buildOrderResult(sku) {
1590
+ const product = aitState.state.iap.products.find((p) => p.sku === sku);
1591
+ const amountStr = product?.displayAmount?.replace(/[^0-9]/g, "") ?? "1000";
1592
+ return {
1593
+ orderId: generateOrderId(),
1594
+ displayName: product?.displayName ?? "Mock Product",
1595
+ displayAmount: product?.displayAmount ?? "1,000원",
1596
+ amount: parseInt(amountStr, 10) || 1e3,
1597
+ currency: "KRW",
1598
+ fraction: 0,
1599
+ miniAppIconUrl: product?.iconUrl || null
1600
+ };
1601
+ }
1602
+ async function handlePurchase(sku, processProductGrant, onEvent, onError) {
1603
+ const nextResult = aitState.state.iap.nextResult;
1604
+ await new Promise((r) => setTimeout(r, 300));
1605
+ if (nextResult !== "success") {
1606
+ onError({ code: nextResult });
1607
+ return;
1608
+ }
1609
+ const result = buildOrderResult(sku);
1610
+ try {
1611
+ if (!await processProductGrant({ orderId: result.orderId })) {
1612
+ onError({ code: "PRODUCT_NOT_GRANTED_BY_PARTNER" });
1613
+ return;
1614
+ }
1615
+ } catch (e) {
1616
+ onError(e);
1617
+ return;
1618
+ }
1619
+ aitState.patch("iap", { completedOrders: [...aitState.state.iap.completedOrders, {
1620
+ orderId: result.orderId,
1621
+ sku,
1622
+ status: "COMPLETED",
1623
+ date: (/* @__PURE__ */ new Date()).toISOString()
1624
+ }] });
1625
+ await onEvent({
1626
+ type: "success",
1627
+ data: result
1628
+ });
1629
+ }
1630
+ const IAP = createMockProxy("IAP", {
1631
+ createOneTimePurchaseOrder(params) {
1632
+ 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));
1633
+ return () => {};
1634
+ },
1635
+ createSubscriptionPurchaseOrder(params) {
1636
+ handlePurchase(params.options.sku, params.options.processProductGrant, params.onEvent, params.onError).catch((e) => console.error("[@ait-co/devtools] IAP unexpected error:", e));
1637
+ return () => {};
1638
+ },
1639
+ async getProductItemList() {
1640
+ return { products: aitState.state.iap.products.map((p) => ({
1641
+ ...p,
1642
+ ...p.type === "SUBSCRIPTION" ? { renewalCycle: p.renewalCycle ?? "MONTHLY" } : {}
1643
+ })) };
1644
+ },
1645
+ async getPendingOrders() {
1646
+ return { orders: [...aitState.state.iap.pendingOrders] };
1647
+ },
1648
+ async getCompletedOrRefundedOrders() {
1649
+ return {
1650
+ hasNext: false,
1651
+ nextKey: null,
1652
+ orders: [...aitState.state.iap.completedOrders]
1653
+ };
1654
+ },
1655
+ async completeProductGrant(args) {
1656
+ const idx = aitState.state.iap.pendingOrders.findIndex((o) => o.orderId === args.params.orderId);
1657
+ if (idx !== -1) {
1658
+ const order = aitState.state.iap.pendingOrders[idx];
1659
+ const pendingOrders = aitState.state.iap.pendingOrders.filter((_, i) => i !== idx);
1660
+ const completedOrders = [...aitState.state.iap.completedOrders, {
1661
+ orderId: order.orderId,
1662
+ sku: order.sku,
1663
+ status: "COMPLETED",
1664
+ date: (/* @__PURE__ */ new Date()).toISOString()
1665
+ }];
1666
+ aitState.patch("iap", {
1667
+ pendingOrders,
1668
+ completedOrders
1669
+ });
1670
+ }
1671
+ return true;
1672
+ },
1673
+ async getSubscriptionInfo(_args) {
1674
+ return { subscription: {
1675
+ catalogId: 1,
1676
+ status: "ACTIVE",
1677
+ expiresAt: new Date(Date.now() + 720 * 60 * 60 * 1e3).toISOString(),
1678
+ isAutoRenew: true,
1679
+ gracePeriodExpiresAt: null,
1680
+ isAccessible: true
1681
+ } };
1682
+ }
1683
+ });
1684
+ //#endregion
1393
1685
  //#region src/panel/tabs/iap.ts
1686
+ function formatTimestamp(iso) {
1687
+ const d = new Date(iso);
1688
+ if (Number.isNaN(d.getTime())) return iso;
1689
+ return d.toLocaleTimeString();
1690
+ }
1691
+ function shortOrderId(orderId) {
1692
+ return orderId.length > 12 ? `…${orderId.slice(-10)}` : orderId;
1693
+ }
1394
1694
  function renderIapTab() {
1395
1695
  const s = aitState.state;
1396
1696
  const disabled = !s.panelEditable;
@@ -1405,11 +1705,26 @@ function renderIapTab() {
1405
1705
  "INTERNAL_ERROR"
1406
1706
  ];
1407
1707
  if (disabled) container.appendChild(monitoringNotice());
1708
+ const pendingOrders = s.iap.pendingOrders;
1709
+ const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Pending Orders (${pendingOrders.length})`));
1710
+ if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, "(no pending orders)"));
1711
+ else for (const o of pendingOrders) {
1712
+ const completeBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Complete");
1713
+ if (disabled) completeBtn.disabled = true;
1714
+ completeBtn.addEventListener("click", () => {
1715
+ IAP.completeProductGrant({ params: { orderId: o.orderId } }).catch((err) => console.error("[@ait-co/devtools] completeProductGrant error:", err));
1716
+ });
1717
+ pendingSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, "PENDING"), `${o.sku} (${shortOrderId(o.orderId)}) · ${formatTimestamp(o.paymentCompletedDate)} `, completeBtn));
1718
+ }
1719
+ const completedOrders = s.iap.completedOrders;
1720
+ const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, `Completed Orders (${completedOrders.length})`));
1721
+ if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, "(no completed orders)"));
1722
+ 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)}`));
1408
1723
  container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "IAP Simulator"), selectRow("Next Purchase Result", results, s.iap.nextResult, (v) => {
1409
1724
  aitState.patch("iap", { nextResult: v });
1410
1725
  }, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "TossPay"), selectRow("Next Payment Result", ["success", "fail"], s.payment.nextResult, (v) => {
1411
1726
  aitState.patch("payment", { nextResult: v });
1412
- }, 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)})`))));
1727
+ }, disabled)), pendingSection, completedSection);
1413
1728
  return container;
1414
1729
  }
1415
1730
  //#endregion
@@ -2154,6 +2469,10 @@ const TABS = [
2154
2469
  id: "iap",
2155
2470
  label: "IAP"
2156
2471
  },
2472
+ {
2473
+ id: "ads",
2474
+ label: "Ads"
2475
+ },
2157
2476
  {
2158
2477
  id: "events",
2159
2478
  label: "Events"
@@ -2175,6 +2494,7 @@ function createTabRenderers(refreshPanel) {
2175
2494
  device: renderDeviceTab,
2176
2495
  viewport: renderViewportTab,
2177
2496
  iap: renderIapTab,
2497
+ ads: renderAdsTab,
2178
2498
  events: renderEventsTab,
2179
2499
  analytics: renderAnalyticsTab,
2180
2500
  storage: () => renderStorageTab(refreshPanel)
@@ -2375,7 +2695,7 @@ function mount() {
2375
2695
  mockBadge.textContent = aitState.state.panelEditable ? "EDIT" : "READ-ONLY";
2376
2696
  refreshPanel();
2377
2697
  });
2378
- 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.6`), closeBtn);
2698
+ 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.8`), closeBtn);
2379
2699
  const header = h("div", { className: "ait-panel-header" }, h("span", {}, "AIT DevTools"), headerRight);
2380
2700
  tabsEl = h("div", { className: "ait-panel-tabs" });
2381
2701
  for (const tab of TABS) {
@@ -2414,7 +2734,7 @@ function mount() {
2414
2734
  });
2415
2735
  aitState.subscribe(() => {
2416
2736
  try {
2417
- if (isOpen && (currentTab === "analytics" || currentTab === "storage" || currentTab === "device" || currentTab === "viewport")) refreshPanel();
2737
+ if (isOpen && (currentTab === "analytics" || currentTab === "storage" || currentTab === "device" || currentTab === "viewport" || currentTab === "iap" || currentTab === "ads")) refreshPanel();
2418
2738
  } catch (err) {
2419
2739
  console.error("[@ait-co/devtools] Error in subscribe callback:", err);
2420
2740
  }