@wowlabtech/mini-app-adapter 0.2.1 → 0.2.3

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/dist/index.cjs CHANGED
@@ -479,7 +479,7 @@ async function startHtml5Qrcode() {
479
479
  if (typeof document === "undefined") {
480
480
  throw new Error("QR scanning requires a browser environment.");
481
481
  }
482
- const [{ Html5Qrcode: Html5Qrcode2 }] = await Promise.all([import("html5-qrcode")]);
482
+ const [{ Html5Qrcode }] = await Promise.all([import("html5-qrcode")]);
483
483
  return new Promise((resolve, reject) => {
484
484
  const elementId = `native-shell-qr-${Date.now()}`;
485
485
  const overlay = document.createElement("div");
@@ -516,7 +516,7 @@ async function startHtml5Qrcode() {
516
516
  const previousOverflow = document.body.style.overflow;
517
517
  document.body.style.overflow = "hidden";
518
518
  let disposed = false;
519
- const scanner = new Html5Qrcode2(elementId);
519
+ const scanner = new Html5Qrcode(elementId);
520
520
  const cleanup = async (result, error) => {
521
521
  if (disposed) {
522
522
  return;
@@ -665,6 +665,27 @@ var BaseMiniAppAdapter = class {
665
665
  }
666
666
  requestFullscreen() {
667
667
  }
668
+ onViewportChange(callback) {
669
+ if (typeof window === "undefined") {
670
+ return () => {
671
+ };
672
+ }
673
+ const fallbackHeight = () => window.visualViewport?.height ?? window.innerHeight;
674
+ const notify = () => {
675
+ const height = fallbackHeight();
676
+ callback({ height, stableHeight: height });
677
+ };
678
+ notify();
679
+ const onResize = () => notify();
680
+ window.visualViewport?.addEventListener("resize", onResize);
681
+ window.visualViewport?.addEventListener("scroll", onResize);
682
+ window.addEventListener("resize", onResize);
683
+ return () => {
684
+ window.visualViewport?.removeEventListener("resize", onResize);
685
+ window.visualViewport?.removeEventListener("scroll", onResize);
686
+ window.removeEventListener("resize", onResize);
687
+ };
688
+ }
668
689
  getViewportInsets() {
669
690
  return void 0;
670
691
  }
@@ -1406,6 +1427,63 @@ var TelegramMiniAppAdapter = class extends BaseMiniAppAdapter {
1406
1427
  return void 0;
1407
1428
  }
1408
1429
  }
1430
+ onViewportChange(callback) {
1431
+ const disposers = [];
1432
+ const fallbackHeight = () => typeof window !== "undefined" ? window.visualViewport?.height ?? window.innerHeight : 0;
1433
+ const notify = (state) => {
1434
+ const heightCandidate = state?.height ?? this.safeHeightFromSdk();
1435
+ const stableCandidate = state?.stableHeight ?? this.stableHeightFromSdk();
1436
+ const height = Number.isFinite(heightCandidate) ? heightCandidate : fallbackHeight();
1437
+ const stableHeight = Number.isFinite(stableCandidate) && stableCandidate > 0 ? stableCandidate : height;
1438
+ callback({ height, stableHeight });
1439
+ };
1440
+ const ensureMounted = async () => {
1441
+ try {
1442
+ await ensureViewportMounted(this.getViewportMountOptions());
1443
+ } catch (error) {
1444
+ console.warn("[tvm-app-adapter] ensureViewportMounted failed:", error);
1445
+ }
1446
+ };
1447
+ void ensureMounted().finally(() => notify());
1448
+ const { sdkViewport } = this.getViewportMountOptions();
1449
+ if (typeof sdkViewport.on === "function") {
1450
+ try {
1451
+ const off2 = sdkViewport.on("change", (next) => notify(next));
1452
+ if (typeof off2 === "function") {
1453
+ disposers.push(off2);
1454
+ }
1455
+ } catch (error) {
1456
+ console.warn("[tvm-app-adapter] viewport.on(change) subscription failed:", error);
1457
+ }
1458
+ }
1459
+ try {
1460
+ if (typeof sdkViewport.height?.sub === "function") {
1461
+ disposers.push(sdkViewport.height.sub(() => notify()));
1462
+ }
1463
+ if (typeof sdkViewport.stableHeight?.sub === "function") {
1464
+ disposers.push(sdkViewport.stableHeight.sub(() => notify()));
1465
+ }
1466
+ } catch (error) {
1467
+ console.warn("[tvm-app-adapter] viewport signal subscriptions failed:", error);
1468
+ }
1469
+ if (typeof window !== "undefined") {
1470
+ const onResize = () => notify();
1471
+ window.visualViewport?.addEventListener("resize", onResize);
1472
+ window.addEventListener("resize", onResize);
1473
+ disposers.push(() => {
1474
+ window.visualViewport?.removeEventListener("resize", onResize);
1475
+ window.removeEventListener("resize", onResize);
1476
+ });
1477
+ }
1478
+ return () => {
1479
+ disposers.forEach((dispose) => {
1480
+ try {
1481
+ dispose();
1482
+ } catch {
1483
+ }
1484
+ });
1485
+ };
1486
+ }
1409
1487
  onAppearanceChange(callback) {
1410
1488
  this.appearanceListeners.add(callback);
1411
1489
  callback(this.environment.appearance);
@@ -1636,6 +1714,26 @@ var TelegramMiniAppAdapter = class extends BaseMiniAppAdapter {
1636
1714
  }
1637
1715
  };
1638
1716
  }
1717
+ safeHeightFromSdk() {
1718
+ try {
1719
+ if (typeof import_sdk.viewport.height === "function") {
1720
+ return import_sdk.viewport.height();
1721
+ }
1722
+ } catch {
1723
+ return void 0;
1724
+ }
1725
+ return void 0;
1726
+ }
1727
+ stableHeightFromSdk() {
1728
+ try {
1729
+ if (typeof import_sdk.viewport.stableHeight === "function") {
1730
+ return import_sdk.viewport.stableHeight();
1731
+ }
1732
+ } catch {
1733
+ return void 0;
1734
+ }
1735
+ return void 0;
1736
+ }
1639
1737
  notifyViewHide() {
1640
1738
  for (const listener of this.viewHideListeners) {
1641
1739
  try {
@@ -2303,7 +2401,7 @@ var VKMiniAppAdapter = class extends BaseMiniAppAdapter {
2303
2401
  };
2304
2402
 
2305
2403
  // src/adapters/webAdapter.ts
2306
- var import_html5_qrcode = require("html5-qrcode");
2404
+ var import_jsqr = __toESM(require("jsqr"), 1);
2307
2405
  var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2308
2406
  constructor() {
2309
2407
  super("web", {
@@ -2373,14 +2471,31 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2373
2471
  closeBtn.style.cursor = "pointer";
2374
2472
  closeBtn.style.zIndex = "9999999999";
2375
2473
  overlay.appendChild(closeBtn);
2474
+ const scanSize = Math.min(Math.floor(Math.min(window.innerWidth, window.innerHeight) * 0.72), 320);
2475
+ const scanBox = document.createElement("div");
2476
+ scanBox.style.width = `${scanSize}px`;
2477
+ scanBox.style.height = `${scanSize}px`;
2478
+ scanBox.style.position = "relative";
2479
+ scanBox.style.flex = "0 0 auto";
2480
+ scanBox.style.borderRadius = "18px";
2481
+ scanBox.style.overflow = "hidden";
2482
+ overlay.appendChild(scanBox);
2376
2483
  const scanArea = document.createElement("div");
2377
- scanArea.id = "qr-reader";
2378
- scanArea.style.width = "300px";
2379
- scanArea.style.height = "300px";
2380
- scanArea.style.borderRadius = "18px";
2381
- scanArea.style.overflow = "hidden";
2382
- scanArea.style.position = "relative";
2383
- overlay.appendChild(scanArea);
2484
+ scanArea.style.position = "absolute";
2485
+ scanArea.style.inset = "0";
2486
+ scanArea.style.zIndex = "1";
2487
+ scanArea.style.background = "#000";
2488
+ scanBox.appendChild(scanArea);
2489
+ const video = document.createElement("video");
2490
+ video.setAttribute("playsinline", "true");
2491
+ video.autoplay = true;
2492
+ video.muted = true;
2493
+ video.style.width = "100%";
2494
+ video.style.height = "100%";
2495
+ video.style.objectFit = "cover";
2496
+ video.style.position = "absolute";
2497
+ video.style.inset = "0";
2498
+ scanArea.appendChild(video);
2384
2499
  const frame = document.createElement("div");
2385
2500
  frame.style.position = "absolute";
2386
2501
  frame.style.top = "0";
@@ -2390,8 +2505,8 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2390
2505
  frame.style.border = "3px solid rgba(255,255,255,0.9)";
2391
2506
  frame.style.borderRadius = "18px";
2392
2507
  frame.style.pointerEvents = "none";
2393
- frame.style.zIndex = "10";
2394
- scanArea.appendChild(frame);
2508
+ frame.style.zIndex = "3";
2509
+ scanBox.appendChild(frame);
2395
2510
  const line = document.createElement("div");
2396
2511
  line.style.position = "absolute";
2397
2512
  line.style.left = "0";
@@ -2400,8 +2515,8 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2400
2515
  line.style.background = "rgba(255,255,255,0.85)";
2401
2516
  line.style.borderRadius = "2px";
2402
2517
  line.style.animation = "qr-line 2s infinite";
2403
- line.style.zIndex = "11";
2404
- scanArea.appendChild(line);
2518
+ line.style.zIndex = "4";
2519
+ scanBox.appendChild(line);
2405
2520
  const styleTag = document.createElement("style");
2406
2521
  styleTag.innerHTML = `
2407
2522
  @keyframes qr-line {
@@ -2418,17 +2533,26 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2418
2533
  hint.style.fontSize = "17px";
2419
2534
  hint.style.opacity = "0.9";
2420
2535
  overlay.appendChild(hint);
2421
- const scanner = new import_html5_qrcode.Html5Qrcode("qr-reader");
2536
+ const canvas = document.createElement("canvas");
2537
+ const context = canvas.getContext("2d", { willReadFrequently: true });
2538
+ let stream = null;
2539
+ let rafId = null;
2540
+ let lastScanAt = 0;
2422
2541
  let closed = false;
2423
2542
  const finalize = async (result) => {
2424
2543
  if (closed) {
2425
2544
  return;
2426
2545
  }
2427
2546
  closed = true;
2428
- try {
2429
- await scanner.stop();
2430
- } catch {
2547
+ if (rafId !== null) {
2548
+ cancelAnimationFrame(rafId);
2549
+ rafId = null;
2431
2550
  }
2551
+ if (stream) {
2552
+ stream.getTracks().forEach((track) => track.stop());
2553
+ stream = null;
2554
+ }
2555
+ video.srcObject = null;
2432
2556
  overlay.remove();
2433
2557
  styleTag.remove();
2434
2558
  document.body.style.overflow = prevOverflow;
@@ -2447,19 +2571,50 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2447
2571
  };
2448
2572
  closeBtn.onclick = () => closeScanner(null);
2449
2573
  try {
2450
- await scanner.start(
2451
- { facingMode: "environment" },
2452
- {
2453
- fps: 10,
2454
- qrbox: { width: 250, height: 250 },
2455
- aspectRatio: 1
2456
- },
2457
- (decodedText) => {
2458
- closeScanner(decodedText);
2459
- },
2460
- () => {
2574
+ const constraints = [
2575
+ { video: { facingMode: { ideal: "environment" } }, audio: false },
2576
+ { video: { facingMode: "environment" }, audio: false },
2577
+ { video: true, audio: false }
2578
+ ];
2579
+ let lastError = null;
2580
+ for (const constraint of constraints) {
2581
+ try {
2582
+ stream = await navigator.mediaDevices.getUserMedia(constraint);
2583
+ break;
2584
+ } catch (error) {
2585
+ lastError = error;
2461
2586
  }
2462
- );
2587
+ }
2588
+ if (!stream) {
2589
+ throw lastError instanceof Error ? lastError : new Error("Unable to access camera");
2590
+ }
2591
+ video.srcObject = stream;
2592
+ await video.play();
2593
+ const scanFrame = (now) => {
2594
+ if (closed) {
2595
+ return;
2596
+ }
2597
+ if (now - lastScanAt >= 100 && context && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
2598
+ const width = video.videoWidth;
2599
+ const height = video.videoHeight;
2600
+ if (width > 0 && height > 0) {
2601
+ canvas.width = width;
2602
+ canvas.height = height;
2603
+ context.drawImage(video, 0, 0, width, height);
2604
+ const imageData = context.getImageData(0, 0, width, height);
2605
+ const result = (0, import_jsqr.default)(imageData.data, width, height, {
2606
+ inversionAttempts: "attemptBoth"
2607
+ });
2608
+ if (result?.data) {
2609
+ closeScanner(result.data);
2610
+ return;
2611
+ }
2612
+ lastScanAt = now;
2613
+ }
2614
+ }
2615
+ rafId = requestAnimationFrame(scanFrame);
2616
+ };
2617
+ rafId = requestAnimationFrame(scanFrame);
2463
2618
  } catch (error) {
2464
2619
  console.error("QR Start error", error);
2465
2620
  closeScanner(null);
@@ -2512,6 +2667,8 @@ ${url}` : url;
2512
2667
  };
2513
2668
 
2514
2669
  // src/adapters/index.ts
2670
+ var CONFIRMED_PLATFORM_STORAGE_KEY = "mini-app-adapter:confirmed-platform";
2671
+ var CONFIRMED_PLATFORM_TTL_MS = 30 * 60 * 1e3;
2515
2672
  function detectPlatform() {
2516
2673
  if (typeof window === "undefined") {
2517
2674
  return "web";
@@ -2523,30 +2680,76 @@ function detectPlatform() {
2523
2680
  })();
2524
2681
  const getParam = (name) => searchParams.get(name) ?? hashParams.get(name);
2525
2682
  const hasParam = (...names) => names.some((name) => getParam(name));
2683
+ const readConfirmedPlatform = () => {
2684
+ try {
2685
+ const raw = window.sessionStorage.getItem(CONFIRMED_PLATFORM_STORAGE_KEY);
2686
+ if (!raw) {
2687
+ return null;
2688
+ }
2689
+ const parsed = JSON.parse(raw);
2690
+ if (!parsed?.platform || typeof parsed.ts !== "number") {
2691
+ return null;
2692
+ }
2693
+ if (Date.now() - parsed.ts > CONFIRMED_PLATFORM_TTL_MS) {
2694
+ return null;
2695
+ }
2696
+ return parsed.platform;
2697
+ } catch {
2698
+ return null;
2699
+ }
2700
+ };
2701
+ const persistConfirmedPlatform = (platform) => {
2702
+ if (platform === "web") {
2703
+ return;
2704
+ }
2705
+ try {
2706
+ window.sessionStorage.setItem(
2707
+ CONFIRMED_PLATFORM_STORAGE_KEY,
2708
+ JSON.stringify({ platform, ts: Date.now() })
2709
+ );
2710
+ } catch {
2711
+ }
2712
+ };
2526
2713
  const shellPlatform = readShellPlatform();
2527
2714
  if (shellPlatform) {
2715
+ persistConfirmedPlatform(shellPlatform);
2528
2716
  return shellPlatform;
2529
2717
  }
2530
2718
  const userAgent = navigator.userAgent.toLowerCase();
2531
2719
  const hasNativeBridge = typeof window.NativeBridge?.postMessage === "function";
2532
2720
  if (hasNativeBridge) {
2533
2721
  if (userAgent.includes("android")) {
2722
+ persistConfirmedPlatform("shell_android");
2534
2723
  return "shell_android";
2535
2724
  }
2725
+ persistConfirmedPlatform("shell_ios");
2536
2726
  return "shell_ios";
2537
2727
  }
2538
- if (window.Telegram?.WebApp || hasParam("tgWebAppPlatform", "tgWebAppVersion", "tgWebAppData", "tgWebAppLanguage") || userAgent.includes("telegram")) {
2728
+ const telegramGlobals = window;
2729
+ const hasTelegramGlobal = Boolean(window.Telegram?.WebApp) || typeof telegramGlobals.TelegramWebviewProxy !== "undefined" || typeof telegramGlobals.TelegramGameProxy !== "undefined";
2730
+ const hasTelegramParams = hasParam("tgWebAppPlatform", "tgWebAppVersion", "tgWebAppData", "tgWebAppLanguage");
2731
+ if (hasTelegramGlobal || hasTelegramParams || userAgent.includes("telegram")) {
2732
+ persistConfirmedPlatform("telegram");
2539
2733
  return "telegram";
2540
2734
  }
2541
2735
  if (window.WebApp) {
2736
+ persistConfirmedPlatform("max");
2542
2737
  return "max";
2543
2738
  }
2544
2739
  if (window.MaxMiniApp) {
2740
+ persistConfirmedPlatform("max");
2545
2741
  return "max";
2546
2742
  }
2547
- if (hasParam("vk_app_id", "vk_platform")) {
2743
+ const hasVkParams = hasParam("vk_app_id", "vk_platform", "vk_user_id", "vk_language", "sign");
2744
+ const hasVkUserAgentSignal = userAgent.includes("vkclient") || userAgent.includes("vk-android") || userAgent.includes("vkontakte");
2745
+ if (hasVkParams || hasVkUserAgentSignal) {
2746
+ persistConfirmedPlatform("vk");
2548
2747
  return "vk";
2549
2748
  }
2749
+ const confirmedPlatform = readConfirmedPlatform();
2750
+ if (confirmedPlatform && confirmedPlatform !== "web") {
2751
+ return confirmedPlatform;
2752
+ }
2550
2753
  return "web";
2551
2754
  }
2552
2755
  function createAdapter(input) {
@@ -2747,14 +2950,18 @@ var cachedPlatform = null;
2747
2950
  function getPlatform() {
2748
2951
  const adapter = getActiveAdapter();
2749
2952
  if (adapter) {
2750
- cachedPlatform = adapter.platform;
2953
+ if (adapter.platform !== "web") {
2954
+ cachedPlatform = adapter.platform;
2955
+ }
2751
2956
  return adapter.platform;
2752
2957
  }
2753
- if (cachedPlatform) {
2958
+ if (cachedPlatform && cachedPlatform !== "web") {
2754
2959
  return cachedPlatform;
2755
2960
  }
2756
2961
  const detectedPlatform = detectPlatform();
2757
- cachedPlatform = detectedPlatform;
2962
+ if (detectedPlatform !== "web") {
2963
+ cachedPlatform = detectedPlatform;
2964
+ }
2758
2965
  return detectedPlatform;
2759
2966
  }
2760
2967