@vkma.dev/bridge 0.0.0-stage → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@vkma.dev/bridge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Independent VK Bridge for VK Mini Apps — drop-in API, built-in devtools tap",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "engines": {
11
+ "node": ">=22"
12
+ },
13
+ "sideEffects": false,
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src",
26
+ "!src/**/*.test.ts",
27
+ "!src/**/*.test.tsx"
28
+ ],
29
+ "dependencies": {
30
+ "@vkma.dev/bridge-schema": "^0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "happy-dom": "^21.0.4",
34
+ "publint": "^0.3.25",
35
+ "tsdown": "^0.23.1",
36
+ "vitest": "^5.0.5"
37
+ },
38
+ "scripts": {
39
+ "build": "tsdown",
40
+ "dev": "tsdown --watch",
41
+ "lint:package": "publint",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest watch"
44
+ }
6
45
  }
package/src/bridge.ts ADDED
@@ -0,0 +1,594 @@
1
+ /**
2
+ * The bridge core: transport dispatch, subscription fan-out, promise pairing
3
+ * and the always-on tap.
4
+ *
5
+ * Transports (checked lazily on every call, unlike the load-time caches of
6
+ * the reference implementation — same in practice, testable in unit tests):
7
+ *
8
+ * - Android WebView: `window.AndroidBridge[method](JSON.stringify(props))`
9
+ * - iOS WebView: `window.webkit.messageHandlers[method].postMessage(props)`
10
+ * - React Native WebView: `window.ReactNativeWebView.postMessage(JSON)`
11
+ * - Web (iframe on vk.ru / m.vk.ru): `parent.postMessage` with the
12
+ * `type: "vk-connect"` envelope; receive side listens to `message`
13
+ * - WebViews receive via `CustomEvent("VKWebAppEvent")`
14
+ *
15
+ * The wire format is the platform interface — the host clients on the other
16
+ * side of these bridges define it, which is why the envelopes above are
17
+ * reproduced exactly.
18
+ */
19
+
20
+ import { createTap, type BridgeTap, type BridgeTapEvent } from "./tap.ts";
21
+ import type { SendOptions, VKBridgeSend, VKBridgeSubscribeHandler, VkmaBridge } from "./types.ts";
22
+
23
+ /** Methods supported on the desktop web hosts. */
24
+ const DESKTOP_METHODS = new Set([
25
+ "VKWebAppInit",
26
+ "VKWebAppGetCommunityAuthToken",
27
+ "VKWebAppAddToCommunity",
28
+ "VKWebAppAddToHomeScreenInfo",
29
+ "VKWebAppClose",
30
+ "VKWebAppCopyText",
31
+ "VKWebAppCreateHash",
32
+ "VKWebAppGetUserInfo",
33
+ "VKWebAppSetLocation",
34
+ "VKWebAppSendToClient",
35
+ "VKWebAppGetClientVersion",
36
+ "VKWebAppGetPhoneNumber",
37
+ "VKWebAppGetEmail",
38
+ "VKWebAppGetGroupInfo",
39
+ "VKWebAppGetGeodata",
40
+ "VKWebAppGetCommunityToken",
41
+ "VKWebAppGetConfig",
42
+ "VKWebAppGetLaunchParams",
43
+ "VKWebAppSetTitle",
44
+ "VKWebAppGetAuthToken",
45
+ "VKWebAppCallAPIMethod",
46
+ "VKWebAppJoinGroup",
47
+ "VKWebAppLeaveGroup",
48
+ "VKWebAppAllowMessagesFromGroup",
49
+ "VKWebAppDenyNotifications",
50
+ "VKWebAppAllowNotifications",
51
+ "VKWebAppOpenPayForm",
52
+ "VKWebAppOpenApp",
53
+ "VKWebAppShare",
54
+ "VKWebAppShowWallPostBox",
55
+ "VKWebAppScroll",
56
+ "VKWebAppShowOrderBox",
57
+ "VKWebAppShowLeaderBoardBox",
58
+ "VKWebAppShowInviteBox",
59
+ "VKWebAppShowRequestBox",
60
+ "VKWebAppAddToFavorites",
61
+ "VKWebAppShowStoryBox",
62
+ "VKWebAppStorageGet",
63
+ "VKWebAppStorageGetKeys",
64
+ "VKWebAppStorageSet",
65
+ "VKWebAppFlashGetInfo",
66
+ "VKWebAppSubscribeStoryApp",
67
+ "VKWebAppOpenWallPost",
68
+ "VKWebAppCheckAllowedScopes",
69
+ "VKWebAppCheckBannerAd",
70
+ "VKWebAppHideBannerAd",
71
+ "VKWebAppShowBannerAd",
72
+ "VKWebAppCheckNativeAds",
73
+ "VKWebAppShowNativeAds",
74
+ "VKWebAppRetargetingPixel",
75
+ "VKWebAppConversionHit",
76
+ "VKWebAppShowSubscriptionBox",
77
+ "VKWebAppCheckSurvey",
78
+ "VKWebAppShowSurvey",
79
+ "VKWebAppScrollTop",
80
+ "VKWebAppScrollTopStart",
81
+ "VKWebAppScrollTopStop",
82
+ "VKWebAppShowSlidesSheet",
83
+ "VKWebAppTranslate",
84
+ "VKWebAppRecommend",
85
+ "VKWebAppAddToProfile",
86
+ "VKWebAppGetFriends",
87
+ // Desktop web specific events
88
+ ...(isDesktopVk()
89
+ ? [
90
+ "VKWebAppResizeWindow",
91
+ "VKWebAppAddToMenu",
92
+ "VKWebAppShowInstallPushBox",
93
+ "VKWebAppShowCommunityWidgetPreviewBox",
94
+ "VKWebAppCallStart",
95
+ "VKWebAppCallJoin",
96
+ "VKWebAppCallGetStatus",
97
+ ]
98
+ : ["VKWebAppShowImages"]),
99
+ ]);
100
+
101
+ // ------------------------------------------------------------- environment
102
+
103
+ function clientWindow(): (Window & typeof globalThis) | undefined {
104
+ return typeof window === "undefined" ? undefined : window;
105
+ }
106
+
107
+ /**
108
+ * The native side of the host (AndroidBridge / webkit / ReactNativeWebView)
109
+ * lives on the window but is not part of any DOM typing — tests install
110
+ * fakes under the same keys.
111
+ */
112
+ function hostGlobal(): Record<string, unknown> | undefined {
113
+ const win = clientWindow();
114
+ return win === undefined ? undefined : (win as unknown as Record<string, unknown>);
115
+ }
116
+
117
+ export function isClientSide(): boolean {
118
+ return typeof window !== "undefined";
119
+ }
120
+
121
+ export function isAndroidWebview(): boolean {
122
+ return Boolean(hostGlobal()?.["AndroidBridge"]);
123
+ }
124
+
125
+ export function isIosWebview(): boolean {
126
+ const webkit = hostGlobal()?.["webkit"] as
127
+ | { messageHandlers?: Record<string, { postMessage?: unknown }> }
128
+ | undefined;
129
+ return Boolean(webkit?.messageHandlers?.["VKWebAppClose"]);
130
+ }
131
+
132
+ export function isReactNativeWebview(): boolean {
133
+ const reactNative = hostGlobal()?.["ReactNativeWebView"] as
134
+ | { postMessage?: unknown }
135
+ | undefined;
136
+ return Boolean(reactNative && typeof reactNative.postMessage === "function");
137
+ }
138
+
139
+ export function isWeb(): boolean {
140
+ return isClientSide() && !isAndroidWebview() && !isIosWebview();
141
+ }
142
+
143
+ export function isMvk(): boolean {
144
+ return (
145
+ isWeb() && /(^\?|&)vk_platform=mobile_web(&|$)/.test(clientWindow()?.location.search ?? "")
146
+ );
147
+ }
148
+
149
+ export function isDesktopVk(): boolean {
150
+ return isWeb() && !isMvk();
151
+ }
152
+
153
+ // -------------------------------------------------------------- the bridge
154
+
155
+ /**
156
+ * request_id policy of a bridge instance.
157
+ */
158
+ export interface RequestIdPolicy {
159
+ /**
160
+ * Own generator of outgoing request ids; receives an incrementing
161
+ * sequence. The default mints `__vkma_<n>` — the ids the devtools tap
162
+ * pairs responses by.
163
+ */
164
+ generator?: (sequence: number) => string | number;
165
+ /**
166
+ * Turn the auto-injection off: sends pass caller props untouched, so
167
+ * promise pairing only works for calls that provide their own
168
+ * `request_id`. Default: on.
169
+ */
170
+ inject?: boolean;
171
+ }
172
+
173
+ export interface VkmaBridgeOptions {
174
+ /** Outgoing request_id policy. */
175
+ requestId?: RequestIdPolicy;
176
+ /** Inject a shared tap (the default instance passes the page-wide one). */
177
+ tap?: BridgeTap;
178
+ /** Version string reported to the web host in the vk-connect envelope. */
179
+ version?: string;
180
+ }
181
+
182
+ interface PendingRequest {
183
+ resolve: (value: unknown) => void;
184
+ reject: (reason: unknown) => void;
185
+ tapId: number;
186
+ options: SendOptions;
187
+ cleanup: () => void;
188
+ }
189
+
190
+ /**
191
+ * Creates a VK Bridge instance: transport dispatch, subscription fan-out,
192
+ * promise pairing and the always-on tap.
193
+ *
194
+ * @param version Version of the package (reported to the web host).
195
+ * @param [options] Tap sharing and the request_id policy.
196
+ */
197
+ export function createVKBridge(version: string, options: VkmaBridgeOptions = {}): VkmaBridge {
198
+ const tap = options.tap ?? createTap();
199
+ const requestIdPolicy = options.requestId ?? {};
200
+
201
+ /** Current frame id, assigned by the host via VKWebAppSettings. */
202
+ let webFrameId: string | undefined;
203
+
204
+ /** Functions subscribed to bridge events. */
205
+ const subscribers: VKBridgeSubscribeHandler[] = [];
206
+
207
+ /** Pending promise controllers by outgoing request_id. */
208
+ const pendingRequests = new Map<string, PendingRequest>();
209
+
210
+ let requestCounter = 0;
211
+ let supportedHandlers: Set<string> | undefined;
212
+
213
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
214
+ typeof value === "object" && value !== null;
215
+
216
+ // ------------------------------------------------------------- transports
217
+
218
+ function sendThroughTransport(method: string, props: Record<string, unknown>): void {
219
+ const host = hostGlobal();
220
+ const android = host?.["AndroidBridge"] as
221
+ | Record<string, ((serializedData: string) => void) | undefined>
222
+ | undefined;
223
+ // Sending data through the Android bridge
224
+ if (typeof android?.[method] === "function") {
225
+ android[method](JSON.stringify(props));
226
+ return;
227
+ }
228
+
229
+ const iosHandlers = (
230
+ host?.["webkit"] as
231
+ | { messageHandlers?: Record<string, { postMessage?: unknown }> }
232
+ | undefined
233
+ )?.messageHandlers;
234
+ // Sending data through the iOS bridge
235
+ if (typeof iosHandlers?.[method]?.postMessage === "function") {
236
+ // Not a window.postMessage — the native handler takes no targetOrigin.
237
+ // oxlint-disable-next-line unicorn/require-post-message-target-origin
238
+ (iosHandlers[method].postMessage as (data: unknown) => void)(props);
239
+ return;
240
+ }
241
+
242
+ // Sending data through the React Native bridge
243
+ if (isReactNativeWebview()) {
244
+ const reactNative = host?.["ReactNativeWebView"] as {
245
+ postMessage: (data: string) => void;
246
+ };
247
+ // Not a window.postMessage — the RN handler takes no targetOrigin.
248
+ // oxlint-disable-next-line unicorn/require-post-message-target-origin
249
+ reactNative.postMessage(JSON.stringify({ handler: method, params: props }));
250
+ return;
251
+ }
252
+
253
+ // Sending data through the web bridge (the parent frame)
254
+ const win = clientWindow();
255
+ const parent = win?.parent;
256
+ if (win && parent && typeof parent.postMessage === "function") {
257
+ // The mini app does not know the host origin ahead of time (vk.ru,
258
+ // m.vk.ru, mobile clients) — "*" is the wire contract here.
259
+ // oxlint-disable-next-line unicorn/require-post-message-target-origin
260
+ parent.postMessage(
261
+ {
262
+ handler: method,
263
+ params: props,
264
+ type: "vk-connect",
265
+ webFrameId,
266
+ connectVersion: version,
267
+ },
268
+ "*",
269
+ );
270
+ }
271
+ }
272
+
273
+ // ------------------------------------------------------------- event intake
274
+
275
+ function handleEvent(event: unknown): void {
276
+ if (isIosWebview() || isAndroidWebview()) {
277
+ // WebViews: the native layer dispatches ready-made events
278
+ deliver(event as { detail?: { type?: unknown; data?: unknown } });
279
+ return;
280
+ }
281
+
282
+ let bridgeEventData = (event as { data?: unknown } | undefined)?.data;
283
+ if (!isWeb() || !bridgeEventData) {
284
+ return;
285
+ }
286
+
287
+ if (isReactNativeWebview() && typeof bridgeEventData === "string") {
288
+ try {
289
+ bridgeEventData = JSON.parse(bridgeEventData) as unknown;
290
+ } catch {}
291
+ }
292
+
293
+ if (!isPlainObject(bridgeEventData)) {
294
+ return;
295
+ }
296
+ const { type, data, frameId } = bridgeEventData as {
297
+ type?: unknown;
298
+ data?: unknown;
299
+ frameId?: unknown;
300
+ };
301
+ if (!type) {
302
+ return;
303
+ }
304
+
305
+ // The host assigns this iframe its frame id — internal protocol,
306
+ // not delivered to subscribers.
307
+ if (type === "VKWebAppSettings") {
308
+ webFrameId = typeof frameId === "string" ? frameId : webFrameId;
309
+ return;
310
+ }
311
+
312
+ deliver({ detail: { type, data } });
313
+ }
314
+
315
+ /**
316
+ * Upstream parity: every incoming event reaches `[...subscribers]` —
317
+ * paired responses included, with the raw wire data (upstream's
318
+ * promisifySend is itself a subscriber and strips request_id only for
319
+ * the promise). Snapshot iteration: a handler may unsubscribe mid-fan.
320
+ */
321
+ function fanOutToSubscribers(type: string, data: unknown): void {
322
+ // The wire type is stringly here by design — handleEvent dispatches
323
+ // arbitrary host envelopes; the public subscribe surface narrows it.
324
+ // The slice is a snapshot: a handler may unsubscribe mid-fan-out,
325
+ // and shifting live iteration would skip its neighbours (upstream
326
+ // copies the array for the same reason).
327
+ const out = { detail: { type, data } } as Parameters<VKBridgeSubscribeHandler>[0];
328
+ for (const listener of subscribers.slice()) {
329
+ listener(out);
330
+ }
331
+ }
332
+
333
+ /** Fan an incoming event out to the tap and to the subscribers. */
334
+ function deliver(event: { detail?: { type?: unknown; data?: unknown } }): void {
335
+ const { type, data } = event.detail ?? {};
336
+ if (typeof type !== "string") {
337
+ return;
338
+ }
339
+
340
+ // A payload carrying one of our pending request ids is the paired
341
+ // response to a send; everything else is an unsolicited event.
342
+ // The wire contract types request_id as a string or a number.
343
+ if (isPlainObject(data)) {
344
+ const wireRequestId = data["request_id"];
345
+ if (typeof wireRequestId !== "string" && typeof wireRequestId !== "number") {
346
+ tap.emit({ direction: "in", kind: "event", name: type, payload: data });
347
+ fanOutToSubscribers(type, data);
348
+ return;
349
+ }
350
+ const requestId = String(wireRequestId);
351
+ const pending = pendingRequests.get(requestId);
352
+ if (pending) {
353
+ pendingRequests.delete(requestId);
354
+ const { request_id: _stripped, ...payload } = data;
355
+ const ok = !("error_type" in data);
356
+ pending.cleanup();
357
+ tap.emit({
358
+ direction: "in",
359
+ kind: "response",
360
+ name: type,
361
+ payload,
362
+ reqId: pending.tapId,
363
+ ok,
364
+ });
365
+ if (ok) {
366
+ pending.resolve(payload);
367
+ } else {
368
+ pending.reject(payload);
369
+ }
370
+ // After the pairing listener's job (the promise is settled
371
+ // already, like upstream's first-registered pairing handler).
372
+ fanOutToSubscribers(type, data);
373
+ return;
374
+ }
375
+ }
376
+
377
+ tap.emit({ direction: "in", kind: "event", name: type, payload: data });
378
+ fanOutToSubscribers(type, data);
379
+ }
380
+
381
+ function subscribe(listener: VKBridgeSubscribeHandler): void {
382
+ subscribers.push(listener);
383
+ }
384
+
385
+ function unsubscribe(listener: VKBridgeSubscribeHandler): void {
386
+ const index = subscribers.indexOf(listener);
387
+ if (index > -1) {
388
+ subscribers.splice(index, 1);
389
+ }
390
+ }
391
+
392
+ // Listening for messages from the runtime env. React Native on Android
393
+ // dispatches on the document, everything else on the window.
394
+ if (isClientSide()) {
395
+ const win = clientWindow();
396
+ if (isReactNativeWebview() && /(android)/i.test(win?.navigator.userAgent ?? "")) {
397
+ document.addEventListener("message", handleEvent);
398
+ } else if (win && "addEventListener" in win) {
399
+ win.addEventListener(isWeb() ? "message" : "VKWebAppEvent", handleEvent);
400
+ }
401
+ }
402
+
403
+ // ------------------------------------------------------------ send + tap
404
+
405
+ const send: VKBridgeSend = ((
406
+ method: string,
407
+ props?: Record<string, unknown>,
408
+ sendOptions?: SendOptions,
409
+ ) => {
410
+ return new Promise((resolve, reject) => {
411
+ const signal = sendOptions?.signal;
412
+ // A pre-aborted signal rejects before anything happens: no wire
413
+ // send, no tap entry, no pending slot (the tap reflects reality).
414
+ if (signal?.aborted) {
415
+ const error = new Error(`VK Bridge send "${method}" aborted`);
416
+ error.name = "AbortError";
417
+ reject(error);
418
+ return;
419
+ }
420
+
421
+ const sourceProps: Record<string, unknown> = { ...props };
422
+ const explicitId =
423
+ typeof sourceProps["request_id"] === "string" ||
424
+ typeof sourceProps["request_id"] === "number"
425
+ ? sourceProps["request_id"]
426
+ : undefined;
427
+ let requestId: string | number | undefined;
428
+ if (explicitId !== undefined) {
429
+ requestId = explicitId;
430
+ } else if (requestIdPolicy.inject !== false) {
431
+ const sequence = ++requestCounter;
432
+ requestId = requestIdPolicy.generator
433
+ ? requestIdPolicy.generator(sequence)
434
+ : `__vkma_${sequence}`;
435
+ }
436
+
437
+ // The wire props are a separate object: the tap payload must not
438
+ // mutate under a listener that observed the emit.
439
+ const wireProps =
440
+ requestId === undefined ? sourceProps : { ...sourceProps, request_id: requestId };
441
+
442
+ const tapId = tap.emit({
443
+ direction: "out",
444
+ kind: "send",
445
+ name: method,
446
+ payload: wireProps,
447
+ });
448
+
449
+ let timer: ReturnType<typeof setTimeout> | undefined;
450
+ let signalHandler: (() => void) | undefined;
451
+
452
+ const cleanup = (): void => {
453
+ if (timer !== undefined) {
454
+ clearTimeout(timer);
455
+ }
456
+ if (signal && signalHandler) {
457
+ signal.removeEventListener("abort", signalHandler);
458
+ }
459
+ };
460
+
461
+ // Timeout and abort must fire even for unpaired sends (no
462
+ // pending slot to consult — e.g. inject: false without a caller
463
+ // request_id): the caller asked for a bounded call. For paired
464
+ // sends the pending entry doubles as the "already answered"
465
+ // guard against a double rejection.
466
+ const timeoutReject = (): void => {
467
+ if (requestId !== undefined && !pendingRequests.delete(String(requestId))) {
468
+ return;
469
+ }
470
+ cleanup();
471
+ const error = new Error(
472
+ `VK Bridge send "${method}" timed out after ${String(sendOptions?.timeout)}ms`,
473
+ );
474
+ error.name = "TimeoutError";
475
+ reject(error);
476
+ };
477
+
478
+ const abortReject = (): void => {
479
+ if (requestId !== undefined && !pendingRequests.delete(String(requestId))) {
480
+ return;
481
+ }
482
+ cleanup();
483
+ const error = new Error(`VK Bridge send "${method}" aborted`);
484
+ error.name = "AbortError";
485
+ reject(error);
486
+ };
487
+
488
+ if (requestId !== undefined) {
489
+ pendingRequests.set(String(requestId), {
490
+ resolve: resolve as (value: unknown) => void,
491
+ reject,
492
+ tapId,
493
+ options: sendOptions ?? {},
494
+ cleanup,
495
+ });
496
+ }
497
+
498
+ if (sendOptions?.timeout !== undefined) {
499
+ timer = setTimeout(timeoutReject, sendOptions.timeout);
500
+ }
501
+ if (signal) {
502
+ signalHandler = abortReject;
503
+ signal.addEventListener("abort", signalHandler);
504
+ }
505
+
506
+ sendThroughTransport(method, wireProps);
507
+ });
508
+ }) as VKBridgeSend;
509
+
510
+ // The cache behind supportsAsync, fed by both the SetSupportedHandlers
511
+ // response and the unsolicited pushes of the same name.
512
+ subscribe((event: { detail?: { type?: unknown; data?: unknown } }) => {
513
+ if (event.detail?.type === "SetSupportedHandlers" && isPlainObject(event.detail.data)) {
514
+ const handlers = (event.detail.data as { supportedHandlers?: unknown })
515
+ .supportedHandlers;
516
+ if (Array.isArray(handlers)) {
517
+ supportedHandlers = new Set(
518
+ handlers.filter((name): name is string => typeof name === "string"),
519
+ );
520
+ }
521
+ }
522
+ });
523
+
524
+ function supportsInner(method: string): boolean {
525
+ if (isAndroidWebview()) {
526
+ const android = hostGlobal()?.["AndroidBridge"] as
527
+ | Record<string, ((serializedData: string) => void) | undefined>
528
+ | undefined;
529
+ return typeof android?.[method] === "function";
530
+ }
531
+ if (isIosWebview()) {
532
+ const handlers = (
533
+ hostGlobal()?.["webkit"] as
534
+ | { messageHandlers?: Record<string, { postMessage?: unknown }> }
535
+ | undefined
536
+ )?.messageHandlers;
537
+ return typeof handlers?.[method]?.postMessage === "function";
538
+ }
539
+ if (isWeb()) {
540
+ return DESKTOP_METHODS.has(method);
541
+ }
542
+ return false;
543
+ }
544
+
545
+ const bridge: VkmaBridge = {
546
+ send,
547
+ sendPromise: send,
548
+ subscribe,
549
+ unsubscribe,
550
+ supports: (method: string) => {
551
+ console.warn("bridge.supports method is deprecated. Use bridge.supportsAsync instead.");
552
+ return supportsInner(method);
553
+ },
554
+ supportsAsync: async (method: string) => {
555
+ if (isAndroidWebview() || isIosWebview()) {
556
+ return supportsInner(method);
557
+ }
558
+ if (supportedHandlers) {
559
+ return supportedHandlers.has(method);
560
+ }
561
+ try {
562
+ const response = (await send("SetSupportedHandlers")) as {
563
+ supportedHandlers?: string[];
564
+ };
565
+ supportedHandlers = new Set(response.supportedHandlers ?? ["VKWebAppInit"]);
566
+ } catch {
567
+ supportedHandlers = new Set(["VKWebAppInit"]);
568
+ }
569
+ return supportedHandlers.has(method);
570
+ },
571
+ isWebView: () => isIosWebview() || isAndroidWebview(),
572
+ isIframe: () => {
573
+ const win = clientWindow();
574
+ return Boolean(win && win.parent !== win);
575
+ },
576
+ isEmbedded: () => bridge.isWebView() || bridge.isIframe(),
577
+ isStandalone: () => !bridge.isEmbedded(),
578
+ tap,
579
+ onSend: listener =>
580
+ tap.subscribe(event => {
581
+ if (event.direction === "out") {
582
+ listener(event as BridgeTapEvent & { direction: "out" });
583
+ }
584
+ }),
585
+ onEvent: listener =>
586
+ tap.subscribe(event => {
587
+ if (event.direction === "in") {
588
+ listener(event as BridgeTapEvent & { direction: "in" });
589
+ }
590
+ }),
591
+ };
592
+
593
+ return bridge;
594
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Pre-`Any*` type names, kept as aliases so older code keeps compiling.
3
+ */
4
+
5
+ import type { CommunityWidgetType } from "@vkma.dev/bridge-schema";
6
+ import type {
7
+ AnyIOMethodName,
8
+ AnyReceiveMethodName,
9
+ AnyReceiveOnlyMethodName,
10
+ AnyRequestMethodName,
11
+ AnyRequestOnlyMethodName,
12
+ } from "./types.ts";
13
+
14
+ /**
15
+ * Name of a method that can be sent.
16
+ * @alias AnyRequestMethodName
17
+ * @deprecated
18
+ */
19
+ export type RequestMethodName = AnyRequestMethodName;
20
+
21
+ /**
22
+ * Name of a method that can be received.
23
+ * @alias AnyReceiveMethodName
24
+ * @deprecated
25
+ */
26
+ export type ReceiveMethodName = AnyReceiveMethodName;
27
+
28
+ /**
29
+ * Name of a method that can be only sent.
30
+ * @alias AnyRequestOnlyMethodName
31
+ * @deprecated
32
+ */
33
+ export type RequestOnlyMethodName = AnyRequestOnlyMethodName;
34
+
35
+ /**
36
+ * Name of a method that can be only received.
37
+ * @alias AnyReceiveOnlyMethodName
38
+ * @deprecated
39
+ */
40
+ export type ReceiveOnlyMethodName = AnyReceiveOnlyMethodName;
41
+
42
+ /**
43
+ * Type of any method name.
44
+ * @alias AnyMethodName
45
+ * @deprecated
46
+ */
47
+ export type MethodName = AnyRequestMethodName | AnyReceiveMethodName;
48
+
49
+ /**
50
+ * The name of the method that can be both sent and received.
51
+ * @alias AnyIOMethodName
52
+ * @deprecated
53
+ */
54
+ export type IOMethodName = AnyIOMethodName;
55
+
56
+ /**
57
+ * Community widget type
58
+ * @alias CommunityWidgetType
59
+ * @deprecated
60
+ */
61
+ export type WidgetType = CommunityWidgetType;