@parity/product-sdk-host 0.0.0-dev.312.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/LICENSE +201 -0
- package/dist/chain-discovery-nLrPzb3d.d.ts +70 -0
- package/dist/chunk-GDXSV7JV.js +50 -0
- package/dist/chunk-GDXSV7JV.js.map +1 -0
- package/dist/index.d.ts +1210 -0
- package/dist/index.js +1163 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +105 -0
- package/dist/testing.js +177 -0
- package/dist/testing.js.map +1 -0
- package/package.json +46 -0
- package/src/accounts.ts +1052 -0
- package/src/chain-discovery.ts +287 -0
- package/src/chain-spec.ts +272 -0
- package/src/chain-transaction.ts +241 -0
- package/src/chains.ts +46 -0
- package/src/chat.ts +122 -0
- package/src/container.ts +339 -0
- package/src/entropy.ts +105 -0
- package/src/errors.ts +239 -0
- package/src/features.ts +172 -0
- package/src/index.ts +149 -0
- package/src/navigation.ts +128 -0
- package/src/notifications.ts +113 -0
- package/src/papi-provider.ts +741 -0
- package/src/payments.ts +117 -0
- package/src/permissions.ts +236 -0
- package/src/result.ts +13 -0
- package/src/testing.ts +493 -0
- package/src/theme.ts +78 -0
- package/src/transport.ts +181 -0
- package/src/truapi.ts +313 -0
- package/src/types.ts +101 -0
- package/src/worker.ts +261 -0
package/src/features.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Higher-level wrappers for the host's feature-support probe.
|
|
5
|
+
*
|
|
6
|
+
* `truApi.system.featureSupported` returns a neverthrow `ResultAsync`;
|
|
7
|
+
* {@link featureSupported} collapses that to a `Result` carrying the host's
|
|
8
|
+
* boolean answer. {@link isChainSupported} is a convenience over the only
|
|
9
|
+
* feature variant the host exposes today (`Chain`).
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createLogger } from "@parity/product-sdk-logger";
|
|
15
|
+
|
|
16
|
+
import { type HostError, HostUnavailableError } from "./errors.js";
|
|
17
|
+
import { type Result, err } from "./result.js";
|
|
18
|
+
import { getTruApi, type HexString, mapHostResult } from "./truapi.js";
|
|
19
|
+
|
|
20
|
+
const log = createLogger("host:features");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A feature the host can be probed for via {@link featureSupported}.
|
|
24
|
+
*
|
|
25
|
+
* The only variant today is `Chain`, carrying the chain's `0x`-prefixed genesis
|
|
26
|
+
* hash. This is a flattened form of truapi's `HostFeatureSupportedRequest`,
|
|
27
|
+
* which nests the hash as `{ tag: "Chain"; value: { genesisHash } }` — we
|
|
28
|
+
* inline `value` as the `HexString` for ergonomics and re-nest it at the call
|
|
29
|
+
* site. New variants surface here as a widening of the union.
|
|
30
|
+
*/
|
|
31
|
+
export type Feature = { tag: "Chain"; value: HexString };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Probe the host for support of a specific feature.
|
|
35
|
+
*
|
|
36
|
+
* Calls `truApi.system.featureSupported`, unwraps the response, and returns the
|
|
37
|
+
* host's boolean answer.
|
|
38
|
+
*
|
|
39
|
+
* @param feature - The feature to probe for.
|
|
40
|
+
* @returns `ok(true)` if the host supports the feature, `ok(false)` otherwise,
|
|
41
|
+
* or `err(HostUnavailableError | HostCallFailedError)`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { featureSupported } from "@parity/product-sdk-host";
|
|
46
|
+
*
|
|
47
|
+
* const r = await featureSupported({ tag: "Chain", value: genesisHash });
|
|
48
|
+
* if (r.ok && r.value) { ... }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export async function featureSupported(feature: Feature): Promise<Result<boolean, HostError>> {
|
|
52
|
+
const truApi = await getTruApi();
|
|
53
|
+
if (!truApi) {
|
|
54
|
+
return err(new HostUnavailableError("featureSupported: TruAPI unavailable"));
|
|
55
|
+
}
|
|
56
|
+
log.debug("featureSupported", { tag: feature.tag });
|
|
57
|
+
|
|
58
|
+
return mapHostResult(
|
|
59
|
+
truApi.system.featureSupported({ tag: feature.tag, value: { genesisHash: feature.value } }),
|
|
60
|
+
(response) => response.supported,
|
|
61
|
+
"featureSupported failed",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convenience probe: is the chain with the given genesis hash supported by the
|
|
67
|
+
* host? Wraps {@link featureSupported} for the `Chain` feature variant.
|
|
68
|
+
*
|
|
69
|
+
* @param genesisHash - The chain's `0x`-prefixed genesis hash.
|
|
70
|
+
* @returns `ok(true)` if the host supports the chain, `ok(false)` otherwise, or
|
|
71
|
+
* `err(HostUnavailableError | HostCallFailedError)`.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { isChainSupported } from "@parity/product-sdk-host";
|
|
76
|
+
*
|
|
77
|
+
* const r = await isChainSupported(genesisHash);
|
|
78
|
+
* if (!r.ok || !r.value) {
|
|
79
|
+
* tellUserChainUnavailable();
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export async function isChainSupported(
|
|
84
|
+
genesisHash: HexString,
|
|
85
|
+
): Promise<Result<boolean, HostError>> {
|
|
86
|
+
return featureSupported({ tag: "Chain", value: genesisHash });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (import.meta.vitest) {
|
|
90
|
+
const { test, expect, describe, vi } = import.meta.vitest;
|
|
91
|
+
|
|
92
|
+
async function withMockedTruApi<T>(
|
|
93
|
+
bridge: { system?: { featureSupported?: (req: unknown) => unknown } } | null,
|
|
94
|
+
fn: (mod: typeof import("./features.js")) => Promise<T>,
|
|
95
|
+
): Promise<T> {
|
|
96
|
+
vi.resetModules();
|
|
97
|
+
vi.doMock("./truapi.js", async (importOriginal) => {
|
|
98
|
+
const original = await importOriginal<typeof import("./truapi.js")>();
|
|
99
|
+
return {
|
|
100
|
+
...original,
|
|
101
|
+
getTruApi: async () => bridge,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
const mod = await import("./features.js");
|
|
106
|
+
return await fn(mod);
|
|
107
|
+
} finally {
|
|
108
|
+
vi.doUnmock("./truapi.js");
|
|
109
|
+
vi.resetModules();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const okBridge = (supported: boolean) => ({
|
|
114
|
+
system: {
|
|
115
|
+
featureSupported: vi.fn().mockReturnValue({
|
|
116
|
+
match: async (onOk: (v: unknown) => unknown) => onOk({ supported }),
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("featureSupported", () => {
|
|
122
|
+
test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
|
|
123
|
+
await withMockedTruApi(null, async (mod) => {
|
|
124
|
+
const result = await mod.featureSupported({ tag: "Chain", value: "0x00" });
|
|
125
|
+
expect(result.ok).toBe(false);
|
|
126
|
+
if (!result.ok) {
|
|
127
|
+
expect(result.error.name).toBe("HostUnavailableError");
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("returns ok with the boolean outcome", async () => {
|
|
133
|
+
await withMockedTruApi(okBridge(true), async (mod) => {
|
|
134
|
+
expect(await mod.featureSupported({ tag: "Chain", value: "0x00" })).toEqual({
|
|
135
|
+
ok: true,
|
|
136
|
+
value: true,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
|
|
142
|
+
await withMockedTruApi(
|
|
143
|
+
{
|
|
144
|
+
system: {
|
|
145
|
+
featureSupported: vi.fn().mockReturnValue({
|
|
146
|
+
match: async (
|
|
147
|
+
_onOk: (v: unknown) => unknown,
|
|
148
|
+
onErr: (e: unknown) => unknown,
|
|
149
|
+
) => onErr({ reason: "boom" }),
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
async (mod) => {
|
|
154
|
+
const result = await mod.featureSupported({ tag: "Chain", value: "0x00" });
|
|
155
|
+
expect(result.ok).toBe(false);
|
|
156
|
+
if (!result.ok) {
|
|
157
|
+
expect(result.error.name).toBe("HostCallFailedError");
|
|
158
|
+
expect(result.error.message).toMatch(/featureSupported failed: boom/);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("isChainSupported", () => {
|
|
166
|
+
test("delegates to featureSupported with the Chain variant", async () => {
|
|
167
|
+
await withMockedTruApi(okBridge(false), async (mod) => {
|
|
168
|
+
expect(await mod.isChainSupported("0x1234")).toEqual({ ok: true, value: false });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* @parity/product-sdk-host — Detect and talk to the Polkadot Desktop/Mobile host container.
|
|
5
|
+
*
|
|
6
|
+
* Use `isInsideContainer` to branch behavior when running embedded vs. standalone,
|
|
7
|
+
* and `getHostLocalStorage`, `getHostProvider`, and `getStatementStore` to reach
|
|
8
|
+
* the storage, signer, and statement-store APIs the host injects.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
export {
|
|
13
|
+
isInsideContainer,
|
|
14
|
+
isInsideContainerSync,
|
|
15
|
+
getHostLocalStorage,
|
|
16
|
+
createHostLocalStorage,
|
|
17
|
+
getHostProvider,
|
|
18
|
+
getStatementStore,
|
|
19
|
+
ChainNotSupportedError,
|
|
20
|
+
} from "./container.js";
|
|
21
|
+
export type {
|
|
22
|
+
HostLocalStorage,
|
|
23
|
+
HostStatementStore,
|
|
24
|
+
HostSubscription,
|
|
25
|
+
ProductAccountId,
|
|
26
|
+
SignedStatement,
|
|
27
|
+
Statement,
|
|
28
|
+
StatementProof,
|
|
29
|
+
StatementTopicFilter,
|
|
30
|
+
StatementsPage,
|
|
31
|
+
Topic,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
export { BULLETIN_RPCS, DEFAULT_BULLETIN_ENDPOINT } from "./chains.js";
|
|
34
|
+
|
|
35
|
+
// TruAPI - @parity/truapi client accessor + convenience wrappers.
|
|
36
|
+
export {
|
|
37
|
+
getTruApi,
|
|
38
|
+
getPreimageManager,
|
|
39
|
+
createHostPreimageManager,
|
|
40
|
+
requestResourceAllocation,
|
|
41
|
+
createProofAuthorized,
|
|
42
|
+
// Hex helpers
|
|
43
|
+
toHex,
|
|
44
|
+
fromHex,
|
|
45
|
+
} from "./truapi.js";
|
|
46
|
+
export type {
|
|
47
|
+
TruApi,
|
|
48
|
+
HexString,
|
|
49
|
+
PreimageManager,
|
|
50
|
+
ResultAsync,
|
|
51
|
+
AllocatableResource,
|
|
52
|
+
AllocationOutcome,
|
|
53
|
+
RemotePermission,
|
|
54
|
+
} from "./truapi.js";
|
|
55
|
+
|
|
56
|
+
// Worker — this product's background worker, called from its rendered surface.
|
|
57
|
+
export { getWorkerManager, WorkerCallError } from "./worker.js";
|
|
58
|
+
export type { WorkerManager, WorkerErrorTag } from "./worker.js";
|
|
59
|
+
|
|
60
|
+
// Host chain discovery.
|
|
61
|
+
export { getHostChainInfo } from "./chain-discovery.js";
|
|
62
|
+
export type { HostChainDiscovery, HostChainIdentifier } from "./chain-discovery.js";
|
|
63
|
+
|
|
64
|
+
// Result type + typed host errors (the throw→Result boundary)
|
|
65
|
+
export { ok, err } from "./result.js";
|
|
66
|
+
export type { Result } from "./result.js";
|
|
67
|
+
export { type SdkError, isSdkError } from "@parity/product-sdk-errors";
|
|
68
|
+
export {
|
|
69
|
+
HostError,
|
|
70
|
+
HostUnavailableError,
|
|
71
|
+
HostCallFailedError,
|
|
72
|
+
isHostError,
|
|
73
|
+
formatHostError,
|
|
74
|
+
} from "./errors.js";
|
|
75
|
+
export type { HostErrorPayload } from "./errors.js";
|
|
76
|
+
|
|
77
|
+
// Accounts — host wallet accounts, product accounts, Ring VRF, and signers.
|
|
78
|
+
export { getAccountsProvider, findRingVrfKeyHandle } from "./accounts.js";
|
|
79
|
+
export type {
|
|
80
|
+
AccountsProvider,
|
|
81
|
+
DerivationIndex,
|
|
82
|
+
HostAccount,
|
|
83
|
+
ProductAccount,
|
|
84
|
+
ProductAccountLookup,
|
|
85
|
+
ContextualAlias,
|
|
86
|
+
ProductProofContext,
|
|
87
|
+
RegisteredRingVrfKey,
|
|
88
|
+
RingLocation,
|
|
89
|
+
RingVRFProof,
|
|
90
|
+
RingVrfKeyDisclosure,
|
|
91
|
+
RingVrfKeyHandle,
|
|
92
|
+
RingVrfPublicKey,
|
|
93
|
+
VrfSignature,
|
|
94
|
+
VrfTranscriptItem,
|
|
95
|
+
} from "./accounts.js";
|
|
96
|
+
|
|
97
|
+
// Higher-level permission wrappers
|
|
98
|
+
export { requestPermission, requestDevicePermission } from "./permissions.js";
|
|
99
|
+
export type { DevicePermissionKind, RemotePermissionItem } from "./permissions.js";
|
|
100
|
+
|
|
101
|
+
// Theme provider
|
|
102
|
+
export { getThemeProvider } from "./theme.js";
|
|
103
|
+
export type { ThemeMode, ThemeName, ThemeProvider, ThemeVariant } from "./theme.js";
|
|
104
|
+
|
|
105
|
+
// Entropy derivation (RFC-0007)
|
|
106
|
+
export { deriveEntropy } from "./entropy.js";
|
|
107
|
+
|
|
108
|
+
// Chat
|
|
109
|
+
export { getChatManager } from "./chat.js";
|
|
110
|
+
export type {
|
|
111
|
+
ChatManager,
|
|
112
|
+
ChatMessageContent,
|
|
113
|
+
ChatReceivedAction,
|
|
114
|
+
ChatRoom,
|
|
115
|
+
ChatRoomRegistrationResult,
|
|
116
|
+
ChatBotRegistrationResult,
|
|
117
|
+
} from "./chat.js";
|
|
118
|
+
|
|
119
|
+
// Payments (RFC-0006)
|
|
120
|
+
export { getPaymentManager } from "./payments.js";
|
|
121
|
+
export type { PaymentManager } from "./payments.js";
|
|
122
|
+
export type {
|
|
123
|
+
HostPaymentBalanceSubscribeItem,
|
|
124
|
+
HostPaymentStatusSubscribeItem,
|
|
125
|
+
PaymentTopUpSource,
|
|
126
|
+
} from "@parity/truapi";
|
|
127
|
+
|
|
128
|
+
// Notifications
|
|
129
|
+
export { getNotificationManager } from "./notifications.js";
|
|
130
|
+
export type {
|
|
131
|
+
NotificationManager,
|
|
132
|
+
NotificationId,
|
|
133
|
+
PushNotificationInput,
|
|
134
|
+
PushNotificationError,
|
|
135
|
+
} from "./notifications.js";
|
|
136
|
+
|
|
137
|
+
// Deep-link navigation
|
|
138
|
+
export { navigateTo } from "./navigation.js";
|
|
139
|
+
|
|
140
|
+
// Feature / chain support probes
|
|
141
|
+
export { featureSupported, isChainSupported } from "./features.js";
|
|
142
|
+
export type { Feature } from "./features.js";
|
|
143
|
+
|
|
144
|
+
// Chain spec lookups
|
|
145
|
+
export { getChainSpec } from "./chain-spec.js";
|
|
146
|
+
export type { ChainSpec, ChainProperties } from "./chain-spec.js";
|
|
147
|
+
|
|
148
|
+
// Transaction broadcast lifecycle
|
|
149
|
+
export { broadcastTransaction, stopTransaction } from "./chain-transaction.js";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Higher-level wrapper for the host's deep-link navigation.
|
|
5
|
+
*
|
|
6
|
+
* `truApi.system.navigateTo` returns a neverthrow `ResultAsync`; consumers
|
|
7
|
+
* still have to unwrap it themselves. {@link navigateTo} collapses that to a
|
|
8
|
+
* `Result<void, HostError>`-returning Promise.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createLogger } from "@parity/product-sdk-logger";
|
|
14
|
+
|
|
15
|
+
import { type HostError, HostUnavailableError } from "./errors.js";
|
|
16
|
+
import { type Result, err } from "./result.js";
|
|
17
|
+
import { getTruApi, mapHostResult } from "./truapi.js";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("host:navigation");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ask the host to navigate to a URL (deep link or external link).
|
|
23
|
+
*
|
|
24
|
+
* Calls `truApi.system.navigateTo` and unwraps the response. The host resolves
|
|
25
|
+
* the destination itself — a `dot`-suffixed deep link (e.g.
|
|
26
|
+
* `"https://search.dot"`) routes to another app/route inside the container, an
|
|
27
|
+
* `https://` URL opens externally.
|
|
28
|
+
*
|
|
29
|
+
* @param url - The URL to navigate to.
|
|
30
|
+
* @returns `ok` on success, or `err`: {@link HostUnavailableError} if the host
|
|
31
|
+
* is unavailable, or {@link HostCallFailedError} if it denies the navigation
|
|
32
|
+
* (`NavigateToErr::PermissionDenied`) or fails otherwise (`NavigateToErr::Unknown`).
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { navigateTo } from "@parity/product-sdk-host";
|
|
37
|
+
*
|
|
38
|
+
* const r = await navigateTo("https://search.dot");
|
|
39
|
+
* if (!r.ok) handle(r.error);
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export async function navigateTo(url: string): Promise<Result<void, HostError>> {
|
|
43
|
+
const truApi = await getTruApi();
|
|
44
|
+
if (!truApi) {
|
|
45
|
+
return err(new HostUnavailableError("navigateTo: TruAPI unavailable"));
|
|
46
|
+
}
|
|
47
|
+
log.debug("navigateTo", { url });
|
|
48
|
+
|
|
49
|
+
return mapHostResult(truApi.system.navigateTo({ url }), () => undefined, "navigateTo failed");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (import.meta.vitest) {
|
|
53
|
+
const { test, expect, describe, vi } = import.meta.vitest;
|
|
54
|
+
|
|
55
|
+
async function withMockedTruApi<T>(
|
|
56
|
+
bridge: { system?: { navigateTo?: (req: unknown) => unknown } } | null,
|
|
57
|
+
fn: (mod: typeof import("./navigation.js")) => Promise<T>,
|
|
58
|
+
): Promise<T> {
|
|
59
|
+
vi.resetModules();
|
|
60
|
+
vi.doMock("./truapi.js", async (importOriginal) => {
|
|
61
|
+
const original = await importOriginal<typeof import("./truapi.js")>();
|
|
62
|
+
return {
|
|
63
|
+
...original,
|
|
64
|
+
getTruApi: async () => bridge,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
const mod = await import("./navigation.js");
|
|
69
|
+
return await fn(mod);
|
|
70
|
+
} finally {
|
|
71
|
+
vi.doUnmock("./truapi.js");
|
|
72
|
+
vi.resetModules();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("navigateTo", () => {
|
|
77
|
+
test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
|
|
78
|
+
await withMockedTruApi(null, async (mod) => {
|
|
79
|
+
const result = await mod.navigateTo("https://search.dot");
|
|
80
|
+
expect(result.ok).toBe(false);
|
|
81
|
+
if (!result.ok) {
|
|
82
|
+
expect(result.error.name).toBe("HostUnavailableError");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("returns ok on success", async () => {
|
|
88
|
+
await withMockedTruApi(
|
|
89
|
+
{
|
|
90
|
+
system: {
|
|
91
|
+
navigateTo: vi.fn().mockReturnValue({
|
|
92
|
+
match: async (onOk: (v: unknown) => unknown) => onOk(undefined),
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
async (mod) => {
|
|
97
|
+
expect(await mod.navigateTo("https://search.dot")).toEqual({
|
|
98
|
+
ok: true,
|
|
99
|
+
value: undefined,
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
|
|
106
|
+
await withMockedTruApi(
|
|
107
|
+
{
|
|
108
|
+
system: {
|
|
109
|
+
navigateTo: vi.fn().mockReturnValue({
|
|
110
|
+
match: async (
|
|
111
|
+
_onOk: (v: unknown) => unknown,
|
|
112
|
+
onErr: (e: unknown) => unknown,
|
|
113
|
+
) => onErr({ tag: "PermissionDenied" }),
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
async (mod) => {
|
|
118
|
+
const result = await mod.navigateTo("https://search.dot");
|
|
119
|
+
expect(result.ok).toBe(false);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
expect(result.error.name).toBe("HostCallFailedError");
|
|
122
|
+
expect(result.error.message).toMatch(/navigateTo failed: PermissionDenied/);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper for the host's scheduled push-notification surface (RFC-0019),
|
|
5
|
+
* backed by `truApi.notifications.*`.
|
|
6
|
+
*
|
|
7
|
+
* `getNotificationManager()` returns a handle exposing `push(input)` (resolves
|
|
8
|
+
* to a {@link NotificationId}) and `cancel(id)`, matching the singleton
|
|
9
|
+
* pattern already used by {@link getPaymentManager}, {@link getPreimageManager},
|
|
10
|
+
* and {@link getHostLocalStorage}.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { HostPushNotificationRequest, NotificationId, TrUApiClient } from "@parity/truapi";
|
|
16
|
+
|
|
17
|
+
import { getClient } from "./transport.js";
|
|
18
|
+
import { unwrapHostResult } from "./truapi.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error variants the host raises when scheduling a push notification.
|
|
22
|
+
*
|
|
23
|
+
* A `{ tag }` tagged union re-exported from `@parity/truapi`:
|
|
24
|
+
* `{ tag: "ScheduleLimitReached" }` (the host-wide pending-notification cap) or
|
|
25
|
+
* `{ tag: "Unknown"; value: { reason } }`. {@link NotificationManager.push} /
|
|
26
|
+
* {@link NotificationManager.cancel} reject with an `Error` whose `cause`
|
|
27
|
+
* carries this value, so branch on it — e.g.
|
|
28
|
+
* `(err as Error).cause?.tag === "ScheduleLimitReached"`.
|
|
29
|
+
*/
|
|
30
|
+
export type { HostPushNotificationError as PushNotificationError } from "@parity/truapi";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Host-assigned id for a scheduled notification — pass to
|
|
34
|
+
* {@link NotificationManager.cancel}. Re-exported from `@parity/truapi`.
|
|
35
|
+
*/
|
|
36
|
+
export type { NotificationId };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Push payload: `text`, an optional `deeplink`, and an optional `scheduledAt`
|
|
40
|
+
* (Unix timestamp in milliseconds; omit for immediate delivery). Re-exported
|
|
41
|
+
* from the truapi wire request type so the shape stays in lockstep with the
|
|
42
|
+
* protocol.
|
|
43
|
+
*/
|
|
44
|
+
export type PushNotificationInput = HostPushNotificationRequest;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Host notification manager handle. Exposes `push(input)` (resolves to a
|
|
48
|
+
* {@link NotificationId}) and `cancel(id)`.
|
|
49
|
+
*/
|
|
50
|
+
export interface NotificationManager {
|
|
51
|
+
push(input: PushNotificationInput): Promise<NotificationId>;
|
|
52
|
+
cancel(id: NotificationId): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Build a {@link NotificationManager} over a TruAPI client's `notifications` domain. */
|
|
56
|
+
function adaptNotificationManager(client: TrUApiClient): NotificationManager {
|
|
57
|
+
const notifications = client.notifications;
|
|
58
|
+
return {
|
|
59
|
+
async push(input) {
|
|
60
|
+
const response = await unwrapHostResult(
|
|
61
|
+
notifications.sendPushNotification(input),
|
|
62
|
+
"notification push failed",
|
|
63
|
+
);
|
|
64
|
+
return response.id;
|
|
65
|
+
},
|
|
66
|
+
async cancel(id) {
|
|
67
|
+
await unwrapHostResult(
|
|
68
|
+
notifications.cancelPushNotification({ id }),
|
|
69
|
+
"notification cancel failed",
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the host notification manager, backed by `truApi.notifications.*`.
|
|
77
|
+
* Returns `null` when running outside a host container.
|
|
78
|
+
*
|
|
79
|
+
* @returns The notification manager, or `null` if unavailable.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { getNotificationManager, type PushNotificationError } from "@parity/product-sdk-host";
|
|
84
|
+
*
|
|
85
|
+
* const notifications = await getNotificationManager();
|
|
86
|
+
* if (notifications) {
|
|
87
|
+
* try {
|
|
88
|
+
* const id = await notifications.push({
|
|
89
|
+
* text: "Doors open in 1h",
|
|
90
|
+
* scheduledAt: someUnixMs,
|
|
91
|
+
* });
|
|
92
|
+
* // later: await notifications.cancel(id);
|
|
93
|
+
* } catch (err) {
|
|
94
|
+
* const cause = (err as Error).cause as PushNotificationError | undefined;
|
|
95
|
+
* if (cause?.tag === "ScheduleLimitReached") {
|
|
96
|
+
* // host hit its pending-notification cap — surface to the user
|
|
97
|
+
* }
|
|
98
|
+
* }
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export async function getNotificationManager(): Promise<NotificationManager | null> {
|
|
103
|
+
const client = await getClient();
|
|
104
|
+
return client ? adaptNotificationManager(client) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (import.meta.vitest) {
|
|
108
|
+
const { test, expect } = import.meta.vitest;
|
|
109
|
+
|
|
110
|
+
test("getNotificationManager returns null outside a container", async () => {
|
|
111
|
+
expect(await getNotificationManager()).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
}
|