@wowlabtech/mini-app-adapter 0.2.2 → 0.2.4

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.d.cts CHANGED
@@ -66,7 +66,7 @@ interface MiniAppInitOptions {
66
66
  */
67
67
  debug?: boolean;
68
68
  /**
69
- * Dynamically loads Eruda devtools if supported.
69
+ * Enables Eruda devtools when host app already exposed `window.eruda`.
70
70
  */
71
71
  eruda?: boolean;
72
72
  /**
@@ -75,13 +75,45 @@ interface MiniAppInitOptions {
75
75
  mockForMacOS?: boolean;
76
76
  }
77
77
  interface MiniAppShareStoryOptions {
78
+ /**
79
+ * Shared story caption. Used by Telegram directly.
80
+ * For VK this value is converted into a simple text sticker when `vk.stickers` is not provided.
81
+ */
78
82
  text?: string;
79
- widgetLink?: {
83
+ /**
84
+ * Universal CTA link.
85
+ * Telegram maps it to `widgetLink`, VK maps it to `attachment` when custom VK attachment is not provided.
86
+ */
87
+ link?: {
80
88
  url: string;
81
89
  name?: string;
82
90
  };
91
+ /**
92
+ * Telegram-specific options.
93
+ */
94
+ telegram?: {
95
+ text?: string;
96
+ widgetLink?: {
97
+ url: string;
98
+ name?: string;
99
+ };
100
+ };
101
+ /**
102
+ * VK-specific options.
103
+ */
104
+ vk?: {
105
+ backgroundType?: 'image' | 'video';
106
+ locked?: boolean;
107
+ attachment?: {
108
+ type: string;
109
+ text?: string;
110
+ url?: string;
111
+ [key: string]: unknown;
112
+ };
113
+ stickers?: Array<Record<string, unknown>>;
114
+ };
83
115
  }
84
- type MiniAppCapability = 'haptics' | 'popup' | 'qrScanner' | 'closeApp' | 'backButton' | 'backButtonVisibility' | 'bindCssVariables' | 'requestPhone' | 'notifications';
116
+ type MiniAppCapability = 'haptics' | 'popup' | 'qrScanner' | 'closeApp' | 'backButton' | 'backButtonVisibility' | 'bindCssVariables' | 'requestPhone' | 'notifications' | 'openInternalLink' | 'requestFullscreen' | 'verticalSwipes' | 'viewVisibility' | 'shareUrl' | 'shareStory' | 'copyTextToClipboard' | 'downloadFile' | 'addToHomeScreen' | 'checkHomeScreenStatus' | 'denyNotifications';
85
117
  interface MiniAppPopupButton {
86
118
  id: string;
87
119
  text?: string;
@@ -307,7 +339,7 @@ declare abstract class BaseMiniAppAdapter implements MiniAppAdapter {
307
339
  onPushToken(callback: (token: string) => void): () => void;
308
340
  onDeepLink(callback: (path: string) => void): () => void;
309
341
  openExternalLink(url: string): Promise<void>;
310
- openInternalLink(_url: string): Promise<void>;
342
+ openInternalLink(url: string): Promise<void>;
311
343
  closeApp(): Promise<void>;
312
344
  setBackButtonVisibility(_visible: boolean): void;
313
345
  onAppearanceChange(callback: (appearance: 'dark' | 'light' | undefined) => void): () => void;
@@ -361,6 +393,7 @@ declare class MaxMiniAppAdapter extends BaseMiniAppAdapter {
361
393
  onBackButton(callback: () => void): () => void;
362
394
  setBackButtonVisibility(visible: boolean): void;
363
395
  openExternalLink(url: string): Promise<void>;
396
+ openInternalLink(url: string): Promise<void>;
364
397
  closeApp(): Promise<void>;
365
398
  vibrateImpact(style: ImpactHapticFeedbackStyle): void;
366
399
  vibrateNotification(type: NotificationHapticFeedbackType): void;
@@ -505,6 +538,7 @@ declare class VKMiniAppAdapter extends BaseMiniAppAdapter {
505
538
  declare class WebMiniAppAdapter extends BaseMiniAppAdapter {
506
539
  private deferredPrompt;
507
540
  constructor();
541
+ supports(capability: MiniAppCapability): boolean | Promise<boolean>;
508
542
  downloadFile(url: string, filename: string): Promise<void>;
509
543
  scanQRCode(_options?: MiniAppQrScanOptions): Promise<string | null>;
510
544
  shareUrl(url: string, text: string): void;
package/dist/index.d.ts CHANGED
@@ -66,7 +66,7 @@ interface MiniAppInitOptions {
66
66
  */
67
67
  debug?: boolean;
68
68
  /**
69
- * Dynamically loads Eruda devtools if supported.
69
+ * Enables Eruda devtools when host app already exposed `window.eruda`.
70
70
  */
71
71
  eruda?: boolean;
72
72
  /**
@@ -75,13 +75,45 @@ interface MiniAppInitOptions {
75
75
  mockForMacOS?: boolean;
76
76
  }
77
77
  interface MiniAppShareStoryOptions {
78
+ /**
79
+ * Shared story caption. Used by Telegram directly.
80
+ * For VK this value is converted into a simple text sticker when `vk.stickers` is not provided.
81
+ */
78
82
  text?: string;
79
- widgetLink?: {
83
+ /**
84
+ * Universal CTA link.
85
+ * Telegram maps it to `widgetLink`, VK maps it to `attachment` when custom VK attachment is not provided.
86
+ */
87
+ link?: {
80
88
  url: string;
81
89
  name?: string;
82
90
  };
91
+ /**
92
+ * Telegram-specific options.
93
+ */
94
+ telegram?: {
95
+ text?: string;
96
+ widgetLink?: {
97
+ url: string;
98
+ name?: string;
99
+ };
100
+ };
101
+ /**
102
+ * VK-specific options.
103
+ */
104
+ vk?: {
105
+ backgroundType?: 'image' | 'video';
106
+ locked?: boolean;
107
+ attachment?: {
108
+ type: string;
109
+ text?: string;
110
+ url?: string;
111
+ [key: string]: unknown;
112
+ };
113
+ stickers?: Array<Record<string, unknown>>;
114
+ };
83
115
  }
84
- type MiniAppCapability = 'haptics' | 'popup' | 'qrScanner' | 'closeApp' | 'backButton' | 'backButtonVisibility' | 'bindCssVariables' | 'requestPhone' | 'notifications';
116
+ type MiniAppCapability = 'haptics' | 'popup' | 'qrScanner' | 'closeApp' | 'backButton' | 'backButtonVisibility' | 'bindCssVariables' | 'requestPhone' | 'notifications' | 'openInternalLink' | 'requestFullscreen' | 'verticalSwipes' | 'viewVisibility' | 'shareUrl' | 'shareStory' | 'copyTextToClipboard' | 'downloadFile' | 'addToHomeScreen' | 'checkHomeScreenStatus' | 'denyNotifications';
85
117
  interface MiniAppPopupButton {
86
118
  id: string;
87
119
  text?: string;
@@ -307,7 +339,7 @@ declare abstract class BaseMiniAppAdapter implements MiniAppAdapter {
307
339
  onPushToken(callback: (token: string) => void): () => void;
308
340
  onDeepLink(callback: (path: string) => void): () => void;
309
341
  openExternalLink(url: string): Promise<void>;
310
- openInternalLink(_url: string): Promise<void>;
342
+ openInternalLink(url: string): Promise<void>;
311
343
  closeApp(): Promise<void>;
312
344
  setBackButtonVisibility(_visible: boolean): void;
313
345
  onAppearanceChange(callback: (appearance: 'dark' | 'light' | undefined) => void): () => void;
@@ -361,6 +393,7 @@ declare class MaxMiniAppAdapter extends BaseMiniAppAdapter {
361
393
  onBackButton(callback: () => void): () => void;
362
394
  setBackButtonVisibility(visible: boolean): void;
363
395
  openExternalLink(url: string): Promise<void>;
396
+ openInternalLink(url: string): Promise<void>;
364
397
  closeApp(): Promise<void>;
365
398
  vibrateImpact(style: ImpactHapticFeedbackStyle): void;
366
399
  vibrateNotification(type: NotificationHapticFeedbackType): void;
@@ -505,6 +538,7 @@ declare class VKMiniAppAdapter extends BaseMiniAppAdapter {
505
538
  declare class WebMiniAppAdapter extends BaseMiniAppAdapter {
506
539
  private deferredPrompt;
507
540
  constructor();
541
+ supports(capability: MiniAppCapability): boolean | Promise<boolean>;
508
542
  downloadFile(url: string, filename: string): Promise<void>;
509
543
  scanQRCode(_options?: MiniAppQrScanOptions): Promise<string | null>;
510
544
  shareUrl(url: string, text: string): void;
package/dist/index.js CHANGED
@@ -421,7 +421,7 @@ async function startHtml5Qrcode() {
421
421
  if (typeof document === "undefined") {
422
422
  throw new Error("QR scanning requires a browser environment.");
423
423
  }
424
- const [{ Html5Qrcode: Html5Qrcode2 }] = await Promise.all([import("html5-qrcode")]);
424
+ const [{ Html5Qrcode }] = await Promise.all([import("html5-qrcode")]);
425
425
  return new Promise((resolve, reject) => {
426
426
  const elementId = `native-shell-qr-${Date.now()}`;
427
427
  const overlay = document.createElement("div");
@@ -458,7 +458,7 @@ async function startHtml5Qrcode() {
458
458
  const previousOverflow = document.body.style.overflow;
459
459
  document.body.style.overflow = "hidden";
460
460
  let disposed = false;
461
- const scanner = new Html5Qrcode2(elementId);
461
+ const scanner = new Html5Qrcode(elementId);
462
462
  const cleanup = async (result, error) => {
463
463
  if (disposed) {
464
464
  return;
@@ -580,7 +580,8 @@ var BaseMiniAppAdapter = class {
580
580
  async openExternalLink(url) {
581
581
  window.open(url, "_blank", "noopener,noreferrer");
582
582
  }
583
- async openInternalLink(_url) {
583
+ async openInternalLink(url) {
584
+ window.open(url, "_self", "noopener,noreferrer");
584
585
  }
585
586
  async closeApp() {
586
587
  if (window.history.length > 1) {
@@ -806,6 +807,10 @@ var MaxMiniAppAdapter = class extends BaseMiniAppAdapter {
806
807
  return Boolean(bridge2?.BackButton?.onClick);
807
808
  case "backButtonVisibility":
808
809
  return Boolean(bridge2?.BackButton?.show && bridge2.BackButton.hide);
810
+ case "openInternalLink":
811
+ return typeof bridge2?.openMaxLink === "function";
812
+ case "downloadFile":
813
+ return typeof bridge2?.downloadFile === "function";
809
814
  case "requestPhone":
810
815
  if (!bridge2) {
811
816
  return false;
@@ -860,6 +865,14 @@ var MaxMiniAppAdapter = class extends BaseMiniAppAdapter {
860
865
  }
861
866
  await super.openExternalLink(url);
862
867
  }
868
+ async openInternalLink(url) {
869
+ const bridge2 = getMaxBridge();
870
+ if (bridge2?.openMaxLink) {
871
+ bridge2.openMaxLink(url);
872
+ return;
873
+ }
874
+ await super.openInternalLink(url);
875
+ }
863
876
  async closeApp() {
864
877
  const bridge2 = getMaxBridge();
865
878
  if (bridge2?.close) {
@@ -1287,6 +1300,30 @@ var TelegramMiniAppAdapter = class extends BaseMiniAppAdapter {
1287
1300
  return backButton.hide.isSupported();
1288
1301
  case "bindCssVariables":
1289
1302
  return true;
1303
+ case "openInternalLink":
1304
+ return true;
1305
+ case "requestFullscreen":
1306
+ return Boolean(
1307
+ typeof rawViewport.requestFullscreen === "function" || viewport.requestFullscreen?.isAvailable?.()
1308
+ );
1309
+ case "verticalSwipes":
1310
+ return Boolean(
1311
+ swipeBehavior.enableVertical.isAvailable() || swipeBehavior.disableVertical.isAvailable()
1312
+ );
1313
+ case "viewVisibility":
1314
+ return true;
1315
+ case "shareUrl":
1316
+ return typeof shareURLSdk === "function";
1317
+ case "shareStory":
1318
+ return typeof shareStorySdk === "function";
1319
+ case "copyTextToClipboard":
1320
+ return typeof copyTextToClipboardSdk === "function";
1321
+ case "downloadFile":
1322
+ return typeof downloadFileSdk === "function";
1323
+ case "addToHomeScreen":
1324
+ return typeof addToHomeScreenSdk?.isAvailable === "function" ? addToHomeScreenSdk.isAvailable() : typeof addToHomeScreenSdk === "function";
1325
+ case "checkHomeScreenStatus":
1326
+ return typeof checkHomeScreenStatusSdk === "function";
1290
1327
  case "requestPhone": {
1291
1328
  return Boolean(isFeatureAvailable(requestPhoneAccess) || isFeatureAvailable(requestContact));
1292
1329
  }
@@ -1532,7 +1569,15 @@ var TelegramMiniAppAdapter = class extends BaseMiniAppAdapter {
1532
1569
  await super.downloadFile(url, filename);
1533
1570
  }
1534
1571
  async shareStory(mediaUrl, options) {
1535
- shareStorySdk(mediaUrl, options);
1572
+ const text = options?.telegram?.text ?? options?.text;
1573
+ const widgetLink = options?.telegram?.widgetLink ?? (options?.link ? {
1574
+ url: options.link.url,
1575
+ ...options.link.name ? { name: options.link.name } : {}
1576
+ } : void 0);
1577
+ shareStorySdk(mediaUrl, {
1578
+ ...text ? { text } : {},
1579
+ ...widgetLink ? { widgetLink } : {}
1580
+ });
1536
1581
  }
1537
1582
  async addToHomeScreen() {
1538
1583
  const isAvailable = typeof addToHomeScreenSdk?.isAvailable === "function" ? addToHomeScreenSdk.isAvailable() : true;
@@ -1889,17 +1934,41 @@ var VKMiniAppAdapter = class extends BaseMiniAppAdapter {
1889
1934
  };
1890
1935
  }
1891
1936
  async supports(capability) {
1892
- if (capability === "requestPhone") {
1893
- const [supportsPhoneNumber, supportsPersonalCard] = await Promise.all([
1894
- this.supportsBridgeMethod("VKWebAppGetPhoneNumber"),
1895
- this.supportsBridgeMethod("VKWebAppGetPersonalCard")
1896
- ]);
1897
- return supportsPhoneNumber || supportsPersonalCard;
1898
- }
1899
- if (capability === "notifications") {
1900
- return this.supportsBridgeMethod("VKWebAppAllowNotifications");
1937
+ switch (capability) {
1938
+ case "haptics": {
1939
+ const [impact, notification, selection] = await Promise.all([
1940
+ this.supportsBridgeMethod("VKWebAppTapticImpactOccurred"),
1941
+ this.supportsBridgeMethod("VKWebAppTapticNotificationOccurred"),
1942
+ this.supportsBridgeMethod("VKWebAppTapticSelectionChanged")
1943
+ ]);
1944
+ return impact || notification || selection;
1945
+ }
1946
+ case "qrScanner":
1947
+ return this.supportsBridgeMethod("VKWebAppOpenCodeReader");
1948
+ case "requestPhone": {
1949
+ const [supportsPhoneNumber, supportsPersonalCard] = await Promise.all([
1950
+ this.supportsBridgeMethod("VKWebAppGetPhoneNumber"),
1951
+ this.supportsBridgeMethod("VKWebAppGetPersonalCard")
1952
+ ]);
1953
+ return supportsPhoneNumber || supportsPersonalCard;
1954
+ }
1955
+ case "notifications":
1956
+ return this.supportsBridgeMethod("VKWebAppAllowNotifications");
1957
+ case "shareUrl":
1958
+ return this.supportsBridgeMethod("VKWebAppShare");
1959
+ case "shareStory":
1960
+ return this.supportsBridgeMethod("VKWebAppShowStoryBox");
1961
+ case "downloadFile":
1962
+ return this.supportsBridgeMethod("VKWebAppDownloadFile");
1963
+ case "addToHomeScreen":
1964
+ return this.supportsBridgeMethod("VKWebAppAddToHomeScreen");
1965
+ case "denyNotifications":
1966
+ return this.supportsBridgeMethod("VKWebAppDenyNotifications");
1967
+ case "viewVisibility":
1968
+ return true;
1969
+ default:
1970
+ return await super.supports(capability);
1901
1971
  }
1902
- return await super.supports(capability);
1903
1972
  }
1904
1973
  async requestPhone() {
1905
1974
  const [supportsPhoneNumber, supportsPersonalCard] = await Promise.all([
@@ -2001,9 +2070,34 @@ var VKMiniAppAdapter = class extends BaseMiniAppAdapter {
2001
2070
  };
2002
2071
  }
2003
2072
  async shareStory(mediaUrl, _options) {
2073
+ const options = _options;
2074
+ const vkOptions = options?.vk;
2075
+ const fallbackAttachment = options?.link ? {
2076
+ type: "url",
2077
+ text: "open",
2078
+ url: options.link.url
2079
+ } : void 0;
2080
+ const fallbackStickers = options?.text ? [{
2081
+ sticker_type: "native",
2082
+ sticker: {
2083
+ action_type: "text",
2084
+ action: {
2085
+ text: options.text,
2086
+ style: "classic",
2087
+ background_style: "none"
2088
+ },
2089
+ transform: {
2090
+ gravity: "center_bottom",
2091
+ translation_y: -0.2
2092
+ }
2093
+ }
2094
+ }] : void 0;
2004
2095
  const bridgeOptions = {
2005
- background_type: "image",
2006
- url: mediaUrl
2096
+ background_type: vkOptions?.backgroundType ?? "image",
2097
+ url: mediaUrl,
2098
+ locked: vkOptions?.locked ?? true,
2099
+ ...vkOptions?.attachment ?? fallbackAttachment ? { attachment: vkOptions?.attachment ?? fallbackAttachment } : {},
2100
+ ...vkOptions?.stickers ?? fallbackStickers ? { stickers: vkOptions?.stickers ?? fallbackStickers } : {}
2007
2101
  };
2008
2102
  await bridge.send("VKWebAppShowStoryBox", bridgeOptions);
2009
2103
  }
@@ -2376,7 +2470,7 @@ var VKMiniAppAdapter = class extends BaseMiniAppAdapter {
2376
2470
  };
2377
2471
 
2378
2472
  // src/adapters/webAdapter.ts
2379
- import { Html5Qrcode } from "html5-qrcode";
2473
+ import jsQR from "jsqr";
2380
2474
  var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2381
2475
  constructor() {
2382
2476
  super("web", {
@@ -2392,6 +2486,22 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2392
2486
  });
2393
2487
  }
2394
2488
  }
2489
+ supports(capability) {
2490
+ switch (capability) {
2491
+ case "copyTextToClipboard":
2492
+ return typeof navigator !== "undefined" && Boolean(navigator.clipboard?.writeText);
2493
+ case "downloadFile":
2494
+ return typeof document !== "undefined";
2495
+ case "shareUrl":
2496
+ return typeof navigator !== "undefined" && (Boolean(navigator.share) || Boolean(navigator.clipboard?.writeText));
2497
+ case "addToHomeScreen":
2498
+ return /android/i.test(navigator.userAgent) && Boolean(this.deferredPrompt);
2499
+ case "checkHomeScreenStatus":
2500
+ return true;
2501
+ default:
2502
+ return super.supports(capability);
2503
+ }
2504
+ }
2395
2505
  async downloadFile(url, filename) {
2396
2506
  try {
2397
2507
  await triggerFileDownload(url, filename, { preferBlob: true });
@@ -2446,14 +2556,31 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2446
2556
  closeBtn.style.cursor = "pointer";
2447
2557
  closeBtn.style.zIndex = "9999999999";
2448
2558
  overlay.appendChild(closeBtn);
2559
+ const scanSize = Math.min(Math.floor(Math.min(window.innerWidth, window.innerHeight) * 0.72), 320);
2560
+ const scanBox = document.createElement("div");
2561
+ scanBox.style.width = `${scanSize}px`;
2562
+ scanBox.style.height = `${scanSize}px`;
2563
+ scanBox.style.position = "relative";
2564
+ scanBox.style.flex = "0 0 auto";
2565
+ scanBox.style.borderRadius = "18px";
2566
+ scanBox.style.overflow = "hidden";
2567
+ overlay.appendChild(scanBox);
2449
2568
  const scanArea = document.createElement("div");
2450
- scanArea.id = "qr-reader";
2451
- scanArea.style.width = "300px";
2452
- scanArea.style.height = "300px";
2453
- scanArea.style.borderRadius = "18px";
2454
- scanArea.style.overflow = "hidden";
2455
- scanArea.style.position = "relative";
2456
- overlay.appendChild(scanArea);
2569
+ scanArea.style.position = "absolute";
2570
+ scanArea.style.inset = "0";
2571
+ scanArea.style.zIndex = "1";
2572
+ scanArea.style.background = "#000";
2573
+ scanBox.appendChild(scanArea);
2574
+ const video = document.createElement("video");
2575
+ video.setAttribute("playsinline", "true");
2576
+ video.autoplay = true;
2577
+ video.muted = true;
2578
+ video.style.width = "100%";
2579
+ video.style.height = "100%";
2580
+ video.style.objectFit = "cover";
2581
+ video.style.position = "absolute";
2582
+ video.style.inset = "0";
2583
+ scanArea.appendChild(video);
2457
2584
  const frame = document.createElement("div");
2458
2585
  frame.style.position = "absolute";
2459
2586
  frame.style.top = "0";
@@ -2463,8 +2590,8 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2463
2590
  frame.style.border = "3px solid rgba(255,255,255,0.9)";
2464
2591
  frame.style.borderRadius = "18px";
2465
2592
  frame.style.pointerEvents = "none";
2466
- frame.style.zIndex = "10";
2467
- scanArea.appendChild(frame);
2593
+ frame.style.zIndex = "3";
2594
+ scanBox.appendChild(frame);
2468
2595
  const line = document.createElement("div");
2469
2596
  line.style.position = "absolute";
2470
2597
  line.style.left = "0";
@@ -2473,8 +2600,8 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2473
2600
  line.style.background = "rgba(255,255,255,0.85)";
2474
2601
  line.style.borderRadius = "2px";
2475
2602
  line.style.animation = "qr-line 2s infinite";
2476
- line.style.zIndex = "11";
2477
- scanArea.appendChild(line);
2603
+ line.style.zIndex = "4";
2604
+ scanBox.appendChild(line);
2478
2605
  const styleTag = document.createElement("style");
2479
2606
  styleTag.innerHTML = `
2480
2607
  @keyframes qr-line {
@@ -2491,17 +2618,26 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2491
2618
  hint.style.fontSize = "17px";
2492
2619
  hint.style.opacity = "0.9";
2493
2620
  overlay.appendChild(hint);
2494
- const scanner = new Html5Qrcode("qr-reader");
2621
+ const canvas = document.createElement("canvas");
2622
+ const context = canvas.getContext("2d", { willReadFrequently: true });
2623
+ let stream = null;
2624
+ let rafId = null;
2625
+ let lastScanAt = 0;
2495
2626
  let closed = false;
2496
2627
  const finalize = async (result) => {
2497
2628
  if (closed) {
2498
2629
  return;
2499
2630
  }
2500
2631
  closed = true;
2501
- try {
2502
- await scanner.stop();
2503
- } catch {
2632
+ if (rafId !== null) {
2633
+ cancelAnimationFrame(rafId);
2634
+ rafId = null;
2635
+ }
2636
+ if (stream) {
2637
+ stream.getTracks().forEach((track) => track.stop());
2638
+ stream = null;
2504
2639
  }
2640
+ video.srcObject = null;
2505
2641
  overlay.remove();
2506
2642
  styleTag.remove();
2507
2643
  document.body.style.overflow = prevOverflow;
@@ -2520,19 +2656,50 @@ var WebMiniAppAdapter = class extends BaseMiniAppAdapter {
2520
2656
  };
2521
2657
  closeBtn.onclick = () => closeScanner(null);
2522
2658
  try {
2523
- await scanner.start(
2524
- { facingMode: "environment" },
2525
- {
2526
- fps: 10,
2527
- qrbox: { width: 250, height: 250 },
2528
- aspectRatio: 1
2529
- },
2530
- (decodedText) => {
2531
- closeScanner(decodedText);
2532
- },
2533
- () => {
2659
+ const constraints = [
2660
+ { video: { facingMode: { ideal: "environment" } }, audio: false },
2661
+ { video: { facingMode: "environment" }, audio: false },
2662
+ { video: true, audio: false }
2663
+ ];
2664
+ let lastError = null;
2665
+ for (const constraint of constraints) {
2666
+ try {
2667
+ stream = await navigator.mediaDevices.getUserMedia(constraint);
2668
+ break;
2669
+ } catch (error) {
2670
+ lastError = error;
2534
2671
  }
2535
- );
2672
+ }
2673
+ if (!stream) {
2674
+ throw lastError instanceof Error ? lastError : new Error("Unable to access camera");
2675
+ }
2676
+ video.srcObject = stream;
2677
+ await video.play();
2678
+ const scanFrame = (now) => {
2679
+ if (closed) {
2680
+ return;
2681
+ }
2682
+ if (now - lastScanAt >= 100 && context && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
2683
+ const width = video.videoWidth;
2684
+ const height = video.videoHeight;
2685
+ if (width > 0 && height > 0) {
2686
+ canvas.width = width;
2687
+ canvas.height = height;
2688
+ context.drawImage(video, 0, 0, width, height);
2689
+ const imageData = context.getImageData(0, 0, width, height);
2690
+ const result = jsQR(imageData.data, width, height, {
2691
+ inversionAttempts: "attemptBoth"
2692
+ });
2693
+ if (result?.data) {
2694
+ closeScanner(result.data);
2695
+ return;
2696
+ }
2697
+ lastScanAt = now;
2698
+ }
2699
+ }
2700
+ rafId = requestAnimationFrame(scanFrame);
2701
+ };
2702
+ rafId = requestAnimationFrame(scanFrame);
2536
2703
  } catch (error) {
2537
2704
  console.error("QR Start error", error);
2538
2705
  closeScanner(null);
@@ -2585,6 +2752,8 @@ ${url}` : url;
2585
2752
  };
2586
2753
 
2587
2754
  // src/adapters/index.ts
2755
+ var CONFIRMED_PLATFORM_STORAGE_KEY = "mini-app-adapter:confirmed-platform";
2756
+ var CONFIRMED_PLATFORM_TTL_MS = 30 * 60 * 1e3;
2588
2757
  function detectPlatform() {
2589
2758
  if (typeof window === "undefined") {
2590
2759
  return "web";
@@ -2596,30 +2765,76 @@ function detectPlatform() {
2596
2765
  })();
2597
2766
  const getParam = (name) => searchParams.get(name) ?? hashParams.get(name);
2598
2767
  const hasParam = (...names) => names.some((name) => getParam(name));
2768
+ const readConfirmedPlatform = () => {
2769
+ try {
2770
+ const raw = window.sessionStorage.getItem(CONFIRMED_PLATFORM_STORAGE_KEY);
2771
+ if (!raw) {
2772
+ return null;
2773
+ }
2774
+ const parsed = JSON.parse(raw);
2775
+ if (!parsed?.platform || typeof parsed.ts !== "number") {
2776
+ return null;
2777
+ }
2778
+ if (Date.now() - parsed.ts > CONFIRMED_PLATFORM_TTL_MS) {
2779
+ return null;
2780
+ }
2781
+ return parsed.platform;
2782
+ } catch {
2783
+ return null;
2784
+ }
2785
+ };
2786
+ const persistConfirmedPlatform = (platform) => {
2787
+ if (platform === "web") {
2788
+ return;
2789
+ }
2790
+ try {
2791
+ window.sessionStorage.setItem(
2792
+ CONFIRMED_PLATFORM_STORAGE_KEY,
2793
+ JSON.stringify({ platform, ts: Date.now() })
2794
+ );
2795
+ } catch {
2796
+ }
2797
+ };
2599
2798
  const shellPlatform = readShellPlatform();
2600
2799
  if (shellPlatform) {
2800
+ persistConfirmedPlatform(shellPlatform);
2601
2801
  return shellPlatform;
2602
2802
  }
2603
2803
  const userAgent = navigator.userAgent.toLowerCase();
2604
2804
  const hasNativeBridge = typeof window.NativeBridge?.postMessage === "function";
2605
2805
  if (hasNativeBridge) {
2606
2806
  if (userAgent.includes("android")) {
2807
+ persistConfirmedPlatform("shell_android");
2607
2808
  return "shell_android";
2608
2809
  }
2810
+ persistConfirmedPlatform("shell_ios");
2609
2811
  return "shell_ios";
2610
2812
  }
2611
- if (window.Telegram?.WebApp || hasParam("tgWebAppPlatform", "tgWebAppVersion", "tgWebAppData", "tgWebAppLanguage") || userAgent.includes("telegram")) {
2813
+ const telegramGlobals = window;
2814
+ const hasTelegramGlobal = Boolean(window.Telegram?.WebApp) || typeof telegramGlobals.TelegramWebviewProxy !== "undefined" || typeof telegramGlobals.TelegramGameProxy !== "undefined";
2815
+ const hasTelegramParams = hasParam("tgWebAppPlatform", "tgWebAppVersion", "tgWebAppData", "tgWebAppLanguage");
2816
+ if (hasTelegramGlobal || hasTelegramParams || userAgent.includes("telegram")) {
2817
+ persistConfirmedPlatform("telegram");
2612
2818
  return "telegram";
2613
2819
  }
2614
2820
  if (window.WebApp) {
2821
+ persistConfirmedPlatform("max");
2615
2822
  return "max";
2616
2823
  }
2617
2824
  if (window.MaxMiniApp) {
2825
+ persistConfirmedPlatform("max");
2618
2826
  return "max";
2619
2827
  }
2620
- if (hasParam("vk_app_id", "vk_platform")) {
2828
+ const hasVkParams = hasParam("vk_app_id", "vk_platform", "vk_user_id", "vk_language", "sign");
2829
+ const hasVkUserAgentSignal = userAgent.includes("vkclient") || userAgent.includes("vk-android") || userAgent.includes("vkontakte");
2830
+ if (hasVkParams || hasVkUserAgentSignal) {
2831
+ persistConfirmedPlatform("vk");
2621
2832
  return "vk";
2622
2833
  }
2834
+ const confirmedPlatform = readConfirmedPlatform();
2835
+ if (confirmedPlatform && confirmedPlatform !== "web") {
2836
+ return confirmedPlatform;
2837
+ }
2623
2838
  return "web";
2624
2839
  }
2625
2840
  function createAdapter(input) {
@@ -2820,14 +3035,18 @@ var cachedPlatform = null;
2820
3035
  function getPlatform() {
2821
3036
  const adapter = getActiveAdapter();
2822
3037
  if (adapter) {
2823
- cachedPlatform = adapter.platform;
3038
+ if (adapter.platform !== "web") {
3039
+ cachedPlatform = adapter.platform;
3040
+ }
2824
3041
  return adapter.platform;
2825
3042
  }
2826
- if (cachedPlatform) {
3043
+ if (cachedPlatform && cachedPlatform !== "web") {
2827
3044
  return cachedPlatform;
2828
3045
  }
2829
3046
  const detectedPlatform = detectPlatform();
2830
- cachedPlatform = detectedPlatform;
3047
+ if (detectedPlatform !== "web") {
3048
+ cachedPlatform = detectedPlatform;
3049
+ }
2831
3050
  return detectedPlatform;
2832
3051
  }
2833
3052