@parity/product-sdk-host 0.10.3 → 0.12.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.
@@ -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
+ }
@@ -1,80 +1,86 @@
1
1
  // Copyright 2026 Parity Technologies (UK) Ltd.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  /**
4
- * Wrapper for the host's scheduled push-notification surface.
4
+ * Wrapper for the host's scheduled push-notification surface (RFC-0019),
5
+ * backed by `truApi.notifications.*`.
5
6
  *
6
- * Shipped flat-in-host rather than as `getTruApi().notification.*` because
7
- * the upstream JS `hostApi` is itself a flat object - there is no
8
- * `.notification` accessor to mirror. A flat `getNotificationManager()`
9
- * matches the singleton pattern already used by {@link getPaymentManager},
10
- * {@link getPreimageManager}, and {@link getHostLocalStorage}.
11
- *
12
- * Returns the shared `notificationManager` singleton from
13
- * `@novasamatech/host-api-wrapper` (not a fresh `createNotificationManager()`
14
- * instance) so callers share one wrapper + hostApi closure across the app.
15
- *
16
- * {@link PushNotificationError} is re-exported from `@novasamatech/host-api`
17
- * so consumers can branch on `err instanceof
18
- * PushNotificationError.ScheduleLimitReached` (the host's pending-notification
19
- * cap) without importing the novasama packages directly.
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}.
20
11
  *
21
12
  * @module
22
13
  */
23
14
 
24
- import { createLogger } from "@parity/product-sdk-logger";
15
+ import type { HostPushNotificationRequest, NotificationId, TrUApiClient } from "@parity/truapi";
25
16
 
26
- import type { notificationManager } from "@novasamatech/host-api-wrapper";
27
-
28
- const log = createLogger("host:notifications");
17
+ import { getClient } from "./transport.js";
18
+ import { unwrapHostResult } from "./truapi.js";
29
19
 
30
20
  /**
31
21
  * Error variants the host raises when scheduling a push notification.
32
22
  *
33
- * A SCALE codec (with a `[Symbol.hasInstance]`), not a plain `Error`
34
- * subclass: branch with `err instanceof
35
- * PushNotificationError.ScheduleLimitReached` to detect the host's
36
- * platform-wide pending-notification cap, or `.Unknown` for everything
37
- * else. Re-exported from `@novasamatech/host-api` so consumers can
38
- * `instanceof`-branch without a direct novasama dependency.
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"`.
39
29
  */
40
- export { PushNotificationError } from "@novasamatech/host-api";
30
+ export type { HostPushNotificationError as PushNotificationError } from "@parity/truapi";
41
31
 
42
32
  /**
43
- * Host notification manager handle. Exposes `push(input)` (resolves to a
44
- * {@link NotificationId}) and `cancel(id)`.
45
- *
46
- * Type identical to `notificationManager` from
47
- * `@novasamatech/host-api-wrapper`.
33
+ * Host-assigned id for a scheduled notification pass to
34
+ * {@link NotificationManager.cancel}. Re-exported from `@parity/truapi`.
48
35
  */
49
- export type NotificationManager = typeof notificationManager;
36
+ export type { NotificationId };
50
37
 
51
38
  /**
52
- * Host-assigned id for a scheduled notification pass to
53
- * {@link NotificationManager.cancel}. Derived from the manager's `push`
54
- * return type so codec changes surface here as compile errors.
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.
55
43
  */
56
- export type NotificationId = Awaited<ReturnType<NotificationManager["push"]>>;
44
+ export type PushNotificationInput = HostPushNotificationRequest;
57
45
 
58
46
  /**
59
- * Push payload: `text`, an optional `deeplink`, and an optional
60
- * `scheduledAt` (omit for immediate delivery). Derived from the manager's
61
- * `push` parameter so the shape stays in lockstep with upstream.
47
+ * Host notification manager handle. Exposes `push(input)` (resolves to a
48
+ * {@link NotificationId}) and `cancel(id)`.
62
49
  */
63
- export type PushNotificationInput = Parameters<NotificationManager["push"]>[0];
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
+ }
64
74
 
65
75
  /**
66
- * Get the host notification manager.
67
- *
68
- * Returns the shared `notificationManager` singleton from
69
- * `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable
70
- * (running outside a host container or the optional peer dep isn't
71
- * installed).
76
+ * Get the host notification manager, backed by `truApi.notifications.*`.
77
+ * Returns `null` when running outside a host container.
72
78
  *
73
79
  * @returns The notification manager, or `null` if unavailable.
74
80
  *
75
81
  * @example
76
82
  * ```ts
77
- * import { getNotificationManager, PushNotificationError } from "@parity/product-sdk-host";
83
+ * import { getNotificationManager, type PushNotificationError } from "@parity/product-sdk-host";
78
84
  *
79
85
  * const notifications = await getNotificationManager();
80
86
  * if (notifications) {
@@ -85,7 +91,8 @@ export type PushNotificationInput = Parameters<NotificationManager["push"]>[0];
85
91
  * });
86
92
  * // later: await notifications.cancel(id);
87
93
  * } catch (err) {
88
- * if (err instanceof PushNotificationError.ScheduleLimitReached) {
94
+ * const cause = (err as Error).cause as PushNotificationError | undefined;
95
+ * if (cause?.tag === "ScheduleLimitReached") {
89
96
  * // host hit its pending-notification cap — surface to the user
90
97
  * }
91
98
  * }
@@ -93,31 +100,14 @@ export type PushNotificationInput = Parameters<NotificationManager["push"]>[0];
93
100
  * ```
94
101
  */
95
102
  export async function getNotificationManager(): Promise<NotificationManager | null> {
96
- try {
97
- const sdk = await import("@novasamatech/host-api-wrapper");
98
- return sdk.notificationManager;
99
- } catch (err) {
100
- log.debug("getNotificationManager unavailable", err);
101
- return null;
102
- }
103
+ const client = await getClient();
104
+ return client ? adaptNotificationManager(client) : null;
103
105
  }
104
106
 
105
107
  if (import.meta.vitest) {
106
108
  const { test, expect } = import.meta.vitest;
107
109
 
108
- test("getNotificationManager returns manager with push/cancel when SDK is available", async () => {
109
- const notifications = await getNotificationManager();
110
- if (notifications === null) {
111
- // Acceptable: SDK couldn't load (e.g. peer dep missing in some envs).
112
- return;
113
- }
114
- expect(typeof notifications.push).toBe("function");
115
- expect(typeof notifications.cancel).toBe("function");
116
- });
117
-
118
- test("PushNotificationError is re-exported with its ScheduleLimitReached variant", async () => {
119
- const { PushNotificationError } = await import("./notifications.js");
120
- expect(PushNotificationError).toBeDefined();
121
- expect(PushNotificationError.ScheduleLimitReached).toBeDefined();
110
+ test("getNotificationManager returns null outside a container", async () => {
111
+ expect(await getNotificationManager()).toBeNull();
122
112
  });
123
113
  }