@vkma.dev/bridge 0.2.0 → 0.2.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/README.md CHANGED
@@ -1,13 +1,17 @@
1
1
  # @vkma.dev/bridge
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@vkma.dev/bridge.svg)](https://www.npmjs.com/package/@vkma.dev/bridge)
4
+ [![minzip](https://img.shields.io/bundlephobia/minzip/@vkma.dev/bridge.svg)](https://bundlephobia.com/package/@vkma.dev/bridge)
5
+
3
6
  Independent VK Bridge for VK Mini Apps. Drop-in classic API (`send` / `subscribe` / `supportsAsync` / …) plus a built-in observability tap, per-send timeout/abort and a configurable `request_id` policy.
4
7
 
5
- Types come from [`@vkma.dev/bridge-schema`](../bridge-schema) — the catalogue of every platform event as data, with an open interface for your own events.
8
+ Types come from [`@vkma.dev/bridge-schema`](https://github.com/pajecawav/vkma/tree/master/packages/bridge-schema#readme) — the catalogue of every platform event as data, with an open interface for your own events.
6
9
 
7
10
  ## Install
8
11
 
9
12
  ```sh
10
- pnpm add @vkma.dev/bridge
13
+ npm install @vkma.dev/bridge
14
+ # or: pnpm add / yarn add / bun add
11
15
  ```
12
16
 
13
17
  ## Use
@@ -28,7 +32,8 @@ Or build isolated instances (own tap, own pairing state):
28
32
  ```ts
29
33
  import { createVKBridge } from "@vkma.dev/bridge";
30
34
 
31
- const bridge = createVKBridge(version, {
35
+ const bridge = createVKBridge("1.0.0", {
36
+ // the version string is reported to the web host
32
37
  requestId: { generator: sequence => `req-${sequence}` },
33
38
  });
34
39
  ```
@@ -47,10 +52,10 @@ await bridge.send("VKWebAppGetUserInfo", undefined, { timeout: 5000 });
47
52
  await bridge.send("VKWebAppShowWallPostBox", props, { signal: controller.signal });
48
53
 
49
54
  // request_id policy (default: auto-inject __vkma_<n>, that's what pairing uses)
50
- createVKBridge(version, { requestId: { inject: false } });
55
+ createVKBridge("1.0.0", { requestId: { inject: false } });
51
56
  ```
52
57
 
53
- The page-wide default instance also publishes the devtools handle on `globalThis.__vkmaDevtoolsBridge` (`{ tap, send }`) — that's the contract the [VKMA devtools](../devtools) collector observes. Importing the package is enough; no opt-in.
58
+ The page-wide default instance also publishes the devtools handle on `globalThis.__vkmaDevtoolsBridge` (`{ tap, send }`) — that's the contract the [VKMA devtools](https://github.com/pajecawav/vkma/tree/master/packages/devtools#readme) collector observes. Importing the package is enough; no opt-in.
54
59
 
55
60
  ## Types & custom events
56
61
 
@@ -73,11 +78,11 @@ declare module "@vkma.dev/bridge-schema" {
73
78
  }
74
79
  ```
75
80
 
76
- See [`@vkma.dev/bridge-schema`](../bridge-schema) for the rules (augment the schema package root; extend only, no overrides).
81
+ The rules live in [`@vkma.dev/bridge-schema`](https://github.com/pajecawav/vkma/tree/master/packages/bridge-schema#readme) — augment that package's root, extend only.
77
82
 
78
83
  ## Transports
79
84
 
80
- Web (iframe `postMessage` with the `vk-connect` envelope), Android WebView, iOS WebView and React Native WebView — with receive-side `message` / `VKWebAppEvent` listeners to match. Web hosts are exercised by the unit suite and the playground e2e; the mobile WebView transports follow the platform contracts and deserve a manual pass on a real device before a release (see `AGENTS.md` at the repo root).
85
+ Web (iframe `postMessage` with the `vk-connect` envelope), Android WebView, iOS WebView and React Native WebView — with receive-side `message` / `VKWebAppEvent` listeners to match. The web transport is covered by the automated suite; the mobile WebView transports follow the platform host contracts.
81
86
 
82
87
  ## License
83
88
 
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.js","names":[],"sources":["../src/bridge.ts"],"sourcesContent":["/**\n * The bridge core: transport dispatch, subscription fan-out, promise pairing\n * and the always-on tap.\n *\n * Transports (checked lazily on every call, unlike the load-time caches of\n * the reference implementation — same in practice, testable in unit tests):\n *\n * - Android WebView: `window.AndroidBridge[method](JSON.stringify(props))`\n * - iOS WebView: `window.webkit.messageHandlers[method].postMessage(props)`\n * - React Native WebView: `window.ReactNativeWebView.postMessage(JSON)`\n * - Web (iframe on vk.ru / m.vk.ru): `parent.postMessage` with the\n * `type: \"vk-connect\"` envelope; receive side listens to `message`\n * - WebViews receive via `CustomEvent(\"VKWebAppEvent\")`\n *\n * The wire format is the platform interface — the host clients on the other\n * side of these bridges define it, which is why the envelopes above are\n * reproduced exactly.\n */\n\nimport { createTap, type BridgeTap, type BridgeTapEvent } from \"./tap.ts\";\nimport type { SendOptions, VKBridgeSend, VKBridgeSubscribeHandler, VkmaBridge } from \"./types.ts\";\n\n/** Methods supported on the desktop web hosts. */\nconst DESKTOP_METHODS = new Set([\n\t\"VKWebAppInit\",\n\t\"VKWebAppGetCommunityAuthToken\",\n\t\"VKWebAppAddToCommunity\",\n\t\"VKWebAppAddToHomeScreenInfo\",\n\t\"VKWebAppClose\",\n\t\"VKWebAppCopyText\",\n\t\"VKWebAppCreateHash\",\n\t\"VKWebAppGetUserInfo\",\n\t\"VKWebAppSetLocation\",\n\t\"VKWebAppSendToClient\",\n\t\"VKWebAppGetClientVersion\",\n\t\"VKWebAppGetPhoneNumber\",\n\t\"VKWebAppGetEmail\",\n\t\"VKWebAppGetGroupInfo\",\n\t\"VKWebAppGetGeodata\",\n\t\"VKWebAppGetCommunityToken\",\n\t\"VKWebAppGetConfig\",\n\t\"VKWebAppGetLaunchParams\",\n\t\"VKWebAppSetTitle\",\n\t\"VKWebAppGetAuthToken\",\n\t\"VKWebAppCallAPIMethod\",\n\t\"VKWebAppJoinGroup\",\n\t\"VKWebAppLeaveGroup\",\n\t\"VKWebAppAllowMessagesFromGroup\",\n\t\"VKWebAppDenyNotifications\",\n\t\"VKWebAppAllowNotifications\",\n\t\"VKWebAppOpenPayForm\",\n\t\"VKWebAppOpenApp\",\n\t\"VKWebAppShare\",\n\t\"VKWebAppShowWallPostBox\",\n\t\"VKWebAppScroll\",\n\t\"VKWebAppShowOrderBox\",\n\t\"VKWebAppShowLeaderBoardBox\",\n\t\"VKWebAppShowInviteBox\",\n\t\"VKWebAppShowRequestBox\",\n\t\"VKWebAppAddToFavorites\",\n\t\"VKWebAppShowStoryBox\",\n\t\"VKWebAppStorageGet\",\n\t\"VKWebAppStorageGetKeys\",\n\t\"VKWebAppStorageSet\",\n\t\"VKWebAppFlashGetInfo\",\n\t\"VKWebAppSubscribeStoryApp\",\n\t\"VKWebAppOpenWallPost\",\n\t\"VKWebAppCheckAllowedScopes\",\n\t\"VKWebAppCheckBannerAd\",\n\t\"VKWebAppHideBannerAd\",\n\t\"VKWebAppShowBannerAd\",\n\t\"VKWebAppCheckNativeAds\",\n\t\"VKWebAppShowNativeAds\",\n\t\"VKWebAppRetargetingPixel\",\n\t\"VKWebAppConversionHit\",\n\t\"VKWebAppShowSubscriptionBox\",\n\t\"VKWebAppCheckSurvey\",\n\t\"VKWebAppShowSurvey\",\n\t\"VKWebAppScrollTop\",\n\t\"VKWebAppScrollTopStart\",\n\t\"VKWebAppScrollTopStop\",\n\t\"VKWebAppShowSlidesSheet\",\n\t\"VKWebAppTranslate\",\n\t\"VKWebAppRecommend\",\n\t\"VKWebAppAddToProfile\",\n\t\"VKWebAppGetFriends\",\n\t// Desktop web specific events\n\t...(isDesktopVk()\n\t\t? [\n\t\t\t\t\"VKWebAppResizeWindow\",\n\t\t\t\t\"VKWebAppAddToMenu\",\n\t\t\t\t\"VKWebAppShowInstallPushBox\",\n\t\t\t\t\"VKWebAppShowCommunityWidgetPreviewBox\",\n\t\t\t\t\"VKWebAppCallStart\",\n\t\t\t\t\"VKWebAppCallJoin\",\n\t\t\t\t\"VKWebAppCallGetStatus\",\n\t\t\t]\n\t\t: [\"VKWebAppShowImages\"]),\n]);\n\n// ------------------------------------------------------------- environment\n\nfunction clientWindow(): (Window & typeof globalThis) | undefined {\n\treturn typeof window === \"undefined\" ? undefined : window;\n}\n\n/**\n * The native side of the host (AndroidBridge / webkit / ReactNativeWebView)\n * lives on the window but is not part of any DOM typing — tests install\n * fakes under the same keys.\n */\nfunction hostGlobal(): Record<string, unknown> | undefined {\n\tconst win = clientWindow();\n\treturn win === undefined ? undefined : (win as unknown as Record<string, unknown>);\n}\n\nexport function isClientSide(): boolean {\n\treturn typeof window !== \"undefined\";\n}\n\nexport function isAndroidWebview(): boolean {\n\treturn Boolean(hostGlobal()?.[\"AndroidBridge\"]);\n}\n\nexport function isIosWebview(): boolean {\n\tconst webkit = hostGlobal()?.[\"webkit\"] as\n\t\t| { messageHandlers?: Record<string, { postMessage?: unknown }> }\n\t\t| undefined;\n\treturn Boolean(webkit?.messageHandlers?.[\"VKWebAppClose\"]);\n}\n\nexport function isReactNativeWebview(): boolean {\n\tconst reactNative = hostGlobal()?.[\"ReactNativeWebView\"] as\n\t\t| { postMessage?: unknown }\n\t\t| undefined;\n\treturn Boolean(reactNative && typeof reactNative.postMessage === \"function\");\n}\n\nexport function isWeb(): boolean {\n\treturn isClientSide() && !isAndroidWebview() && !isIosWebview();\n}\n\nexport function isMvk(): boolean {\n\treturn (\n\t\tisWeb() && /(^\\?|&)vk_platform=mobile_web(&|$)/.test(clientWindow()?.location.search ?? \"\")\n\t);\n}\n\nexport function isDesktopVk(): boolean {\n\treturn isWeb() && !isMvk();\n}\n\n// -------------------------------------------------------------- the bridge\n\n/**\n * request_id policy of a bridge instance.\n */\nexport interface RequestIdPolicy {\n\t/**\n\t * Own generator of outgoing request ids; receives an incrementing\n\t * sequence. The default mints `__vkma_<n>` — the ids the devtools tap\n\t * pairs responses by.\n\t */\n\tgenerator?: (sequence: number) => string | number;\n\t/**\n\t * Turn the auto-injection off: sends pass caller props untouched, so\n\t * promise pairing only works for calls that provide their own\n\t * `request_id`. Default: on.\n\t */\n\tinject?: boolean;\n}\n\nexport interface VkmaBridgeOptions {\n\t/** Outgoing request_id policy. */\n\trequestId?: RequestIdPolicy;\n\t/** Inject a shared tap (the default instance passes the page-wide one). */\n\ttap?: BridgeTap;\n\t/** Version string reported to the web host in the vk-connect envelope. */\n\tversion?: string;\n}\n\ninterface PendingRequest {\n\tresolve: (value: unknown) => void;\n\treject: (reason: unknown) => void;\n\ttapId: number;\n\toptions: SendOptions;\n\tcleanup: () => void;\n}\n\n/**\n * Creates a VK Bridge instance: transport dispatch, subscription fan-out,\n * promise pairing and the always-on tap.\n *\n * @param version Version of the package (reported to the web host).\n * @param [options] Tap sharing and the request_id policy.\n */\nexport function createVKBridge(version: string, options: VkmaBridgeOptions = {}): VkmaBridge {\n\tconst tap = options.tap ?? createTap();\n\tconst requestIdPolicy = options.requestId ?? {};\n\n\t/** Current frame id, assigned by the host via VKWebAppSettings. */\n\tlet webFrameId: string | undefined;\n\n\t/** Functions subscribed to bridge events. */\n\tconst subscribers: VKBridgeSubscribeHandler[] = [];\n\n\t/** Pending promise controllers by outgoing request_id. */\n\tconst pendingRequests = new Map<string, PendingRequest>();\n\n\tlet requestCounter = 0;\n\tlet supportedHandlers: Set<string> | undefined;\n\n\tconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n\t\ttypeof value === \"object\" && value !== null;\n\n\t// ------------------------------------------------------------- transports\n\n\tfunction sendThroughTransport(method: string, props: Record<string, unknown>): void {\n\t\tconst host = hostGlobal();\n\t\tconst android = host?.[\"AndroidBridge\"] as\n\t\t\t| Record<string, ((serializedData: string) => void) | undefined>\n\t\t\t| undefined;\n\t\t// Sending data through the Android bridge\n\t\tif (typeof android?.[method] === \"function\") {\n\t\t\tandroid[method](JSON.stringify(props));\n\t\t\treturn;\n\t\t}\n\n\t\tconst iosHandlers = (\n\t\t\thost?.[\"webkit\"] as\n\t\t\t\t| { messageHandlers?: Record<string, { postMessage?: unknown }> }\n\t\t\t\t| undefined\n\t\t)?.messageHandlers;\n\t\t// Sending data through the iOS bridge\n\t\tif (typeof iosHandlers?.[method]?.postMessage === \"function\") {\n\t\t\t// Not a window.postMessage — the native handler takes no targetOrigin.\n\t\t\t// oxlint-disable-next-line unicorn/require-post-message-target-origin\n\t\t\t(iosHandlers[method].postMessage as (data: unknown) => void)(props);\n\t\t\treturn;\n\t\t}\n\n\t\t// Sending data through the React Native bridge\n\t\tif (isReactNativeWebview()) {\n\t\t\tconst reactNative = host?.[\"ReactNativeWebView\"] as {\n\t\t\t\tpostMessage: (data: string) => void;\n\t\t\t};\n\t\t\t// Not a window.postMessage — the RN handler takes no targetOrigin.\n\t\t\t// oxlint-disable-next-line unicorn/require-post-message-target-origin\n\t\t\treactNative.postMessage(JSON.stringify({ handler: method, params: props }));\n\t\t\treturn;\n\t\t}\n\n\t\t// Sending data through the web bridge (the parent frame)\n\t\tconst win = clientWindow();\n\t\tconst parent = win?.parent;\n\t\tif (win && parent && typeof parent.postMessage === \"function\") {\n\t\t\t// The mini app does not know the host origin ahead of time (vk.ru,\n\t\t\t// m.vk.ru, mobile clients) — \"*\" is the wire contract here.\n\t\t\t// oxlint-disable-next-line unicorn/require-post-message-target-origin\n\t\t\tparent.postMessage(\n\t\t\t\t{\n\t\t\t\t\thandler: method,\n\t\t\t\t\tparams: props,\n\t\t\t\t\ttype: \"vk-connect\",\n\t\t\t\t\twebFrameId,\n\t\t\t\t\tconnectVersion: version,\n\t\t\t\t},\n\t\t\t\t\"*\",\n\t\t\t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------- event intake\n\n\tfunction handleEvent(event: unknown): void {\n\t\tif (isIosWebview() || isAndroidWebview()) {\n\t\t\t// WebViews: the native layer dispatches ready-made events\n\t\t\tdeliver(event as { detail?: { type?: unknown; data?: unknown } });\n\t\t\treturn;\n\t\t}\n\n\t\tlet bridgeEventData = (event as { data?: unknown } | undefined)?.data;\n\t\tif (!isWeb() || !bridgeEventData) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (isReactNativeWebview() && typeof bridgeEventData === \"string\") {\n\t\t\ttry {\n\t\t\t\tbridgeEventData = JSON.parse(bridgeEventData) as unknown;\n\t\t\t} catch {}\n\t\t}\n\n\t\tif (!isPlainObject(bridgeEventData)) {\n\t\t\treturn;\n\t\t}\n\t\tconst { type, data, frameId } = bridgeEventData as {\n\t\t\ttype?: unknown;\n\t\t\tdata?: unknown;\n\t\t\tframeId?: unknown;\n\t\t};\n\t\tif (!type) {\n\t\t\treturn;\n\t\t}\n\n\t\t// The host assigns this iframe its frame id — internal protocol,\n\t\t// not delivered to subscribers.\n\t\tif (type === \"VKWebAppSettings\") {\n\t\t\twebFrameId = typeof frameId === \"string\" ? frameId : webFrameId;\n\t\t\treturn;\n\t\t}\n\n\t\tdeliver({ detail: { type, data } });\n\t}\n\n\t/**\n\t * Upstream parity: every incoming event reaches `[...subscribers]` —\n\t * paired responses included, with the raw wire data (upstream's\n\t * promisifySend is itself a subscriber and strips request_id only for\n\t * the promise). Snapshot iteration: a handler may unsubscribe mid-fan.\n\t */\n\tfunction fanOutToSubscribers(type: string, data: unknown): void {\n\t\t// The wire type is stringly here by design — handleEvent dispatches\n\t\t// arbitrary host envelopes; the public subscribe surface narrows it.\n\t\t// The slice is a snapshot: a handler may unsubscribe mid-fan-out,\n\t\t// and shifting live iteration would skip its neighbours (upstream\n\t\t// copies the array for the same reason).\n\t\tconst out = { detail: { type, data } } as Parameters<VKBridgeSubscribeHandler>[0];\n\t\tfor (const listener of subscribers.slice()) {\n\t\t\tlistener(out);\n\t\t}\n\t}\n\n\t/** Fan an incoming event out to the tap and to the subscribers. */\n\tfunction deliver(event: { detail?: { type?: unknown; data?: unknown } }): void {\n\t\tconst { type, data } = event.detail ?? {};\n\t\tif (typeof type !== \"string\") {\n\t\t\treturn;\n\t\t}\n\n\t\t// A payload carrying one of our pending request ids is the paired\n\t\t// response to a send; everything else is an unsolicited event.\n\t\t// The wire contract types request_id as a string or a number.\n\t\tif (isPlainObject(data)) {\n\t\t\tconst wireRequestId = data[\"request_id\"];\n\t\t\tif (typeof wireRequestId !== \"string\" && typeof wireRequestId !== \"number\") {\n\t\t\t\ttap.emit({ direction: \"in\", kind: \"event\", name: type, payload: data });\n\t\t\t\tfanOutToSubscribers(type, data);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst requestId = String(wireRequestId);\n\t\t\tconst pending = pendingRequests.get(requestId);\n\t\t\tif (pending) {\n\t\t\t\tpendingRequests.delete(requestId);\n\t\t\t\tconst { request_id: _stripped, ...payload } = data;\n\t\t\t\tconst ok = !(\"error_type\" in data);\n\t\t\t\tpending.cleanup();\n\t\t\t\ttap.emit({\n\t\t\t\t\tdirection: \"in\",\n\t\t\t\t\tkind: \"response\",\n\t\t\t\t\tname: type,\n\t\t\t\t\tpayload,\n\t\t\t\t\treqId: pending.tapId,\n\t\t\t\t\tok,\n\t\t\t\t});\n\t\t\t\tif (ok) {\n\t\t\t\t\tpending.resolve(payload);\n\t\t\t\t} else {\n\t\t\t\t\tpending.reject(payload);\n\t\t\t\t}\n\t\t\t\t// After the pairing listener's job (the promise is settled\n\t\t\t\t// already, like upstream's first-registered pairing handler).\n\t\t\t\tfanOutToSubscribers(type, data);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\ttap.emit({ direction: \"in\", kind: \"event\", name: type, payload: data });\n\t\tfanOutToSubscribers(type, data);\n\t}\n\n\tfunction subscribe(listener: VKBridgeSubscribeHandler): void {\n\t\tsubscribers.push(listener);\n\t}\n\n\tfunction unsubscribe(listener: VKBridgeSubscribeHandler): void {\n\t\tconst index = subscribers.indexOf(listener);\n\t\tif (index > -1) {\n\t\t\tsubscribers.splice(index, 1);\n\t\t}\n\t}\n\n\t// Listening for messages from the runtime env. React Native on Android\n\t// dispatches on the document, everything else on the window.\n\tif (isClientSide()) {\n\t\tconst win = clientWindow();\n\t\tif (isReactNativeWebview() && /(android)/i.test(win?.navigator.userAgent ?? \"\")) {\n\t\t\tdocument.addEventListener(\"message\", handleEvent);\n\t\t} else if (win && \"addEventListener\" in win) {\n\t\t\twin.addEventListener(isWeb() ? \"message\" : \"VKWebAppEvent\", handleEvent);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------ send + tap\n\n\tconst send: VKBridgeSend = ((\n\t\tmethod: string,\n\t\tprops?: Record<string, unknown>,\n\t\tsendOptions?: SendOptions,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst signal = sendOptions?.signal;\n\t\t\t// A pre-aborted signal rejects before anything happens: no wire\n\t\t\t// send, no tap entry, no pending slot (the tap reflects reality).\n\t\t\tif (signal?.aborted) {\n\t\t\t\tconst error = new Error(`VK Bridge send \"${method}\" aborted`);\n\t\t\t\terror.name = \"AbortError\";\n\t\t\t\treject(error);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst sourceProps: Record<string, unknown> = { ...props };\n\t\t\tconst explicitId =\n\t\t\t\ttypeof sourceProps[\"request_id\"] === \"string\" ||\n\t\t\t\ttypeof sourceProps[\"request_id\"] === \"number\"\n\t\t\t\t\t? sourceProps[\"request_id\"]\n\t\t\t\t\t: undefined;\n\t\t\tlet requestId: string | number | undefined;\n\t\t\tif (explicitId !== undefined) {\n\t\t\t\trequestId = explicitId;\n\t\t\t} else if (requestIdPolicy.inject !== false) {\n\t\t\t\tconst sequence = ++requestCounter;\n\t\t\t\trequestId = requestIdPolicy.generator\n\t\t\t\t\t? requestIdPolicy.generator(sequence)\n\t\t\t\t\t: `__vkma_${sequence}`;\n\t\t\t}\n\n\t\t\t// The wire props are a separate object: the tap payload must not\n\t\t\t// mutate under a listener that observed the emit.\n\t\t\tconst wireProps =\n\t\t\t\trequestId === undefined ? sourceProps : { ...sourceProps, request_id: requestId };\n\n\t\t\tconst tapId = tap.emit({\n\t\t\t\tdirection: \"out\",\n\t\t\t\tkind: \"send\",\n\t\t\t\tname: method,\n\t\t\t\tpayload: wireProps,\n\t\t\t});\n\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\t\tlet signalHandler: (() => void) | undefined;\n\n\t\t\tconst cleanup = (): void => {\n\t\t\t\tif (timer !== undefined) {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t}\n\t\t\t\tif (signal && signalHandler) {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", signalHandler);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Timeout and abort must fire even for unpaired sends (no\n\t\t\t// pending slot to consult — e.g. inject: false without a caller\n\t\t\t// request_id): the caller asked for a bounded call. For paired\n\t\t\t// sends the pending entry doubles as the \"already answered\"\n\t\t\t// guard against a double rejection.\n\t\t\tconst timeoutReject = (): void => {\n\t\t\t\tif (requestId !== undefined && !pendingRequests.delete(String(requestId))) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcleanup();\n\t\t\t\tconst error = new Error(\n\t\t\t\t\t`VK Bridge send \"${method}\" timed out after ${String(sendOptions?.timeout)}ms`,\n\t\t\t\t);\n\t\t\t\terror.name = \"TimeoutError\";\n\t\t\t\treject(error);\n\t\t\t};\n\n\t\t\tconst abortReject = (): void => {\n\t\t\t\tif (requestId !== undefined && !pendingRequests.delete(String(requestId))) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcleanup();\n\t\t\t\tconst error = new Error(`VK Bridge send \"${method}\" aborted`);\n\t\t\t\terror.name = \"AbortError\";\n\t\t\t\treject(error);\n\t\t\t};\n\n\t\t\tif (requestId !== undefined) {\n\t\t\t\tpendingRequests.set(String(requestId), {\n\t\t\t\t\tresolve: resolve as (value: unknown) => void,\n\t\t\t\t\treject,\n\t\t\t\t\ttapId,\n\t\t\t\t\toptions: sendOptions ?? {},\n\t\t\t\t\tcleanup,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (sendOptions?.timeout !== undefined) {\n\t\t\t\ttimer = setTimeout(timeoutReject, sendOptions.timeout);\n\t\t\t}\n\t\t\tif (signal) {\n\t\t\t\tsignalHandler = abortReject;\n\t\t\t\tsignal.addEventListener(\"abort\", signalHandler);\n\t\t\t}\n\n\t\t\tsendThroughTransport(method, wireProps);\n\t\t});\n\t}) as VKBridgeSend;\n\n\t// The cache behind supportsAsync, fed by both the SetSupportedHandlers\n\t// response and the unsolicited pushes of the same name.\n\tsubscribe((event: { detail?: { type?: unknown; data?: unknown } }) => {\n\t\tif (event.detail?.type === \"SetSupportedHandlers\" && isPlainObject(event.detail.data)) {\n\t\t\tconst handlers = (event.detail.data as { supportedHandlers?: unknown })\n\t\t\t\t.supportedHandlers;\n\t\t\tif (Array.isArray(handlers)) {\n\t\t\t\tsupportedHandlers = new Set(\n\t\t\t\t\thandlers.filter((name): name is string => typeof name === \"string\"),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t});\n\n\tfunction supportsInner(method: string): boolean {\n\t\tif (isAndroidWebview()) {\n\t\t\tconst android = hostGlobal()?.[\"AndroidBridge\"] as\n\t\t\t\t| Record<string, ((serializedData: string) => void) | undefined>\n\t\t\t\t| undefined;\n\t\t\treturn typeof android?.[method] === \"function\";\n\t\t}\n\t\tif (isIosWebview()) {\n\t\t\tconst handlers = (\n\t\t\t\thostGlobal()?.[\"webkit\"] as\n\t\t\t\t\t| { messageHandlers?: Record<string, { postMessage?: unknown }> }\n\t\t\t\t\t| undefined\n\t\t\t)?.messageHandlers;\n\t\t\treturn typeof handlers?.[method]?.postMessage === \"function\";\n\t\t}\n\t\tif (isWeb()) {\n\t\t\treturn DESKTOP_METHODS.has(method);\n\t\t}\n\t\treturn false;\n\t}\n\n\tconst bridge: VkmaBridge = {\n\t\tsend,\n\t\tsendPromise: send,\n\t\tsubscribe,\n\t\tunsubscribe,\n\t\tsupports: (method: string) => {\n\t\t\tconsole.warn(\"bridge.supports method is deprecated. Use bridge.supportsAsync instead.\");\n\t\t\treturn supportsInner(method);\n\t\t},\n\t\tsupportsAsync: async (method: string) => {\n\t\t\tif (isAndroidWebview() || isIosWebview()) {\n\t\t\t\treturn supportsInner(method);\n\t\t\t}\n\t\t\tif (supportedHandlers) {\n\t\t\t\treturn supportedHandlers.has(method);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = (await send(\"SetSupportedHandlers\")) as {\n\t\t\t\t\tsupportedHandlers?: string[];\n\t\t\t\t};\n\t\t\t\tsupportedHandlers = new Set(response.supportedHandlers ?? [\"VKWebAppInit\"]);\n\t\t\t} catch {\n\t\t\t\tsupportedHandlers = new Set([\"VKWebAppInit\"]);\n\t\t\t}\n\t\t\treturn supportedHandlers.has(method);\n\t\t},\n\t\tisWebView: () => isIosWebview() || isAndroidWebview(),\n\t\tisIframe: () => {\n\t\t\tconst win = clientWindow();\n\t\t\treturn Boolean(win && win.parent !== win);\n\t\t},\n\t\tisEmbedded: () => bridge.isWebView() || bridge.isIframe(),\n\t\tisStandalone: () => !bridge.isEmbedded(),\n\t\ttap,\n\t\tonSend: listener =>\n\t\t\ttap.subscribe(event => {\n\t\t\t\tif (event.direction === \"out\") {\n\t\t\t\t\tlistener(event as BridgeTapEvent & { direction: \"out\" });\n\t\t\t\t}\n\t\t\t}),\n\t\tonEvent: listener =>\n\t\t\ttap.subscribe(event => {\n\t\t\t\tif (event.direction === \"in\") {\n\t\t\t\t\tlistener(event as BridgeTapEvent & { direction: \"in\" });\n\t\t\t\t}\n\t\t\t}),\n\t};\n\n\treturn bridge;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bridge.js","names":[],"sources":["../src/bridge.ts"],"sourcesContent":["/**\n * The bridge core: transport dispatch, subscription fan-out, promise pairing\n * and the always-on tap.\n *\n * Transports (checked lazily on every call, unlike the load-time caches of\n * the reference implementation — same in practice, testable in unit tests):\n *\n * - Android WebView: `window.AndroidBridge[method](JSON.stringify(props))`\n * - iOS WebView: `window.webkit.messageHandlers[method].postMessage(props)`\n * - React Native WebView: `window.ReactNativeWebView.postMessage(JSON)`\n * - Web (iframe on vk.ru / m.vk.ru): `parent.postMessage` with the\n * `type: \"vk-connect\"` envelope; receive side listens to `message`\n * - WebViews receive via `CustomEvent(\"VKWebAppEvent\")`\n *\n * The wire format is the platform interface — the host clients on the other\n * side of these bridges define it, which is why the envelopes above are\n * reproduced exactly.\n */\n\nimport { createTap, type BridgeTap, type BridgeTapEvent } from \"./tap.ts\";\nimport type { SendOptions, VKBridgeSend, VKBridgeSubscribeHandler, VkmaBridge } from \"./types.ts\";\n\n/** Methods supported on the desktop web hosts. */\nconst DESKTOP_METHODS = new Set([\n\t\"VKWebAppInit\",\n\t\"VKWebAppGetCommunityAuthToken\",\n\t\"VKWebAppAddToCommunity\",\n\t\"VKWebAppAddToHomeScreenInfo\",\n\t\"VKWebAppClose\",\n\t\"VKWebAppCopyText\",\n\t\"VKWebAppCreateHash\",\n\t\"VKWebAppGetUserInfo\",\n\t\"VKWebAppSetLocation\",\n\t\"VKWebAppSendToClient\",\n\t\"VKWebAppGetClientVersion\",\n\t\"VKWebAppGetPhoneNumber\",\n\t\"VKWebAppGetEmail\",\n\t\"VKWebAppGetGroupInfo\",\n\t\"VKWebAppGetGeodata\",\n\t\"VKWebAppGetCommunityToken\",\n\t\"VKWebAppGetConfig\",\n\t\"VKWebAppGetLaunchParams\",\n\t\"VKWebAppSetTitle\",\n\t\"VKWebAppGetAuthToken\",\n\t\"VKWebAppCallAPIMethod\",\n\t\"VKWebAppJoinGroup\",\n\t\"VKWebAppLeaveGroup\",\n\t\"VKWebAppAllowMessagesFromGroup\",\n\t\"VKWebAppDenyNotifications\",\n\t\"VKWebAppAllowNotifications\",\n\t\"VKWebAppOpenPayForm\",\n\t\"VKWebAppOpenApp\",\n\t\"VKWebAppShare\",\n\t\"VKWebAppShowWallPostBox\",\n\t\"VKWebAppScroll\",\n\t\"VKWebAppShowOrderBox\",\n\t\"VKWebAppShowLeaderBoardBox\",\n\t\"VKWebAppShowInviteBox\",\n\t\"VKWebAppShowRequestBox\",\n\t\"VKWebAppAddToFavorites\",\n\t\"VKWebAppShowStoryBox\",\n\t\"VKWebAppStorageGet\",\n\t\"VKWebAppStorageGetKeys\",\n\t\"VKWebAppStorageSet\",\n\t\"VKWebAppFlashGetInfo\",\n\t\"VKWebAppSubscribeStoryApp\",\n\t\"VKWebAppOpenWallPost\",\n\t\"VKWebAppCheckAllowedScopes\",\n\t\"VKWebAppCheckBannerAd\",\n\t\"VKWebAppHideBannerAd\",\n\t\"VKWebAppShowBannerAd\",\n\t\"VKWebAppCheckNativeAds\",\n\t\"VKWebAppShowNativeAds\",\n\t\"VKWebAppRetargetingPixel\",\n\t\"VKWebAppConversionHit\",\n\t\"VKWebAppShowSubscriptionBox\",\n\t\"VKWebAppCheckSurvey\",\n\t\"VKWebAppShowSurvey\",\n\t\"VKWebAppScrollTop\",\n\t\"VKWebAppScrollTopStart\",\n\t\"VKWebAppScrollTopStop\",\n\t\"VKWebAppShowSlidesSheet\",\n\t\"VKWebAppTranslate\",\n\t\"VKWebAppRecommend\",\n\t\"VKWebAppAddToProfile\",\n\t\"VKWebAppGetFriends\",\n\t// Desktop web specific events\n\t...(isDesktopVk()\n\t\t? [\n\t\t\t\t\"VKWebAppResizeWindow\",\n\t\t\t\t\"VKWebAppAddToMenu\",\n\t\t\t\t\"VKWebAppShowInstallPushBox\",\n\t\t\t\t\"VKWebAppShowCommunityWidgetPreviewBox\",\n\t\t\t\t\"VKWebAppCallStart\",\n\t\t\t\t\"VKWebAppCallJoin\",\n\t\t\t\t\"VKWebAppCallGetStatus\",\n\t\t\t]\n\t\t: [\"VKWebAppShowImages\"]),\n]);\n\n// ------------------------------------------------------------- environment\n\nfunction clientWindow(): (Window & typeof globalThis) | undefined {\n\treturn typeof window === \"undefined\" ? undefined : window;\n}\n\n/**\n * The native side of the host (AndroidBridge / webkit / ReactNativeWebView)\n * lives on the window but is not part of any DOM typing — tests install\n * fakes under the same keys.\n */\nfunction hostGlobal(): Record<string, unknown> | undefined {\n\tconst win = clientWindow();\n\treturn win === undefined ? undefined : (win as unknown as Record<string, unknown>);\n}\n\nexport function isClientSide(): boolean {\n\treturn typeof window !== \"undefined\";\n}\n\nexport function isAndroidWebview(): boolean {\n\treturn Boolean(hostGlobal()?.[\"AndroidBridge\"]);\n}\n\nexport function isIosWebview(): boolean {\n\tconst webkit = hostGlobal()?.[\"webkit\"] as\n\t\t| { messageHandlers?: Record<string, { postMessage?: unknown }> }\n\t\t| undefined;\n\treturn Boolean(webkit?.messageHandlers?.[\"VKWebAppClose\"]);\n}\n\nexport function isReactNativeWebview(): boolean {\n\tconst reactNative = hostGlobal()?.[\"ReactNativeWebView\"] as\n\t\t| { postMessage?: unknown }\n\t\t| undefined;\n\treturn Boolean(reactNative && typeof reactNative.postMessage === \"function\");\n}\n\nexport function isWeb(): boolean {\n\treturn isClientSide() && !isAndroidWebview() && !isIosWebview();\n}\n\nexport function isMvk(): boolean {\n\treturn (\n\t\tisWeb() && /(^\\?|&)vk_platform=mobile_web(&|$)/.test(clientWindow()?.location.search ?? \"\")\n\t);\n}\n\nexport function isDesktopVk(): boolean {\n\treturn isWeb() && !isMvk();\n}\n\n// -------------------------------------------------------------- the bridge\n\n/**\n * request_id policy of a bridge instance.\n */\nexport interface RequestIdPolicy {\n\t/**\n\t * Own generator of outgoing request ids; receives an incrementing\n\t * sequence. The default mints `__vkma_<n>` — the ids the devtools tap\n\t * pairs responses by.\n\t */\n\tgenerator?: (sequence: number) => string | number;\n\t/**\n\t * Turn the auto-injection off: sends pass caller props untouched, so\n\t * promise pairing only works for calls that provide their own\n\t * `request_id`. Default: on.\n\t */\n\tinject?: boolean;\n}\n\nexport interface VkmaBridgeOptions {\n\t/** Outgoing request_id policy. */\n\trequestId?: RequestIdPolicy;\n\t/** Inject a shared tap (the default instance passes the page-wide one). */\n\ttap?: BridgeTap;\n\t/** Version string reported to the web host in the vk-connect envelope. */\n\tversion?: string;\n}\n\ninterface PendingRequest {\n\tresolve: (value: unknown) => void;\n\treject: (reason: unknown) => void;\n\ttapId: number;\n\toptions: SendOptions;\n\tcleanup: () => void;\n}\n\n/**\n * Creates a VK Bridge instance: transport dispatch, subscription fan-out,\n * promise pairing and the always-on tap.\n *\n * @param version Version of the package (reported to the web host).\n * @param [options] Tap sharing and the request_id policy.\n */\nexport function createVKBridge(version: string, options: VkmaBridgeOptions = {}): VkmaBridge {\n\tconst tap = options.tap ?? createTap();\n\tconst requestIdPolicy = options.requestId ?? {};\n\n\t/** Current frame id, assigned by the host via VKWebAppSettings. */\n\tlet webFrameId: string | undefined;\n\n\t/** Functions subscribed to bridge events. */\n\tconst subscribers: VKBridgeSubscribeHandler[] = [];\n\n\t/** Pending promise controllers by outgoing request_id. */\n\tconst pendingRequests = new Map<string, PendingRequest>();\n\n\tlet requestCounter = 0;\n\tlet supportedHandlers: Set<string> | undefined;\n\n\tconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n\t\ttypeof value === \"object\" && value !== null;\n\n\t// ------------------------------------------------------------- transports\n\n\tfunction sendThroughTransport(method: string, props: Record<string, unknown>): void {\n\t\tconst host = hostGlobal();\n\t\tconst android = host?.[\"AndroidBridge\"] as\n\t\t\t| Record<string, ((serializedData: string) => void) | undefined>\n\t\t\t| undefined;\n\t\t// Sending data through the Android bridge\n\t\tif (typeof android?.[method] === \"function\") {\n\t\t\tandroid[method](JSON.stringify(props));\n\t\t\treturn;\n\t\t}\n\n\t\tconst iosHandlers = (\n\t\t\thost?.[\"webkit\"] as\n\t\t\t\t| { messageHandlers?: Record<string, { postMessage?: unknown }> }\n\t\t\t\t| undefined\n\t\t)?.messageHandlers;\n\t\t// Sending data through the iOS bridge\n\t\tif (typeof iosHandlers?.[method]?.postMessage === \"function\") {\n\t\t\t// Not a window.postMessage — the native handler takes no targetOrigin.\n\t\t\t// oxlint-disable-next-line unicorn/require-post-message-target-origin\n\t\t\t(iosHandlers[method].postMessage as (data: unknown) => void)(props);\n\t\t\treturn;\n\t\t}\n\n\t\t// Sending data through the React Native bridge\n\t\tif (isReactNativeWebview()) {\n\t\t\tconst reactNative = host?.[\"ReactNativeWebView\"] as {\n\t\t\t\tpostMessage: (data: string) => void;\n\t\t\t};\n\t\t\t// Not a window.postMessage — the RN handler takes no targetOrigin.\n\t\t\t// oxlint-disable-next-line unicorn/require-post-message-target-origin\n\t\t\treactNative.postMessage(JSON.stringify({ handler: method, params: props }));\n\t\t\treturn;\n\t\t}\n\n\t\t// Sending data through the web bridge (the parent frame)\n\t\tconst win = clientWindow();\n\t\tconst parent = win?.parent;\n\t\tif (win && parent && typeof parent.postMessage === \"function\") {\n\t\t\t// A hoisted handle, not a literal `parent.postMessage(...)` call:\n\t\t\t// Facebook's Instant Games static analysis rejects builds that\n\t\t\t// contain the literal substring (VKCOM/vk-bridge#290) — the same\n\t\t\t// pattern as the `iosBridge`/`reactNative` handles above.\n\t\t\tconst webBridge = parent;\n\t\t\t// The mini app does not know the host origin ahead of time (vk.ru,\n\t\t\t// m.vk.ru, mobile clients) — \"*\" is the wire contract here.\n\t\t\t// oxlint-disable-next-line unicorn/require-post-message-target-origin\n\t\t\twebBridge.postMessage(\n\t\t\t\t{\n\t\t\t\t\thandler: method,\n\t\t\t\t\tparams: props,\n\t\t\t\t\ttype: \"vk-connect\",\n\t\t\t\t\twebFrameId,\n\t\t\t\t\tconnectVersion: version,\n\t\t\t\t},\n\t\t\t\t\"*\",\n\t\t\t);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------- event intake\n\n\tfunction handleEvent(event: unknown): void {\n\t\tif (isIosWebview() || isAndroidWebview()) {\n\t\t\t// WebViews: the native layer dispatches ready-made events\n\t\t\tdeliver(event as { detail?: { type?: unknown; data?: unknown } });\n\t\t\treturn;\n\t\t}\n\n\t\tlet bridgeEventData = (event as { data?: unknown } | undefined)?.data;\n\t\tif (!isWeb() || !bridgeEventData) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (isReactNativeWebview() && typeof bridgeEventData === \"string\") {\n\t\t\ttry {\n\t\t\t\tbridgeEventData = JSON.parse(bridgeEventData) as unknown;\n\t\t\t} catch {}\n\t\t}\n\n\t\tif (!isPlainObject(bridgeEventData)) {\n\t\t\treturn;\n\t\t}\n\t\tconst { type, data, frameId } = bridgeEventData as {\n\t\t\ttype?: unknown;\n\t\t\tdata?: unknown;\n\t\t\tframeId?: unknown;\n\t\t};\n\t\tif (!type) {\n\t\t\treturn;\n\t\t}\n\n\t\t// The host assigns this iframe its frame id — internal protocol,\n\t\t// not delivered to subscribers.\n\t\tif (type === \"VKWebAppSettings\") {\n\t\t\twebFrameId = typeof frameId === \"string\" ? frameId : webFrameId;\n\t\t\treturn;\n\t\t}\n\n\t\tdeliver({ detail: { type, data } });\n\t}\n\n\t/**\n\t * Upstream parity: every incoming event reaches `[...subscribers]` —\n\t * paired responses included, with the raw wire data (upstream's\n\t * promisifySend is itself a subscriber and strips request_id only for\n\t * the promise). Snapshot iteration: a handler may unsubscribe mid-fan.\n\t */\n\tfunction fanOutToSubscribers(type: string, data: unknown): void {\n\t\t// The wire type is stringly here by design — handleEvent dispatches\n\t\t// arbitrary host envelopes; the public subscribe surface narrows it.\n\t\t// The slice is a snapshot: a handler may unsubscribe mid-fan-out,\n\t\t// and shifting live iteration would skip its neighbours (upstream\n\t\t// copies the array for the same reason).\n\t\tconst out = { detail: { type, data } } as Parameters<VKBridgeSubscribeHandler>[0];\n\t\tfor (const listener of subscribers.slice()) {\n\t\t\tlistener(out);\n\t\t}\n\t}\n\n\t/** Fan an incoming event out to the tap and to the subscribers. */\n\tfunction deliver(event: { detail?: { type?: unknown; data?: unknown } }): void {\n\t\tconst { type, data } = event.detail ?? {};\n\t\tif (typeof type !== \"string\") {\n\t\t\treturn;\n\t\t}\n\n\t\t// A payload carrying one of our pending request ids is the paired\n\t\t// response to a send; everything else is an unsolicited event.\n\t\t// The wire contract types request_id as a string or a number.\n\t\tif (isPlainObject(data)) {\n\t\t\tconst wireRequestId = data[\"request_id\"];\n\t\t\tif (typeof wireRequestId !== \"string\" && typeof wireRequestId !== \"number\") {\n\t\t\t\ttap.emit({ direction: \"in\", kind: \"event\", name: type, payload: data });\n\t\t\t\tfanOutToSubscribers(type, data);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst requestId = String(wireRequestId);\n\t\t\tconst pending = pendingRequests.get(requestId);\n\t\t\tif (pending) {\n\t\t\t\tpendingRequests.delete(requestId);\n\t\t\t\tconst { request_id: _stripped, ...payload } = data;\n\t\t\t\tconst ok = !(\"error_type\" in data);\n\t\t\t\tpending.cleanup();\n\t\t\t\ttap.emit({\n\t\t\t\t\tdirection: \"in\",\n\t\t\t\t\tkind: \"response\",\n\t\t\t\t\tname: type,\n\t\t\t\t\tpayload,\n\t\t\t\t\treqId: pending.tapId,\n\t\t\t\t\tok,\n\t\t\t\t});\n\t\t\t\tif (ok) {\n\t\t\t\t\tpending.resolve(payload);\n\t\t\t\t} else {\n\t\t\t\t\tpending.reject(payload);\n\t\t\t\t}\n\t\t\t\t// After the pairing listener's job (the promise is settled\n\t\t\t\t// already, like upstream's first-registered pairing handler).\n\t\t\t\tfanOutToSubscribers(type, data);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\ttap.emit({ direction: \"in\", kind: \"event\", name: type, payload: data });\n\t\tfanOutToSubscribers(type, data);\n\t}\n\n\tfunction subscribe(listener: VKBridgeSubscribeHandler): void {\n\t\tsubscribers.push(listener);\n\t}\n\n\tfunction unsubscribe(listener: VKBridgeSubscribeHandler): void {\n\t\tconst index = subscribers.indexOf(listener);\n\t\tif (index > -1) {\n\t\t\tsubscribers.splice(index, 1);\n\t\t}\n\t}\n\n\t// Listening for messages from the runtime env. React Native on Android\n\t// dispatches on the document, everything else on the window.\n\tif (isClientSide()) {\n\t\tconst win = clientWindow();\n\t\tif (isReactNativeWebview() && /(android)/i.test(win?.navigator.userAgent ?? \"\")) {\n\t\t\tdocument.addEventListener(\"message\", handleEvent);\n\t\t} else if (win && \"addEventListener\" in win) {\n\t\t\twin.addEventListener(isWeb() ? \"message\" : \"VKWebAppEvent\", handleEvent);\n\t\t}\n\t}\n\n\t// ------------------------------------------------------------ send + tap\n\n\tconst send: VKBridgeSend = ((\n\t\tmethod: string,\n\t\tprops?: Record<string, unknown>,\n\t\tsendOptions?: SendOptions,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst signal = sendOptions?.signal;\n\t\t\t// A pre-aborted signal rejects before anything happens: no wire\n\t\t\t// send, no tap entry, no pending slot (the tap reflects reality).\n\t\t\tif (signal?.aborted) {\n\t\t\t\tconst error = new Error(`VK Bridge send \"${method}\" aborted`);\n\t\t\t\terror.name = \"AbortError\";\n\t\t\t\treject(error);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst sourceProps: Record<string, unknown> = { ...props };\n\t\t\tconst explicitId =\n\t\t\t\ttypeof sourceProps[\"request_id\"] === \"string\" ||\n\t\t\t\ttypeof sourceProps[\"request_id\"] === \"number\"\n\t\t\t\t\t? sourceProps[\"request_id\"]\n\t\t\t\t\t: undefined;\n\t\t\tlet requestId: string | number | undefined;\n\t\t\tif (explicitId !== undefined) {\n\t\t\t\trequestId = explicitId;\n\t\t\t} else if (requestIdPolicy.inject !== false) {\n\t\t\t\tconst sequence = ++requestCounter;\n\t\t\t\trequestId = requestIdPolicy.generator\n\t\t\t\t\t? requestIdPolicy.generator(sequence)\n\t\t\t\t\t: `__vkma_${sequence}`;\n\t\t\t}\n\n\t\t\t// The wire props are a separate object: the tap payload must not\n\t\t\t// mutate under a listener that observed the emit.\n\t\t\tconst wireProps =\n\t\t\t\trequestId === undefined ? sourceProps : { ...sourceProps, request_id: requestId };\n\n\t\t\tconst tapId = tap.emit({\n\t\t\t\tdirection: \"out\",\n\t\t\t\tkind: \"send\",\n\t\t\t\tname: method,\n\t\t\t\tpayload: wireProps,\n\t\t\t});\n\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\t\tlet signalHandler: (() => void) | undefined;\n\n\t\t\tconst cleanup = (): void => {\n\t\t\t\tif (timer !== undefined) {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t}\n\t\t\t\tif (signal && signalHandler) {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", signalHandler);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Timeout and abort must fire even for unpaired sends (no\n\t\t\t// pending slot to consult — e.g. inject: false without a caller\n\t\t\t// request_id): the caller asked for a bounded call. For paired\n\t\t\t// sends the pending entry doubles as the \"already answered\"\n\t\t\t// guard against a double rejection.\n\t\t\tconst timeoutReject = (): void => {\n\t\t\t\tif (requestId !== undefined && !pendingRequests.delete(String(requestId))) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcleanup();\n\t\t\t\tconst error = new Error(\n\t\t\t\t\t`VK Bridge send \"${method}\" timed out after ${String(sendOptions?.timeout)}ms`,\n\t\t\t\t);\n\t\t\t\terror.name = \"TimeoutError\";\n\t\t\t\treject(error);\n\t\t\t};\n\n\t\t\tconst abortReject = (): void => {\n\t\t\t\tif (requestId !== undefined && !pendingRequests.delete(String(requestId))) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcleanup();\n\t\t\t\tconst error = new Error(`VK Bridge send \"${method}\" aborted`);\n\t\t\t\terror.name = \"AbortError\";\n\t\t\t\treject(error);\n\t\t\t};\n\n\t\t\tif (requestId !== undefined) {\n\t\t\t\tpendingRequests.set(String(requestId), {\n\t\t\t\t\tresolve: resolve as (value: unknown) => void,\n\t\t\t\t\treject,\n\t\t\t\t\ttapId,\n\t\t\t\t\toptions: sendOptions ?? {},\n\t\t\t\t\tcleanup,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (sendOptions?.timeout !== undefined) {\n\t\t\t\ttimer = setTimeout(timeoutReject, sendOptions.timeout);\n\t\t\t}\n\t\t\tif (signal) {\n\t\t\t\tsignalHandler = abortReject;\n\t\t\t\tsignal.addEventListener(\"abort\", signalHandler);\n\t\t\t}\n\n\t\t\tsendThroughTransport(method, wireProps);\n\t\t});\n\t}) as VKBridgeSend;\n\n\t// The cache behind supportsAsync, fed by both the SetSupportedHandlers\n\t// response and the unsolicited pushes of the same name.\n\tsubscribe((event: { detail?: { type?: unknown; data?: unknown } }) => {\n\t\tif (event.detail?.type === \"SetSupportedHandlers\" && isPlainObject(event.detail.data)) {\n\t\t\tconst handlers = (event.detail.data as { supportedHandlers?: unknown })\n\t\t\t\t.supportedHandlers;\n\t\t\tif (Array.isArray(handlers)) {\n\t\t\t\tsupportedHandlers = new Set(\n\t\t\t\t\thandlers.filter((name): name is string => typeof name === \"string\"),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t});\n\n\tfunction supportsInner(method: string): boolean {\n\t\tif (isAndroidWebview()) {\n\t\t\tconst android = hostGlobal()?.[\"AndroidBridge\"] as\n\t\t\t\t| Record<string, ((serializedData: string) => void) | undefined>\n\t\t\t\t| undefined;\n\t\t\treturn typeof android?.[method] === \"function\";\n\t\t}\n\t\tif (isIosWebview()) {\n\t\t\tconst handlers = (\n\t\t\t\thostGlobal()?.[\"webkit\"] as\n\t\t\t\t\t| { messageHandlers?: Record<string, { postMessage?: unknown }> }\n\t\t\t\t\t| undefined\n\t\t\t)?.messageHandlers;\n\t\t\treturn typeof handlers?.[method]?.postMessage === \"function\";\n\t\t}\n\t\tif (isWeb()) {\n\t\t\treturn DESKTOP_METHODS.has(method);\n\t\t}\n\t\treturn false;\n\t}\n\n\tconst bridge: VkmaBridge = {\n\t\tsend,\n\t\tsendPromise: send,\n\t\tsubscribe,\n\t\tunsubscribe,\n\t\tsupports: (method: string) => {\n\t\t\tconsole.warn(\"bridge.supports method is deprecated. Use bridge.supportsAsync instead.\");\n\t\t\treturn supportsInner(method);\n\t\t},\n\t\tsupportsAsync: async (method: string) => {\n\t\t\tif (isAndroidWebview() || isIosWebview()) {\n\t\t\t\treturn supportsInner(method);\n\t\t\t}\n\t\t\tif (supportedHandlers) {\n\t\t\t\treturn supportedHandlers.has(method);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = (await send(\"SetSupportedHandlers\")) as {\n\t\t\t\t\tsupportedHandlers?: string[];\n\t\t\t\t};\n\t\t\t\tsupportedHandlers = new Set(response.supportedHandlers ?? [\"VKWebAppInit\"]);\n\t\t\t} catch {\n\t\t\t\tsupportedHandlers = new Set([\"VKWebAppInit\"]);\n\t\t\t}\n\t\t\treturn supportedHandlers.has(method);\n\t\t},\n\t\tisWebView: () => isIosWebview() || isAndroidWebview(),\n\t\tisIframe: () => {\n\t\t\tconst win = clientWindow();\n\t\t\treturn Boolean(win && win.parent !== win);\n\t\t},\n\t\tisEmbedded: () => bridge.isWebView() || bridge.isIframe(),\n\t\tisStandalone: () => !bridge.isEmbedded(),\n\t\ttap,\n\t\tonSend: listener =>\n\t\t\ttap.subscribe(event => {\n\t\t\t\tif (event.direction === \"out\") {\n\t\t\t\t\tlistener(event as BridgeTapEvent & { direction: \"out\" });\n\t\t\t\t}\n\t\t\t}),\n\t\tonEvent: listener =>\n\t\t\ttap.subscribe(event => {\n\t\t\t\tif (event.direction === \"in\") {\n\t\t\t\t\tlistener(event as BridgeTapEvent & { direction: \"in\" });\n\t\t\t\t}\n\t\t\t}),\n\t};\n\n\treturn bridge;\n}\n"],"mappings":"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"}
@@ -2,8 +2,9 @@ import { GetLaunchParamsResponse } from "@vkma.dev/bridge-schema";
2
2
  //#region src/launch-params.d.ts
3
3
  /**
4
4
  * Launch params as they appear in the URL — a superset of
5
- * `GetLaunchParamsResponse` with the keys the host only sends via the query
6
- * string.
5
+ * `GetLaunchParamsResponse` with the URL-only keys. The OK (Odnoklassniki)
6
+ * keys (`vk_client`, `vk_ok_*`, `vk_original_vk_id`, `vk_are_pushes_enabled`)
7
+ * come from the catalogue as optional — vk.com hosts never send them.
7
8
  */
8
9
  export interface LaunchParams extends GetLaunchParamsResponse {
9
10
  vk_chat_id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"launch-params.d.ts","names":[],"sources":["../src/launch-params.ts"],"mappings":";;;;;;;iBA6BiB,qBAAqB;EACrC;EACA;EACA;EACA;EACA;EACA;;;;;;;;;qBAUY,yCAAsC,yBAEhD,QAAQ"}
1
+ {"version":3,"file":"launch-params.d.ts","names":[],"sources":["../src/launch-params.ts"],"mappings":";;;;;;;;iBAkCiB,qBAAqB;EACrC;EACA;EACA;EACA;EACA;EACA;;;;;;;;;qBAUY,yCAAsC,yBAEhD,QAAQ"}
@@ -20,10 +20,14 @@ const PLATFORMS = /* @__PURE__ */ new Set([
20
20
  "desktop_web",
21
21
  "desktop_web_messenger",
22
22
  "desktop_app_messenger",
23
+ "desktop_web_ok",
23
24
  "mobile_web",
25
+ "mobile_web_ok",
24
26
  "mobile_android",
27
+ "mobile_android_ok",
25
28
  "mobile_android_messenger",
26
29
  "mobile_iphone",
30
+ "mobile_iphone_ok",
27
31
  "mobile_iphone_messenger",
28
32
  "mobile_ipad"
29
33
  ]);
@@ -40,8 +44,10 @@ const parseURLSearchParamsForGetLaunchParams = (searchParams) => {
40
44
  const parsedSearchParams = new URLSearchParams(searchParams);
41
45
  const convertToggleStateFromStringToNumber = (value) => {
42
46
  switch (value) {
43
- case "0": return 0;
44
- case "1": return 1;
47
+ case "0":
48
+ case "false": return 0;
49
+ case "1":
50
+ case "true": return 1;
45
51
  default: return;
46
52
  }
47
53
  };
@@ -55,11 +61,16 @@ const parseURLSearchParamsForGetLaunchParams = (searchParams) => {
55
61
  case "vk_user_id":
56
62
  case "vk_app_id":
57
63
  case "vk_group_id":
64
+ case "vk_ok_app_id":
65
+ case "vk_ok_user_id":
66
+ case "vk_original_vk_id":
58
67
  launchParams[query] = Number(value);
59
68
  break;
60
69
  case "sign":
61
70
  case "vk_chat_id":
62
71
  case "vk_ref":
72
+ case "vk_ok_ref":
73
+ case "vk_client":
63
74
  case "vk_access_token_settings":
64
75
  launchParams[query] = value;
65
76
  break;
@@ -68,6 +79,7 @@ const parseURLSearchParamsForGetLaunchParams = (searchParams) => {
68
79
  break;
69
80
  case "vk_is_app_user":
70
81
  case "vk_are_notifications_enabled":
82
+ case "vk_are_pushes_enabled":
71
83
  case "vk_is_favorite":
72
84
  launchParams[query] = convertToggleStateFromStringToNumber(value);
73
85
  break;
@@ -1 +1 @@
1
- {"version":3,"file":"launch-params.js","names":[],"sources":["../src/launch-params.ts"],"sourcesContent":["/**\n * Launch params: parsing the `vk_*` query string every mini app is opened\n * with, into the typed shape of the catalogue's `GetLaunchParamsResponse`\n * plus the extra keys only present in the URL form.\n */\n\nimport type { GetLaunchParamsResponse } from \"@vkma.dev/bridge-schema\";\n\nconst LANGUAGES = new Set([\"ru\", \"uk\", \"ua\", \"en\", \"be\", \"kz\", \"pt\", \"es\"]);\n\nconst GROUP_ROLES = new Set([\"admin\", \"editor\", \"member\", \"moder\", \"none\"]);\n\nconst PLATFORMS = new Set([\n\t\"desktop_web\",\n\t\"desktop_web_messenger\",\n\t\"desktop_app_messenger\",\n\t\"mobile_web\",\n\t\"mobile_android\",\n\t\"mobile_android_messenger\",\n\t\"mobile_iphone\",\n\t\"mobile_iphone_messenger\",\n\t\"mobile_ipad\",\n]);\n\n/**\n * Launch params as they appear in the URL — a superset of\n * `GetLaunchParamsResponse` with the keys the host only sends via the query\n * string.\n */\nexport interface LaunchParams extends GetLaunchParamsResponse {\n\tvk_chat_id: string;\n\tvk_is_recommended: number;\n\tvk_profile_id: number;\n\tvk_has_profile_button: number;\n\tvk_testing_group_id: number;\n\todr_enabled: undefined | 1;\n}\n\n/**\n * Parses launch params out of a query string (à la\n * `location.search.slice(1)`), field by field, keeping only the known keys\n * with validated values.\n *\n * @see https://dev.vk.ru/mini-apps/development/launch-params\n */\nexport const parseURLSearchParamsForGetLaunchParams = (\n\tsearchParams: string,\n): Partial<LaunchParams> => {\n\tconst launchParams: Partial<LaunchParams> = {};\n\n\ttry {\n\t\tconst parsedSearchParams = new URLSearchParams(searchParams);\n\n\t\tconst convertToggleStateFromStringToNumber = (value: string): 0 | 1 | undefined => {\n\t\t\tswitch (value) {\n\t\t\t\tcase \"0\":\n\t\t\t\t\treturn 0;\n\t\t\t\tcase \"1\":\n\t\t\t\t\treturn 1;\n\t\t\t\tdefault:\n\t\t\t\t\treturn undefined;\n\t\t\t}\n\t\t};\n\n\t\tparsedSearchParams.forEach((value, query) => {\n\t\t\tswitch (query) {\n\t\t\t\tcase \"vk_ts\":\n\t\t\t\tcase \"vk_is_recommended\":\n\t\t\t\tcase \"vk_profile_id\":\n\t\t\t\tcase \"vk_has_profile_button\":\n\t\t\t\tcase \"vk_testing_group_id\":\n\t\t\t\tcase \"vk_user_id\":\n\t\t\t\tcase \"vk_app_id\":\n\t\t\t\tcase \"vk_group_id\":\n\t\t\t\t\tlaunchParams[query] = Number(value);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"sign\":\n\t\t\t\tcase \"vk_chat_id\":\n\t\t\t\tcase \"vk_ref\":\n\t\t\t\tcase \"vk_access_token_settings\":\n\t\t\t\t\tlaunchParams[query] = value;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"odr_enabled\":\n\t\t\t\t\tlaunchParams.odr_enabled = value === \"1\" ? 1 : undefined;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"vk_is_app_user\":\n\t\t\t\tcase \"vk_are_notifications_enabled\":\n\t\t\t\tcase \"vk_is_favorite\": {\n\t\t\t\t\tlaunchParams[query] = convertToggleStateFromStringToNumber(value);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"vk_language\": {\n\t\t\t\t\tlaunchParams.vk_language = LANGUAGES.has(value)\n\t\t\t\t\t\t? (value as Partial<LaunchParams>[\"vk_language\"])\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"vk_viewer_group_role\": {\n\t\t\t\t\tlaunchParams.vk_viewer_group_role = GROUP_ROLES.has(value)\n\t\t\t\t\t\t? (value as Partial<LaunchParams>[\"vk_viewer_group_role\"])\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"vk_platform\": {\n\t\t\t\t\tlaunchParams.vk_platform = PLATFORMS.has(value)\n\t\t\t\t\t\t? (value as Partial<LaunchParams>[\"vk_platform\"])\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t} catch (error) {\n\t\tconsole.warn(error);\n\t}\n\n\treturn launchParams;\n};\n"],"mappings":";AAQA,MAAM,4BAAY,IAAI,IAAI;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI,CAAC;AAE1E,MAAM,8BAAc,IAAI,IAAI;CAAC;CAAS;CAAU;CAAU;CAAS;AAAM,CAAC;AAE1E,MAAM,4BAAY,IAAI,IAAI;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD,CAAC;;;;;;;;AAuBD,MAAa,0CACZ,iBAC2B;CAC3B,MAAM,eAAsC,CAAC;CAE7C,IAAI;EACH,MAAM,qBAAqB,IAAI,gBAAgB,YAAY;EAE3D,MAAM,wCAAwC,UAAqC;GAClF,QAAQ,OAAR;IACC,KAAK,KACJ,OAAO;IACR,KAAK,KACJ,OAAO;IACR,SACC;GACF;EACD;EAEA,mBAAmB,SAAS,OAAO,UAAU;GAC5C,QAAQ,OAAR;IACC,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;KACJ,aAAa,SAAS,OAAO,KAAK;KAClC;IACD,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;KACJ,aAAa,SAAS;KACtB;IACD,KAAK;KACJ,aAAa,cAAc,UAAU,MAAM,IAAI,KAAA;KAC/C;IACD,KAAK;IACL,KAAK;IACL,KAAK;KACJ,aAAa,SAAS,qCAAqC,KAAK;KAChE;IAED,KAAK;KACJ,aAAa,cAAc,UAAU,IAAI,KAAK,IAC1C,QACD,KAAA;KACH;IAED,KAAK;KACJ,aAAa,uBAAuB,YAAY,IAAI,KAAK,IACrD,QACD,KAAA;KACH;IAED,KAAK,eACJ,aAAa,cAAc,UAAU,IAAI,KAAK,IAC1C,QACD,KAAA;GAGL;EACD,CAAC;CACF,SAAS,OAAO;EACf,QAAQ,KAAK,KAAK;CACnB;CAEA,OAAO;AACR"}
1
+ {"version":3,"file":"launch-params.js","names":[],"sources":["../src/launch-params.ts"],"sourcesContent":["/**\n * Launch params: parsing the `vk_*` query string every mini app is opened\n * with, into the typed shape of the catalogue's `GetLaunchParamsResponse`\n * plus the extra keys only present in the URL form.\n */\n\nimport type { GetLaunchParamsResponse } from \"@vkma.dev/bridge-schema\";\n\nconst LANGUAGES = new Set([\"ru\", \"uk\", \"ua\", \"en\", \"be\", \"kz\", \"pt\", \"es\"]);\n\nconst GROUP_ROLES = new Set([\"admin\", \"editor\", \"member\", \"moder\", \"none\"]);\n\nconst PLATFORMS = new Set([\n\t\"desktop_web\",\n\t\"desktop_web_messenger\",\n\t\"desktop_app_messenger\",\n\t\"desktop_web_ok\",\n\t\"mobile_web\",\n\t\"mobile_web_ok\",\n\t\"mobile_android\",\n\t\"mobile_android_ok\",\n\t\"mobile_android_messenger\",\n\t\"mobile_iphone\",\n\t\"mobile_iphone_ok\",\n\t\"mobile_iphone_messenger\",\n\t\"mobile_ipad\",\n]);\n\n/**\n * Launch params as they appear in the URL — a superset of\n * `GetLaunchParamsResponse` with the URL-only keys. The OK (Odnoklassniki)\n * keys (`vk_client`, `vk_ok_*`, `vk_original_vk_id`, `vk_are_pushes_enabled`)\n * come from the catalogue as optional — vk.com hosts never send them.\n */\nexport interface LaunchParams extends GetLaunchParamsResponse {\n\tvk_chat_id: string;\n\tvk_is_recommended: number;\n\tvk_profile_id: number;\n\tvk_has_profile_button: number;\n\tvk_testing_group_id: number;\n\todr_enabled: undefined | 1;\n}\n\n/**\n * Parses launch params out of a query string (à la\n * `location.search.slice(1)`), field by field, keeping only the known keys\n * with validated values.\n *\n * @see https://dev.vk.ru/mini-apps/development/launch-params\n */\nexport const parseURLSearchParamsForGetLaunchParams = (\n\tsearchParams: string,\n): Partial<LaunchParams> => {\n\tconst launchParams: Partial<LaunchParams> = {};\n\n\ttry {\n\t\tconst parsedSearchParams = new URLSearchParams(searchParams);\n\n\t\tconst convertToggleStateFromStringToNumber = (value: string): 0 | 1 | undefined => {\n\t\t\t// OK hosts serialize toggles both ways (\"true\"/\"false\" and \"0\"/\"1\")\n\t\t\tswitch (value) {\n\t\t\t\tcase \"0\":\n\t\t\t\tcase \"false\":\n\t\t\t\t\treturn 0;\n\t\t\t\tcase \"1\":\n\t\t\t\tcase \"true\":\n\t\t\t\t\treturn 1;\n\t\t\t\tdefault:\n\t\t\t\t\treturn undefined;\n\t\t\t}\n\t\t};\n\n\t\tparsedSearchParams.forEach((value, query) => {\n\t\t\tswitch (query) {\n\t\t\t\tcase \"vk_ts\":\n\t\t\t\tcase \"vk_is_recommended\":\n\t\t\t\tcase \"vk_profile_id\":\n\t\t\t\tcase \"vk_has_profile_button\":\n\t\t\t\tcase \"vk_testing_group_id\":\n\t\t\t\tcase \"vk_user_id\":\n\t\t\t\tcase \"vk_app_id\":\n\t\t\t\tcase \"vk_group_id\":\n\t\t\t\tcase \"vk_ok_app_id\":\n\t\t\t\tcase \"vk_ok_user_id\":\n\t\t\t\tcase \"vk_original_vk_id\":\n\t\t\t\t\tlaunchParams[query] = Number(value);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"sign\":\n\t\t\t\tcase \"vk_chat_id\":\n\t\t\t\tcase \"vk_ref\":\n\t\t\t\tcase \"vk_ok_ref\":\n\t\t\t\tcase \"vk_client\":\n\t\t\t\tcase \"vk_access_token_settings\":\n\t\t\t\t\tlaunchParams[query] = value;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"odr_enabled\":\n\t\t\t\t\tlaunchParams.odr_enabled = value === \"1\" ? 1 : undefined;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"vk_is_app_user\":\n\t\t\t\tcase \"vk_are_notifications_enabled\":\n\t\t\t\tcase \"vk_are_pushes_enabled\":\n\t\t\t\tcase \"vk_is_favorite\": {\n\t\t\t\t\tlaunchParams[query] = convertToggleStateFromStringToNumber(value);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"vk_language\": {\n\t\t\t\t\tlaunchParams.vk_language = LANGUAGES.has(value)\n\t\t\t\t\t\t? (value as Partial<LaunchParams>[\"vk_language\"])\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"vk_viewer_group_role\": {\n\t\t\t\t\tlaunchParams.vk_viewer_group_role = GROUP_ROLES.has(value)\n\t\t\t\t\t\t? (value as Partial<LaunchParams>[\"vk_viewer_group_role\"])\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"vk_platform\": {\n\t\t\t\t\tlaunchParams.vk_platform = PLATFORMS.has(value)\n\t\t\t\t\t\t? (value as Partial<LaunchParams>[\"vk_platform\"])\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t} catch (error) {\n\t\tconsole.warn(error);\n\t}\n\n\treturn launchParams;\n};\n"],"mappings":";AAQA,MAAM,4BAAY,IAAI,IAAI;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI,CAAC;AAE1E,MAAM,8BAAc,IAAI,IAAI;CAAC;CAAS;CAAU;CAAU;CAAS;AAAM,CAAC;AAE1E,MAAM,4BAAY,IAAI,IAAI;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD,CAAC;;;;;;;;AAwBD,MAAa,0CACZ,iBAC2B;CAC3B,MAAM,eAAsC,CAAC;CAE7C,IAAI;EACH,MAAM,qBAAqB,IAAI,gBAAgB,YAAY;EAE3D,MAAM,wCAAwC,UAAqC;GAElF,QAAQ,OAAR;IACC,KAAK;IACL,KAAK,SACJ,OAAO;IACR,KAAK;IACL,KAAK,QACJ,OAAO;IACR,SACC;GACF;EACD;EAEA,mBAAmB,SAAS,OAAO,UAAU;GAC5C,QAAQ,OAAR;IACC,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;KACJ,aAAa,SAAS,OAAO,KAAK;KAClC;IACD,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;KACJ,aAAa,SAAS;KACtB;IACD,KAAK;KACJ,aAAa,cAAc,UAAU,MAAM,IAAI,KAAA;KAC/C;IACD,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;KACJ,aAAa,SAAS,qCAAqC,KAAK;KAChE;IAED,KAAK;KACJ,aAAa,cAAc,UAAU,IAAI,KAAK,IAC1C,QACD,KAAA;KACH;IAED,KAAK;KACJ,aAAa,uBAAuB,YAAY,IAAI,KAAK,IACrD,QACD,KAAA;KACH;IAED,KAAK,eACJ,aAAa,cAAc,UAAU,IAAI,KAAK,IAC1C,QACD,KAAA;GAGL;EACD,CAAC;CACF,SAAS,OAAO;EACf,QAAQ,KAAK,KAAK;CACnB;CAEA,OAAO;AACR"}
package/package.json CHANGED
@@ -1,9 +1,28 @@
1
1
  {
2
2
  "name": "@vkma.dev/bridge",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Independent VK Bridge for VK Mini Apps — drop-in API, built-in devtools tap",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "author": "pajecawav",
8
+ "homepage": "https://github.com/pajecawav/vkma/tree/master/packages/bridge#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/pajecawav/vkma.git",
12
+ "directory": "packages/bridge"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/pajecawav/vkma/issues"
16
+ },
17
+ "keywords": [
18
+ "vk",
19
+ "vkontakte",
20
+ "vk-bridge",
21
+ "vk-mini-apps",
22
+ "mini-apps",
23
+ "bridge",
24
+ "typescript"
25
+ ],
7
26
  "publishConfig": {
8
27
  "access": "public"
9
28
  },
@@ -27,7 +46,7 @@
27
46
  "!src/**/*.test.tsx"
28
47
  ],
29
48
  "dependencies": {
30
- "@vkma.dev/bridge-schema": "^0.1.0"
49
+ "@vkma.dev/bridge-schema": "^0.2.0"
31
50
  },
32
51
  "devDependencies": {
33
52
  "happy-dom": "^21.0.4",
package/src/bridge.ts CHANGED
@@ -254,10 +254,15 @@ export function createVKBridge(version: string, options: VkmaBridgeOptions = {})
254
254
  const win = clientWindow();
255
255
  const parent = win?.parent;
256
256
  if (win && parent && typeof parent.postMessage === "function") {
257
+ // A hoisted handle, not a literal `parent.postMessage(...)` call:
258
+ // Facebook's Instant Games static analysis rejects builds that
259
+ // contain the literal substring (VKCOM/vk-bridge#290) — the same
260
+ // pattern as the `iosBridge`/`reactNative` handles above.
261
+ const webBridge = parent;
257
262
  // The mini app does not know the host origin ahead of time (vk.ru,
258
263
  // m.vk.ru, mobile clients) — "*" is the wire contract here.
259
264
  // oxlint-disable-next-line unicorn/require-post-message-target-origin
260
- parent.postMessage(
265
+ webBridge.postMessage(
261
266
  {
262
267
  handler: method,
263
268
  params: props,
@@ -14,18 +14,23 @@ const PLATFORMS = new Set([
14
14
  "desktop_web",
15
15
  "desktop_web_messenger",
16
16
  "desktop_app_messenger",
17
+ "desktop_web_ok",
17
18
  "mobile_web",
19
+ "mobile_web_ok",
18
20
  "mobile_android",
21
+ "mobile_android_ok",
19
22
  "mobile_android_messenger",
20
23
  "mobile_iphone",
24
+ "mobile_iphone_ok",
21
25
  "mobile_iphone_messenger",
22
26
  "mobile_ipad",
23
27
  ]);
24
28
 
25
29
  /**
26
30
  * Launch params as they appear in the URL — a superset of
27
- * `GetLaunchParamsResponse` with the keys the host only sends via the query
28
- * string.
31
+ * `GetLaunchParamsResponse` with the URL-only keys. The OK (Odnoklassniki)
32
+ * keys (`vk_client`, `vk_ok_*`, `vk_original_vk_id`, `vk_are_pushes_enabled`)
33
+ * come from the catalogue as optional — vk.com hosts never send them.
29
34
  */
30
35
  export interface LaunchParams extends GetLaunchParamsResponse {
31
36
  vk_chat_id: string;
@@ -52,10 +57,13 @@ export const parseURLSearchParamsForGetLaunchParams = (
52
57
  const parsedSearchParams = new URLSearchParams(searchParams);
53
58
 
54
59
  const convertToggleStateFromStringToNumber = (value: string): 0 | 1 | undefined => {
60
+ // OK hosts serialize toggles both ways ("true"/"false" and "0"/"1")
55
61
  switch (value) {
56
62
  case "0":
63
+ case "false":
57
64
  return 0;
58
65
  case "1":
66
+ case "true":
59
67
  return 1;
60
68
  default:
61
69
  return undefined;
@@ -72,11 +80,16 @@ export const parseURLSearchParamsForGetLaunchParams = (
72
80
  case "vk_user_id":
73
81
  case "vk_app_id":
74
82
  case "vk_group_id":
83
+ case "vk_ok_app_id":
84
+ case "vk_ok_user_id":
85
+ case "vk_original_vk_id":
75
86
  launchParams[query] = Number(value);
76
87
  break;
77
88
  case "sign":
78
89
  case "vk_chat_id":
79
90
  case "vk_ref":
91
+ case "vk_ok_ref":
92
+ case "vk_client":
80
93
  case "vk_access_token_settings":
81
94
  launchParams[query] = value;
82
95
  break;
@@ -85,6 +98,7 @@ export const parseURLSearchParamsForGetLaunchParams = (
85
98
  break;
86
99
  case "vk_is_app_user":
87
100
  case "vk_are_notifications_enabled":
101
+ case "vk_are_pushes_enabled":
88
102
  case "vk_is_favorite": {
89
103
  launchParams[query] = convertToggleStateFromStringToNumber(value);
90
104
  break;