@parity/product-sdk-host 0.11.0 → 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.
package/src/navigation.ts CHANGED
@@ -3,62 +3,57 @@
3
3
  /**
4
4
  * Higher-level wrapper for the host's deep-link navigation.
5
5
  *
6
- * `hostApi.navigateTo` is reachable via {@link getTruApi}, but consumers have
7
- * to wrap the URL in the versioned envelope (`enumValue("v1", ...)`) and
8
- * unwrap the neverthrow `ResultAsync` themselves. {@link navigateTo} collapses
9
- * that to a throw-on-error Promise that matches the shape of
10
- * {@link requestPermission} and {@link deriveEntropy}.
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.
11
9
  *
12
10
  * @module
13
11
  */
14
12
 
15
13
  import { createLogger } from "@parity/product-sdk-logger";
16
14
 
17
- import { enumValue, formatHostError, getTruApi } from "./truapi.js";
15
+ import { type HostError, HostUnavailableError } from "./errors.js";
16
+ import { type Result, err } from "./result.js";
17
+ import { getTruApi, mapHostResult } from "./truapi.js";
18
18
 
19
19
  const log = createLogger("host:navigation");
20
20
 
21
21
  /**
22
22
  * Ask the host to navigate to a URL (deep link or external link).
23
23
  *
24
- * Builds the `v1` envelope, calls `hostApi.navigateTo`, and unwraps the
25
- * response. The host resolves the destination itself — a `dot`-suffixed
26
- * deep link (e.g. `"https://search.dot"`) routes to another app/route inside
27
- * the container, an `https://` URL opens externally.
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
28
  *
29
29
  * @param url - The URL to navigate to.
30
- * @throws If the host is unavailable, denies the navigation
31
- * (`NavigateToErr::PermissionDenied`), or fails for any other reason
32
- * (`NavigateToErr::Unknown`).
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
33
  *
34
34
  * @example
35
35
  * ```ts
36
36
  * import { navigateTo } from "@parity/product-sdk-host";
37
37
  *
38
- * await navigateTo("https://search.dot");
38
+ * const r = await navigateTo("https://search.dot");
39
+ * if (!r.ok) handle(r.error);
39
40
  * ```
40
41
  */
41
- export async function navigateTo(url: string): Promise<void> {
42
+ export async function navigateTo(url: string): Promise<Result<void, HostError>> {
42
43
  const truApi = await getTruApi();
43
44
  if (!truApi) {
44
- throw new Error("navigateTo: TruAPI unavailable");
45
+ return err(new HostUnavailableError("navigateTo: TruAPI unavailable"));
45
46
  }
46
47
  log.debug("navigateTo", { url });
47
48
 
48
- // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.
49
- await truApi.navigateTo(enumValue("v1", url)).match(
50
- (_envelope: { tag: "v1"; value: undefined }) => undefined,
51
- (err: unknown) => {
52
- throw new Error(`navigateTo failed: ${formatHostError(err)}`, { cause: err });
53
- },
54
- );
49
+ return mapHostResult(truApi.system.navigateTo({ url }), () => undefined, "navigateTo failed");
55
50
  }
56
51
 
57
52
  if (import.meta.vitest) {
58
53
  const { test, expect, describe, vi } = import.meta.vitest;
59
54
 
60
55
  async function withMockedTruApi<T>(
61
- bridge: { navigateTo?: (req: unknown) => unknown } | null,
56
+ bridge: { system?: { navigateTo?: (req: unknown) => unknown } } | null,
62
57
  fn: (mod: typeof import("./navigation.js")) => Promise<T>,
63
58
  ): Promise<T> {
64
59
  vi.resetModules();
@@ -67,7 +62,6 @@ if (import.meta.vitest) {
67
62
  return {
68
63
  ...original,
69
64
  getTruApi: async () => bridge,
70
- enumValue: (version: string, value: unknown) => ({ tag: version, value }),
71
65
  };
72
66
  });
73
67
  try {
@@ -80,46 +74,53 @@ if (import.meta.vitest) {
80
74
  }
81
75
 
82
76
  describe("navigateTo", () => {
83
- test("throws when TruAPI is unavailable", async () => {
77
+ test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
84
78
  await withMockedTruApi(null, async (mod) => {
85
- await expect(mod.navigateTo("https://search.dot")).rejects.toThrow(
86
- /TruAPI unavailable/,
87
- );
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
+ }
88
84
  });
89
85
  });
90
86
 
91
- test("resolves on the v1 success envelope", async () => {
87
+ test("returns ok on success", async () => {
92
88
  await withMockedTruApi(
93
89
  {
94
- navigateTo: vi.fn().mockReturnValue({
95
- match: async (onOk: (v: unknown) => unknown) =>
96
- onOk({ tag: "v1", value: undefined }),
97
- }),
90
+ system: {
91
+ navigateTo: vi.fn().mockReturnValue({
92
+ match: async (onOk: (v: unknown) => unknown) => onOk(undefined),
93
+ }),
94
+ },
98
95
  },
99
96
  async (mod) => {
100
- await expect(mod.navigateTo("https://search.dot")).resolves.toBeUndefined();
97
+ expect(await mod.navigateTo("https://search.dot")).toEqual({
98
+ ok: true,
99
+ value: undefined,
100
+ });
101
101
  },
102
102
  );
103
103
  });
104
104
 
105
- test("wraps host errors with a diagnostic message", async () => {
105
+ test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
106
106
  await withMockedTruApi(
107
107
  {
108
- navigateTo: vi.fn().mockReturnValue({
109
- match: async (
110
- _onOk: (v: unknown) => unknown,
111
- onErr: (e: unknown) => unknown,
112
- ) =>
113
- onErr({
114
- tag: "v1",
115
- value: { name: "NavigateToErr::PermissionDenied", message: "no" },
116
- }),
117
- }),
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
+ },
118
116
  },
119
117
  async (mod) => {
120
- await expect(mod.navigateTo("https://search.dot")).rejects.toThrow(
121
- /navigateTo failed: NavigateToErr::PermissionDenied: no/,
122
- );
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
+ }
123
124
  },
124
125
  );
125
126
  });
@@ -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
  }