@flighthq/protocol 0.1.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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +22 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +293 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +38 -0
- package/src/protocol.test.ts +492 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ParsedProtocolUrl, ProtocolBackend, ProtocolHandler } from '@flighthq/types';
|
|
2
|
+
export declare function attachProtocolHandler(handler: ProtocolHandler): void;
|
|
3
|
+
export declare function createProtocolHandler(): ProtocolHandler;
|
|
4
|
+
export declare function createProtocolUrl(parts: Readonly<Partial<ParsedProtocolUrl>>): string;
|
|
5
|
+
export declare function createWebProtocolBackend(): ProtocolBackend;
|
|
6
|
+
export declare function detachProtocolHandler(handler: ProtocolHandler): void;
|
|
7
|
+
export declare function disposeProtocolHandler(handler: ProtocolHandler): void;
|
|
8
|
+
export declare function getProtocolBackend(): ProtocolBackend;
|
|
9
|
+
export declare function getProtocolLaunchUrl(): string | null;
|
|
10
|
+
export declare function getRegisteredProtocolSchemes(): readonly string[];
|
|
11
|
+
export declare function isProtocolSchemeDefault(scheme: string): boolean;
|
|
12
|
+
export declare function isProtocolSchemeRegistered(scheme: string): boolean;
|
|
13
|
+
export declare function isValidProtocolScheme(scheme: string): boolean;
|
|
14
|
+
export declare function parseProtocolUrl(url: string): ParsedProtocolUrl | null;
|
|
15
|
+
export declare function registerProtocolScheme(scheme: string): boolean;
|
|
16
|
+
export declare function registerProtocolSchemes(schemes: readonly string[]): boolean;
|
|
17
|
+
export declare function removeProtocolSchemeAsDefault(scheme: string): boolean;
|
|
18
|
+
export declare function setProtocolBackend(backend: ProtocolBackend | null): void;
|
|
19
|
+
export declare function setProtocolSchemeAsDefault(scheme: string): boolean;
|
|
20
|
+
export declare function unregisterProtocolScheme(scheme: string): boolean;
|
|
21
|
+
export declare function unregisterProtocolSchemes(schemes: readonly string[]): boolean;
|
|
22
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAO3F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAUpE;AAID,wBAAgB,qBAAqB,IAAI,eAAe,CAEvD;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,GAAG,MAAM,CAgBrF;AAID,wBAAgB,wBAAwB,IAAI,eAAe,CAiE1D;AAGD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAMpE;AAID,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAErE;AAGD,wBAAgB,kBAAkB,IAAI,eAAe,CAGpD;AAKD,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAEpD;AAID,wBAAgB,4BAA4B,IAAI,SAAS,MAAM,EAAE,CAEhE;AAID,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE/D;AAGD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElE;AAMD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK7D;AAMD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAuDtE;AAKD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAG9D;AAID,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAO3E;AAID,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErE;AAGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI,CAExE;AAID,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElE;AAID,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEhE;AAGD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAO7E"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { createSignal, emitSignal } from '@flighthq/signals';
|
|
2
|
+
// Begins delivering deep-link opens to `handler`'s signal by subscribing to the active backend. Wires
|
|
3
|
+
// subscribe→onOpenUrl. Idempotent: a prior subscription is torn down first. Drains any URLs that
|
|
4
|
+
// arrived before this call (buffered by the backend between process start and first attach) and
|
|
5
|
+
// emits each one before activating the live subscription. Pair with
|
|
6
|
+
// detachProtocolHandler/disposeProtocolHandler.
|
|
7
|
+
export function attachProtocolHandler(handler) {
|
|
8
|
+
detachProtocolHandler(handler);
|
|
9
|
+
const backend = getProtocolBackend();
|
|
10
|
+
// Drain URLs that arrived between process start and first attach (pre-attach burst).
|
|
11
|
+
const pending = backend.drainPendingUrls();
|
|
12
|
+
for (const url of pending) {
|
|
13
|
+
emitSignal(handler.onOpenUrl, url);
|
|
14
|
+
}
|
|
15
|
+
const unsubscribe = backend.subscribe((url) => emitSignal(handler.onOpenUrl, url));
|
|
16
|
+
_subscriptions.set(handler, unsubscribe);
|
|
17
|
+
}
|
|
18
|
+
// Allocates a ProtocolHandler event entity with an inert signal; call attachProtocolHandler to start
|
|
19
|
+
// delivery.
|
|
20
|
+
export function createProtocolHandler() {
|
|
21
|
+
return { onOpenUrl: createSignal() };
|
|
22
|
+
}
|
|
23
|
+
// Builds a protocol URL string from its parsed components. scheme, host, path, and query are all
|
|
24
|
+
// optional; the result is always a valid absolute URI string. Round-trips with parseProtocolUrl for
|
|
25
|
+
// well-formed inputs.
|
|
26
|
+
export function createProtocolUrl(parts) {
|
|
27
|
+
const scheme = parts.scheme ?? 'unknown';
|
|
28
|
+
const host = parts.host ?? '';
|
|
29
|
+
const path = parts.path ?? '';
|
|
30
|
+
const query = parts.query;
|
|
31
|
+
const authority = host ? `//${host}` : '';
|
|
32
|
+
const normalizedPath = path && !path.startsWith('/') ? `/${path}` : path;
|
|
33
|
+
let url = `${scheme}:${authority}${normalizedPath}`;
|
|
34
|
+
if (query) {
|
|
35
|
+
const entries = Object.entries(query).filter(([k]) => k.length > 0);
|
|
36
|
+
if (entries.length > 0) {
|
|
37
|
+
const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
|
|
38
|
+
url += `?${qs}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return url;
|
|
42
|
+
}
|
|
43
|
+
// Builds the default web backend over navigator.registerProtocolHandler. Registration degrades to
|
|
44
|
+
// false where the API is absent. Deep-link delivery needs a native host, so subscribe is inert.
|
|
45
|
+
export function createWebProtocolBackend() {
|
|
46
|
+
const _registeredSchemes = [];
|
|
47
|
+
return {
|
|
48
|
+
register(scheme) {
|
|
49
|
+
if (typeof navigator === 'undefined' || typeof location === 'undefined')
|
|
50
|
+
return false;
|
|
51
|
+
const nav = navigator;
|
|
52
|
+
if (typeof nav.registerProtocolHandler !== 'function')
|
|
53
|
+
return false;
|
|
54
|
+
try {
|
|
55
|
+
nav.registerProtocolHandler(scheme, location.origin + '/?url=%s');
|
|
56
|
+
if (!_registeredSchemes.includes(scheme))
|
|
57
|
+
_registeredSchemes.push(scheme);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
unregister(scheme) {
|
|
65
|
+
// The web platform offers no programmatic unregister; report failure rather than throw.
|
|
66
|
+
const idx = _registeredSchemes.indexOf(scheme);
|
|
67
|
+
if (idx >= 0)
|
|
68
|
+
_registeredSchemes.splice(idx, 1);
|
|
69
|
+
return false;
|
|
70
|
+
},
|
|
71
|
+
isRegistered() {
|
|
72
|
+
// The web platform offers no registration query; report not-registered.
|
|
73
|
+
return false;
|
|
74
|
+
},
|
|
75
|
+
getRegisteredSchemes() {
|
|
76
|
+
// Web cannot enumerate registered schemes reliably; return what we tracked locally.
|
|
77
|
+
return _registeredSchemes.slice();
|
|
78
|
+
},
|
|
79
|
+
setAsDefault() {
|
|
80
|
+
// The web platform cannot claim a scheme as the OS default; report failure.
|
|
81
|
+
return false;
|
|
82
|
+
},
|
|
83
|
+
isDefault() {
|
|
84
|
+
// The web platform cannot query OS default handler; report false.
|
|
85
|
+
return false;
|
|
86
|
+
},
|
|
87
|
+
removeAsDefault() {
|
|
88
|
+
// The web platform cannot remove an OS default handler; report failure.
|
|
89
|
+
return false;
|
|
90
|
+
},
|
|
91
|
+
getLaunchUrl() {
|
|
92
|
+
// On the web the registered handler redirects to /?url=<encoded-url>. Read that query param as
|
|
93
|
+
// the cold-start launch URL when present.
|
|
94
|
+
if (typeof location === 'undefined')
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
const params = new URLSearchParams(location.search);
|
|
98
|
+
const url = params.get('url');
|
|
99
|
+
return url && url.length > 0 ? url : null;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
drainPendingUrls() {
|
|
106
|
+
// The web backend has no pre-attach buffering; the cold-start URL is handled by getLaunchUrl.
|
|
107
|
+
return [];
|
|
108
|
+
},
|
|
109
|
+
subscribe() {
|
|
110
|
+
// Web deep-link delivery requires a native host to route incoming URLs into the page; the web
|
|
111
|
+
// backend cannot observe protocol opens on its own, so this subscription is inert.
|
|
112
|
+
return () => { };
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Stops delivery to `handler` and forgets its subscription. Safe to call when not attached.
|
|
117
|
+
export function detachProtocolHandler(handler) {
|
|
118
|
+
const unsubscribe = _subscriptions.get(handler);
|
|
119
|
+
if (unsubscribe !== undefined) {
|
|
120
|
+
unsubscribe();
|
|
121
|
+
_subscriptions.delete(handler);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Releases `handler` for garbage collection by detaching its backend subscription. The signal remains
|
|
125
|
+
// plain GC-managed memory afterward.
|
|
126
|
+
export function disposeProtocolHandler(handler) {
|
|
127
|
+
detachProtocolHandler(handler);
|
|
128
|
+
}
|
|
129
|
+
// The active protocol backend, or a lazily-created web default. There is always a backend.
|
|
130
|
+
export function getProtocolBackend() {
|
|
131
|
+
if (_backend === null)
|
|
132
|
+
_backend = createWebProtocolBackend();
|
|
133
|
+
return _backend;
|
|
134
|
+
}
|
|
135
|
+
// Returns the URL the app was launched with via a deep link (cold start), or null when the app was
|
|
136
|
+
// not launched via a link. This is a one-shot idempotent query — re-readable. Distinct from
|
|
137
|
+
// onOpenUrl, which fires only for warm (subsequent) opens.
|
|
138
|
+
export function getProtocolLaunchUrl() {
|
|
139
|
+
return getProtocolBackend().getLaunchUrl();
|
|
140
|
+
}
|
|
141
|
+
// Returns all custom URI schemes currently registered by this app. Returns [] when the host cannot
|
|
142
|
+
// enumerate registered schemes.
|
|
143
|
+
export function getRegisteredProtocolSchemes() {
|
|
144
|
+
return getProtocolBackend().getRegisteredSchemes();
|
|
145
|
+
}
|
|
146
|
+
// True when `scheme` is the OS default handler for deep links. Returns false where the host cannot
|
|
147
|
+
// report it (e.g. the web platform).
|
|
148
|
+
export function isProtocolSchemeDefault(scheme) {
|
|
149
|
+
return getProtocolBackend().isDefault(scheme);
|
|
150
|
+
}
|
|
151
|
+
// True when `scheme` is currently registered to this app. Returns false where the host cannot report it.
|
|
152
|
+
export function isProtocolSchemeRegistered(scheme) {
|
|
153
|
+
return getProtocolBackend().isRegistered(scheme);
|
|
154
|
+
}
|
|
155
|
+
// Returns true when `scheme` is a valid RFC 3986 URI scheme: starts with an ASCII letter followed by
|
|
156
|
+
// zero or more letters, digits, '+', '-', or '.'. Reserved schemes ('http', 'https', 'ftp', 'ftps',
|
|
157
|
+
// 'mailto', 'file') are rejected to prevent OS conflicts. Scheme is lowercased before validation so
|
|
158
|
+
// 'MyApp' is treated identically to 'myapp'.
|
|
159
|
+
export function isValidProtocolScheme(scheme) {
|
|
160
|
+
if (typeof scheme !== 'string' || scheme.length === 0)
|
|
161
|
+
return false;
|
|
162
|
+
const lower = scheme.toLowerCase();
|
|
163
|
+
if (_reservedSchemes.has(lower))
|
|
164
|
+
return false;
|
|
165
|
+
return _schemePattern.test(lower);
|
|
166
|
+
}
|
|
167
|
+
// Parses a deep-link URL into its components. Returns null for malformed or non-custom-scheme URLs.
|
|
168
|
+
// query values are percent-decoded. Multi-value query keys: last value wins (matches URLSearchParams
|
|
169
|
+
// behavior). This function does not allocate a large object on the happy path — it reads all fields
|
|
170
|
+
// before writing to avoid aliasing issues if the caller reuses a buffer.
|
|
171
|
+
export function parseProtocolUrl(url) {
|
|
172
|
+
if (typeof url !== 'string' || url.length === 0)
|
|
173
|
+
return null;
|
|
174
|
+
const colonIdx = url.indexOf(':');
|
|
175
|
+
if (colonIdx <= 0)
|
|
176
|
+
return null;
|
|
177
|
+
const scheme = url.slice(0, colonIdx).toLowerCase();
|
|
178
|
+
if (!_schemePattern.test(scheme))
|
|
179
|
+
return null;
|
|
180
|
+
let rest = url.slice(colonIdx + 1);
|
|
181
|
+
// Authority (//host)
|
|
182
|
+
let host = '';
|
|
183
|
+
if (rest.startsWith('//')) {
|
|
184
|
+
rest = rest.slice(2);
|
|
185
|
+
const slashIdx = rest.indexOf('/');
|
|
186
|
+
const qIdx = rest.indexOf('?');
|
|
187
|
+
let hostEnd;
|
|
188
|
+
if (slashIdx >= 0 && (qIdx < 0 || slashIdx < qIdx)) {
|
|
189
|
+
hostEnd = slashIdx;
|
|
190
|
+
}
|
|
191
|
+
else if (qIdx >= 0) {
|
|
192
|
+
hostEnd = qIdx;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
hostEnd = rest.length;
|
|
196
|
+
}
|
|
197
|
+
host = rest.slice(0, hostEnd);
|
|
198
|
+
rest = rest.slice(hostEnd);
|
|
199
|
+
}
|
|
200
|
+
// Path and query
|
|
201
|
+
const qIdx = rest.indexOf('?');
|
|
202
|
+
let path;
|
|
203
|
+
let queryString;
|
|
204
|
+
if (qIdx >= 0) {
|
|
205
|
+
path = rest.slice(0, qIdx);
|
|
206
|
+
queryString = rest.slice(qIdx + 1);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
path = rest;
|
|
210
|
+
queryString = '';
|
|
211
|
+
}
|
|
212
|
+
// Decode query
|
|
213
|
+
const query = {};
|
|
214
|
+
if (queryString.length > 0) {
|
|
215
|
+
for (const pair of queryString.split('&')) {
|
|
216
|
+
const eqIdx = pair.indexOf('=');
|
|
217
|
+
if (eqIdx < 0) {
|
|
218
|
+
const k = _safeDecode(pair);
|
|
219
|
+
if (k.length > 0)
|
|
220
|
+
query[k] = '';
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const k = _safeDecode(pair.slice(0, eqIdx));
|
|
224
|
+
if (k.length > 0)
|
|
225
|
+
query[k] = _safeDecode(pair.slice(eqIdx + 1));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { scheme, host, path, query };
|
|
230
|
+
}
|
|
231
|
+
// Registers a custom URI scheme (for example 'myapp') to this app. Returns false when the scheme is
|
|
232
|
+
// invalid (RFC 3986 grammar, no reserved schemes) or when the host denies or does not support
|
|
233
|
+
// registration.
|
|
234
|
+
export function registerProtocolScheme(scheme) {
|
|
235
|
+
if (!isValidProtocolScheme(scheme))
|
|
236
|
+
return false;
|
|
237
|
+
return getProtocolBackend().register(scheme);
|
|
238
|
+
}
|
|
239
|
+
// Registers multiple custom URI schemes in one call. Returns false if any scheme is invalid or if any
|
|
240
|
+
// registration fails.
|
|
241
|
+
export function registerProtocolSchemes(schemes) {
|
|
242
|
+
const backend = getProtocolBackend();
|
|
243
|
+
let allOk = true;
|
|
244
|
+
for (const scheme of schemes) {
|
|
245
|
+
if (!isValidProtocolScheme(scheme) || !backend.register(scheme))
|
|
246
|
+
allOk = false;
|
|
247
|
+
}
|
|
248
|
+
return allOk;
|
|
249
|
+
}
|
|
250
|
+
// Removes this app as the OS default handler for `scheme`. Returns false when the host denies or does
|
|
251
|
+
// not support it.
|
|
252
|
+
export function removeProtocolSchemeAsDefault(scheme) {
|
|
253
|
+
return getProtocolBackend().removeAsDefault(scheme);
|
|
254
|
+
}
|
|
255
|
+
// Installs a native host protocol backend; pass null to fall back to the web default.
|
|
256
|
+
export function setProtocolBackend(backend) {
|
|
257
|
+
_backend = backend;
|
|
258
|
+
}
|
|
259
|
+
// Makes this app the default handler for `scheme`. Returns false when the host denies or does not
|
|
260
|
+
// support it.
|
|
261
|
+
export function setProtocolSchemeAsDefault(scheme) {
|
|
262
|
+
return getProtocolBackend().setAsDefault(scheme);
|
|
263
|
+
}
|
|
264
|
+
// Unregisters a previously registered custom URI scheme. Returns false when the host denies or does
|
|
265
|
+
// not support unregistration.
|
|
266
|
+
export function unregisterProtocolScheme(scheme) {
|
|
267
|
+
return getProtocolBackend().unregister(scheme);
|
|
268
|
+
}
|
|
269
|
+
// Unregisters multiple custom URI schemes in one call. Returns false if any unregistration fails.
|
|
270
|
+
export function unregisterProtocolSchemes(schemes) {
|
|
271
|
+
const backend = getProtocolBackend();
|
|
272
|
+
let allOk = true;
|
|
273
|
+
for (const scheme of schemes) {
|
|
274
|
+
if (!backend.unregister(scheme))
|
|
275
|
+
allOk = false;
|
|
276
|
+
}
|
|
277
|
+
return allOk;
|
|
278
|
+
}
|
|
279
|
+
// RFC 3986 scheme grammar: letter followed by zero or more letter/digit/+/-/. (lowercased).
|
|
280
|
+
const _schemePattern = /^[a-z][a-z0-9+\-.]*$/;
|
|
281
|
+
// Schemes that should never be claimed as custom URI handlers to avoid OS conflicts.
|
|
282
|
+
const _reservedSchemes = new Set(['file', 'ftp', 'ftps', 'http', 'https', 'mailto']);
|
|
283
|
+
let _backend = null;
|
|
284
|
+
const _subscriptions = new WeakMap();
|
|
285
|
+
function _safeDecode(s) {
|
|
286
|
+
try {
|
|
287
|
+
return decodeURIComponent(s.replace(/\+/g, ' '));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return s;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/signals": "0.1.0",
|
|
31
|
+
"@flighthq/types": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"description": "Custom URI scheme registration and deep-link open-URL signals over a swappable web/native backend",
|
|
37
|
+
"sideEffects": false
|
|
38
|
+
}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { connectSignal } from '@flighthq/signals';
|
|
2
|
+
import type { ProtocolBackend } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
attachProtocolHandler,
|
|
6
|
+
createProtocolHandler,
|
|
7
|
+
createProtocolUrl,
|
|
8
|
+
createWebProtocolBackend,
|
|
9
|
+
detachProtocolHandler,
|
|
10
|
+
disposeProtocolHandler,
|
|
11
|
+
getProtocolBackend,
|
|
12
|
+
getProtocolLaunchUrl,
|
|
13
|
+
getRegisteredProtocolSchemes,
|
|
14
|
+
isProtocolSchemeDefault,
|
|
15
|
+
isProtocolSchemeRegistered,
|
|
16
|
+
isValidProtocolScheme,
|
|
17
|
+
parseProtocolUrl,
|
|
18
|
+
registerProtocolScheme,
|
|
19
|
+
registerProtocolSchemes,
|
|
20
|
+
removeProtocolSchemeAsDefault,
|
|
21
|
+
setProtocolBackend,
|
|
22
|
+
setProtocolSchemeAsDefault,
|
|
23
|
+
unregisterProtocolScheme,
|
|
24
|
+
unregisterProtocolSchemes,
|
|
25
|
+
} from './protocol';
|
|
26
|
+
|
|
27
|
+
function fakeBackend(): ProtocolBackend & {
|
|
28
|
+
registered: boolean;
|
|
29
|
+
defaulted: boolean;
|
|
30
|
+
launchUrl: string | null;
|
|
31
|
+
pendingUrls: string[];
|
|
32
|
+
fire: (url: string) => void;
|
|
33
|
+
} {
|
|
34
|
+
let listener: ((url: string) => void) | null = null;
|
|
35
|
+
const registeredSchemes: string[] = [];
|
|
36
|
+
return {
|
|
37
|
+
registered: false,
|
|
38
|
+
defaulted: false,
|
|
39
|
+
launchUrl: null,
|
|
40
|
+
pendingUrls: [],
|
|
41
|
+
register(_scheme) {
|
|
42
|
+
this.registered = true;
|
|
43
|
+
if (!registeredSchemes.includes(_scheme)) registeredSchemes.push(_scheme);
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
unregister(_scheme) {
|
|
47
|
+
this.registered = false;
|
|
48
|
+
const idx = registeredSchemes.indexOf(_scheme);
|
|
49
|
+
if (idx >= 0) registeredSchemes.splice(idx, 1);
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
isRegistered(_scheme) {
|
|
53
|
+
return this.registered;
|
|
54
|
+
},
|
|
55
|
+
getRegisteredSchemes() {
|
|
56
|
+
return registeredSchemes.slice();
|
|
57
|
+
},
|
|
58
|
+
setAsDefault(_scheme) {
|
|
59
|
+
this.defaulted = true;
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
isDefault(_scheme) {
|
|
63
|
+
return this.defaulted;
|
|
64
|
+
},
|
|
65
|
+
removeAsDefault(_scheme) {
|
|
66
|
+
this.defaulted = false;
|
|
67
|
+
return true;
|
|
68
|
+
},
|
|
69
|
+
getLaunchUrl() {
|
|
70
|
+
return this.launchUrl;
|
|
71
|
+
},
|
|
72
|
+
drainPendingUrls() {
|
|
73
|
+
const drained = this.pendingUrls.slice();
|
|
74
|
+
this.pendingUrls = [];
|
|
75
|
+
return drained;
|
|
76
|
+
},
|
|
77
|
+
subscribe(l) {
|
|
78
|
+
listener = l;
|
|
79
|
+
return () => {
|
|
80
|
+
listener = null;
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
fire(url) {
|
|
84
|
+
listener?.(url);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
afterEach(() => setProtocolBackend(null));
|
|
90
|
+
|
|
91
|
+
describe('attachProtocolHandler', () => {
|
|
92
|
+
it('drains pre-attach pending URLs before subscribing', () => {
|
|
93
|
+
const backend = fakeBackend();
|
|
94
|
+
backend.pendingUrls = ['myapp://early/1', 'myapp://early/2'];
|
|
95
|
+
setProtocolBackend(backend);
|
|
96
|
+
const handler = createProtocolHandler();
|
|
97
|
+
const received: string[] = [];
|
|
98
|
+
connectSignal(handler.onOpenUrl, (url) => received.push(url));
|
|
99
|
+
attachProtocolHandler(handler);
|
|
100
|
+
expect(received).toEqual(['myapp://early/1', 'myapp://early/2']);
|
|
101
|
+
// Pending queue is cleared — a second attach does not re-deliver them.
|
|
102
|
+
attachProtocolHandler(handler);
|
|
103
|
+
expect(received).toEqual(['myapp://early/1', 'myapp://early/2']);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('emits onOpenUrl when the backend delivers a deep link', () => {
|
|
107
|
+
const backend = fakeBackend();
|
|
108
|
+
setProtocolBackend(backend);
|
|
109
|
+
const handler = createProtocolHandler();
|
|
110
|
+
let opened = '';
|
|
111
|
+
connectSignal(handler.onOpenUrl, (url) => (opened = url));
|
|
112
|
+
attachProtocolHandler(handler);
|
|
113
|
+
backend.fire('myapp://open/123');
|
|
114
|
+
expect(opened).toBe('myapp://open/123');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('is idempotent — reattaching does not double-fire', () => {
|
|
118
|
+
const backend = fakeBackend();
|
|
119
|
+
setProtocolBackend(backend);
|
|
120
|
+
const handler = createProtocolHandler();
|
|
121
|
+
let count = 0;
|
|
122
|
+
connectSignal(handler.onOpenUrl, () => count++);
|
|
123
|
+
attachProtocolHandler(handler);
|
|
124
|
+
attachProtocolHandler(handler);
|
|
125
|
+
backend.fire('myapp://x');
|
|
126
|
+
expect(count).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('createProtocolHandler', () => {
|
|
131
|
+
it('creates an entity with the open-URL signal', () => {
|
|
132
|
+
const handler = createProtocolHandler();
|
|
133
|
+
expect(handler.onOpenUrl).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('createProtocolUrl', () => {
|
|
138
|
+
it('builds a basic URL from all parts', () => {
|
|
139
|
+
const url = createProtocolUrl({ scheme: 'myapp', host: 'open', path: '/item/1', query: { ref: 'home' } });
|
|
140
|
+
expect(url).toBe('myapp://open/item/1?ref=home');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('omits authority when host is empty', () => {
|
|
144
|
+
const url = createProtocolUrl({ scheme: 'myapp', path: '/action' });
|
|
145
|
+
expect(url).toBe('myapp:/action');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('omits query when no query params', () => {
|
|
149
|
+
const url = createProtocolUrl({ scheme: 'myapp', host: 'x' });
|
|
150
|
+
expect(url).toBe('myapp://x');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('percent-encodes query keys and values', () => {
|
|
154
|
+
const url = createProtocolUrl({ scheme: 'myapp', query: { 'a b': 'c d' } });
|
|
155
|
+
expect(url).toContain('a%20b=c%20d');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('round-trips with parseProtocolUrl', () => {
|
|
159
|
+
const parts = { scheme: 'myapp', host: 'host', path: '/path', query: { k: 'v' } };
|
|
160
|
+
const url = createProtocolUrl(parts);
|
|
161
|
+
const parsed = parseProtocolUrl(url);
|
|
162
|
+
expect(parsed?.scheme).toBe('myapp');
|
|
163
|
+
expect(parsed?.host).toBe('host');
|
|
164
|
+
expect(parsed?.path).toBe('/path');
|
|
165
|
+
expect(parsed?.query['k']).toBe('v');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('createWebProtocolBackend', () => {
|
|
170
|
+
it('reports a subscribe function and non-throwing queries', () => {
|
|
171
|
+
const backend = createWebProtocolBackend();
|
|
172
|
+
expect(backend.subscribe(() => {})).toBeInstanceOf(Function);
|
|
173
|
+
expect(typeof backend.isRegistered('myapp')).toBe('boolean');
|
|
174
|
+
expect(backend.setAsDefault('myapp')).toBe(false);
|
|
175
|
+
expect(backend.isDefault('myapp')).toBe(false);
|
|
176
|
+
expect(backend.removeAsDefault('myapp')).toBe(false);
|
|
177
|
+
expect(backend.getLaunchUrl()).toBeNull();
|
|
178
|
+
expect(backend.getRegisteredSchemes()).toBeInstanceOf(Array);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('detachProtocolHandler', () => {
|
|
183
|
+
it('stops further delivery', () => {
|
|
184
|
+
const backend = fakeBackend();
|
|
185
|
+
setProtocolBackend(backend);
|
|
186
|
+
const handler = createProtocolHandler();
|
|
187
|
+
let opened = 0;
|
|
188
|
+
connectSignal(handler.onOpenUrl, () => opened++);
|
|
189
|
+
attachProtocolHandler(handler);
|
|
190
|
+
detachProtocolHandler(handler);
|
|
191
|
+
backend.fire('myapp://x');
|
|
192
|
+
expect(opened).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('is safe when not attached', () => {
|
|
196
|
+
const handler = createProtocolHandler();
|
|
197
|
+
expect(() => detachProtocolHandler(handler)).not.toThrow();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('disposeProtocolHandler', () => {
|
|
202
|
+
it('detaches the subscription', () => {
|
|
203
|
+
const backend = fakeBackend();
|
|
204
|
+
setProtocolBackend(backend);
|
|
205
|
+
const handler = createProtocolHandler();
|
|
206
|
+
attachProtocolHandler(handler);
|
|
207
|
+
expect(() => disposeProtocolHandler(handler)).not.toThrow();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('stops delivery after dispose', () => {
|
|
211
|
+
const backend = fakeBackend();
|
|
212
|
+
setProtocolBackend(backend);
|
|
213
|
+
const handler = createProtocolHandler();
|
|
214
|
+
let count = 0;
|
|
215
|
+
connectSignal(handler.onOpenUrl, () => count++);
|
|
216
|
+
attachProtocolHandler(handler);
|
|
217
|
+
disposeProtocolHandler(handler);
|
|
218
|
+
backend.fire('myapp://x');
|
|
219
|
+
expect(count).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('getProtocolBackend', () => {
|
|
224
|
+
it('falls back to a web backend', () => {
|
|
225
|
+
expect(getProtocolBackend()).not.toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('returns the installed backend', () => {
|
|
229
|
+
const backend = fakeBackend();
|
|
230
|
+
setProtocolBackend(backend);
|
|
231
|
+
expect(getProtocolBackend()).toBe(backend);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('getProtocolLaunchUrl', () => {
|
|
236
|
+
it('returns null when no launch URL', () => {
|
|
237
|
+
const backend = fakeBackend();
|
|
238
|
+
backend.launchUrl = null;
|
|
239
|
+
setProtocolBackend(backend);
|
|
240
|
+
expect(getProtocolLaunchUrl()).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('returns the launch URL when present', () => {
|
|
244
|
+
const backend = fakeBackend();
|
|
245
|
+
backend.launchUrl = 'myapp://cold-start/item';
|
|
246
|
+
setProtocolBackend(backend);
|
|
247
|
+
expect(getProtocolLaunchUrl()).toBe('myapp://cold-start/item');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('is idempotent — returns the same value on repeated calls', () => {
|
|
251
|
+
const backend = fakeBackend();
|
|
252
|
+
backend.launchUrl = 'myapp://x';
|
|
253
|
+
setProtocolBackend(backend);
|
|
254
|
+
expect(getProtocolLaunchUrl()).toBe('myapp://x');
|
|
255
|
+
expect(getProtocolLaunchUrl()).toBe('myapp://x');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('getRegisteredProtocolSchemes', () => {
|
|
260
|
+
it('returns empty when no schemes registered', () => {
|
|
261
|
+
const backend = fakeBackend();
|
|
262
|
+
setProtocolBackend(backend);
|
|
263
|
+
expect(getRegisteredProtocolSchemes()).toEqual([]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('includes schemes that have been registered', () => {
|
|
267
|
+
const backend = fakeBackend();
|
|
268
|
+
setProtocolBackend(backend);
|
|
269
|
+
registerProtocolScheme('myapp');
|
|
270
|
+
expect(getRegisteredProtocolSchemes()).toContain('myapp');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('isProtocolSchemeDefault', () => {
|
|
275
|
+
it('returns false by default', () => {
|
|
276
|
+
const backend = fakeBackend();
|
|
277
|
+
setProtocolBackend(backend);
|
|
278
|
+
expect(isProtocolSchemeDefault('myapp')).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('returns true after setProtocolSchemeAsDefault', () => {
|
|
282
|
+
const backend = fakeBackend();
|
|
283
|
+
setProtocolBackend(backend);
|
|
284
|
+
setProtocolSchemeAsDefault('myapp');
|
|
285
|
+
expect(isProtocolSchemeDefault('myapp')).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('returns false after removeProtocolSchemeAsDefault', () => {
|
|
289
|
+
const backend = fakeBackend();
|
|
290
|
+
setProtocolBackend(backend);
|
|
291
|
+
setProtocolSchemeAsDefault('myapp');
|
|
292
|
+
removeProtocolSchemeAsDefault('myapp');
|
|
293
|
+
expect(isProtocolSchemeDefault('myapp')).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('isProtocolSchemeRegistered', () => {
|
|
298
|
+
it('reflects the backend registration state', () => {
|
|
299
|
+
const backend = fakeBackend();
|
|
300
|
+
setProtocolBackend(backend);
|
|
301
|
+
expect(isProtocolSchemeRegistered('myapp')).toBe(false);
|
|
302
|
+
registerProtocolScheme('myapp');
|
|
303
|
+
expect(isProtocolSchemeRegistered('myapp')).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('isValidProtocolScheme', () => {
|
|
308
|
+
it('accepts valid custom schemes', () => {
|
|
309
|
+
expect(isValidProtocolScheme('myapp')).toBe(true);
|
|
310
|
+
expect(isValidProtocolScheme('my-app')).toBe(true);
|
|
311
|
+
expect(isValidProtocolScheme('my.app')).toBe(true);
|
|
312
|
+
expect(isValidProtocolScheme('my+app')).toBe(true);
|
|
313
|
+
expect(isValidProtocolScheme('app123')).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('rejects schemes not starting with a letter', () => {
|
|
317
|
+
expect(isValidProtocolScheme('1app')).toBe(false);
|
|
318
|
+
expect(isValidProtocolScheme('-app')).toBe(false);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('rejects empty and non-string inputs', () => {
|
|
322
|
+
expect(isValidProtocolScheme('')).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('rejects reserved schemes', () => {
|
|
326
|
+
expect(isValidProtocolScheme('http')).toBe(false);
|
|
327
|
+
expect(isValidProtocolScheme('https')).toBe(false);
|
|
328
|
+
expect(isValidProtocolScheme('ftp')).toBe(false);
|
|
329
|
+
expect(isValidProtocolScheme('mailto')).toBe(false);
|
|
330
|
+
expect(isValidProtocolScheme('file')).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('is case-insensitive — lowercases before validation', () => {
|
|
334
|
+
expect(isValidProtocolScheme('MyApp')).toBe(true);
|
|
335
|
+
expect(isValidProtocolScheme('HTTP')).toBe(false);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('parseProtocolUrl', () => {
|
|
340
|
+
it('parses a full URL with host, path, and query', () => {
|
|
341
|
+
const result = parseProtocolUrl('myapp://host/path?foo=bar&baz=qux');
|
|
342
|
+
expect(result?.scheme).toBe('myapp');
|
|
343
|
+
expect(result?.host).toBe('host');
|
|
344
|
+
expect(result?.path).toBe('/path');
|
|
345
|
+
expect(result?.query['foo']).toBe('bar');
|
|
346
|
+
expect(result?.query['baz']).toBe('qux');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('parses a URL with no host', () => {
|
|
350
|
+
const result = parseProtocolUrl('myapp:/action');
|
|
351
|
+
expect(result?.scheme).toBe('myapp');
|
|
352
|
+
expect(result?.host).toBe('');
|
|
353
|
+
expect(result?.path).toBe('/action');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('parses a URL with no path', () => {
|
|
357
|
+
const result = parseProtocolUrl('myapp://host');
|
|
358
|
+
expect(result?.scheme).toBe('myapp');
|
|
359
|
+
expect(result?.host).toBe('host');
|
|
360
|
+
expect(result?.path).toBe('');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('decodes percent-encoded query values', () => {
|
|
364
|
+
const result = parseProtocolUrl('myapp://x?key=hello%20world');
|
|
365
|
+
expect(result?.query['key']).toBe('hello world');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('last-value-wins for duplicate query keys', () => {
|
|
369
|
+
const result = parseProtocolUrl('myapp://x?k=first&k=second');
|
|
370
|
+
expect(result?.query['k']).toBe('second');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('returns null for malformed or empty input', () => {
|
|
374
|
+
expect(parseProtocolUrl('')).toBeNull();
|
|
375
|
+
expect(parseProtocolUrl('notaurl')).toBeNull();
|
|
376
|
+
expect(parseProtocolUrl(':noscheme')).toBeNull();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('lowercases the scheme', () => {
|
|
380
|
+
const result = parseProtocolUrl('MyApp://host');
|
|
381
|
+
expect(result?.scheme).toBe('myapp');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('handles URL with no query string', () => {
|
|
385
|
+
const result = parseProtocolUrl('myapp://host/path');
|
|
386
|
+
expect(result?.query).toEqual({});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe('registerProtocolScheme', () => {
|
|
391
|
+
it('registers through the backend', () => {
|
|
392
|
+
setProtocolBackend(fakeBackend());
|
|
393
|
+
expect(registerProtocolScheme('myapp')).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('returns false for an invalid scheme without calling the backend', () => {
|
|
397
|
+
const backend = fakeBackend();
|
|
398
|
+
setProtocolBackend(backend);
|
|
399
|
+
expect(registerProtocolScheme('123invalid')).toBe(false);
|
|
400
|
+
expect(backend.registered).toBe(false);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('returns false for a reserved scheme', () => {
|
|
404
|
+
setProtocolBackend(fakeBackend());
|
|
405
|
+
expect(registerProtocolScheme('https')).toBe(false);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('registerProtocolSchemes', () => {
|
|
410
|
+
it('registers multiple schemes and returns true when all succeed', () => {
|
|
411
|
+
setProtocolBackend(fakeBackend());
|
|
412
|
+
expect(registerProtocolSchemes(['myapp', 'myapp2'])).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('returns false if any registration fails', () => {
|
|
416
|
+
const backend = fakeBackend();
|
|
417
|
+
let callCount = 0;
|
|
418
|
+
const orig = backend.register.bind(backend);
|
|
419
|
+
backend.register = (scheme) => {
|
|
420
|
+
callCount++;
|
|
421
|
+
return callCount === 1 ? orig(scheme) : false;
|
|
422
|
+
};
|
|
423
|
+
setProtocolBackend(backend);
|
|
424
|
+
expect(registerProtocolSchemes(['myapp', 'myapp2'])).toBe(false);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('returns false and skips backend for invalid schemes in the batch', () => {
|
|
428
|
+
const backend = fakeBackend();
|
|
429
|
+
setProtocolBackend(backend);
|
|
430
|
+
expect(registerProtocolSchemes(['myapp', 'http'])).toBe(false);
|
|
431
|
+
// 'myapp' succeeded but 'http' failed — registered reflects the partial success
|
|
432
|
+
expect(backend.registered).toBe(true);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('removeProtocolSchemeAsDefault', () => {
|
|
437
|
+
it('removes default status through the backend', () => {
|
|
438
|
+
const backend = fakeBackend();
|
|
439
|
+
setProtocolBackend(backend);
|
|
440
|
+
setProtocolSchemeAsDefault('myapp');
|
|
441
|
+
expect(removeProtocolSchemeAsDefault('myapp')).toBe(true);
|
|
442
|
+
expect(isProtocolSchemeDefault('myapp')).toBe(false);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
describe('setProtocolBackend', () => {
|
|
447
|
+
it('clears back to the web fallback when passed null', () => {
|
|
448
|
+
setProtocolBackend(fakeBackend());
|
|
449
|
+
setProtocolBackend(null);
|
|
450
|
+
expect(getProtocolBackend()).not.toBeNull();
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe('setProtocolSchemeAsDefault', () => {
|
|
455
|
+
it('marks the scheme as default through the backend', () => {
|
|
456
|
+
const backend = fakeBackend();
|
|
457
|
+
setProtocolBackend(backend);
|
|
458
|
+
expect(setProtocolSchemeAsDefault('myapp')).toBe(true);
|
|
459
|
+
expect(backend.defaulted).toBe(true);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe('unregisterProtocolScheme', () => {
|
|
464
|
+
it('unregisters through the backend', () => {
|
|
465
|
+
const backend = fakeBackend();
|
|
466
|
+
setProtocolBackend(backend);
|
|
467
|
+
registerProtocolScheme('myapp');
|
|
468
|
+
expect(unregisterProtocolScheme('myapp')).toBe(true);
|
|
469
|
+
expect(backend.registered).toBe(false);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe('unregisterProtocolSchemes', () => {
|
|
474
|
+
it('unregisters multiple schemes and returns true when all succeed', () => {
|
|
475
|
+
const backend = fakeBackend();
|
|
476
|
+
setProtocolBackend(backend);
|
|
477
|
+
registerProtocolScheme('myapp');
|
|
478
|
+
expect(unregisterProtocolSchemes(['myapp', 'myapp2'])).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('returns false if any unregistration fails', () => {
|
|
482
|
+
const backend = fakeBackend();
|
|
483
|
+
let callCount = 0;
|
|
484
|
+
const orig = backend.unregister.bind(backend);
|
|
485
|
+
backend.unregister = (scheme) => {
|
|
486
|
+
callCount++;
|
|
487
|
+
return callCount === 1 ? orig(scheme) : false;
|
|
488
|
+
};
|
|
489
|
+
setProtocolBackend(backend);
|
|
490
|
+
expect(unregisterProtocolSchemes(['myapp', 'myapp2'])).toBe(false);
|
|
491
|
+
});
|
|
492
|
+
});
|