Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a393468
feat: multi-device SSO V2 + chat V2 (#178)
kalininilya May 22, 2026
34b7ff4
docs: changelog
johnthecat May 22, 2026
9435d77
docs: migration guide
johnthecat May 22, 2026
802e844
chore(release): publish 0.8.0-0
johnthecat May 22, 2026
ea43c35
fix(host-chat): drop private flag so the package is publishable (#180)
kalininilya May 22, 2026
e8acd61
chore(release): publish 0.8.0-1
johnthecat May 22, 2026
ef82bef
fix: dependencies
johnthecat May 25, 2026
5fb3cb9
chore: up deps
johnthecat May 26, 2026
c5c628d
fix(host-papp): raise SSO request size limit (#181)
charlesHetterich May 26, 2026
9e60c2b
feat: design system tokens (#172)
foxwoosh May 26, 2026
19ed300
feat(host-papp): watchIdentity subscription primitive (#185)
cuteWarmFrog May 27, 2026
607cec9
refactor: align ABI with spec (#182)
johnthecat May 27, 2026
93145d3
allowance link (#173)
Yanaty May 27, 2026
c9f5245
feat: sandbox module system (#189)
johnthecat May 29, 2026
c3cb900
feat: clear/abort the outgoing signing batch (#188)
cuteWarmFrog May 29, 2026
edac622
fix: substrate-slot-sr25519-wasm build with vite instead of weird lin…
johnthecat May 29, 2026
341eb5e
fix: clear outgoing batch issues (#190)
johnthecat May 29, 2026
db46496
chore: correct build flow for host-papp-react-ui
johnthecat May 29, 2026
c40713a
docs: rearranged migration guide
johnthecat Jun 1, 2026
326fb52
feat: extended theme API (#191)
johnthecat Jun 1, 2026
b8d7864
chore: up dev deps
johnthecat Jun 1, 2026
86caea6
chore(release): publish 0.8.0
johnthecat Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,51 @@
## 0.8.0 (2026-06-01)

### 🚀 Features

- **host-api / host-api-wrapper:** `host_theme_subscribe` now delivers a structured theme instead of a flat `light` / `dark` enum. The payload is a `Theme` struct — `{ name, variant }`, where `name` is `Custom(string)` or `Default` and `variant` is `Light` / `Dark` — so the host can communicate both the active theme and its light/dark variant. `host-api` exports `Theme`, `ThemeName`, and `ThemeVariant`; the wrapper's `subscribeTheme` callback now receives the struct (read `theme.variant` for the previous light/dark value).
- **host-api-wrapper:** payment methods take an optional purse selector — `subscribeBalance(cb, purse?)`, `topUp(amount, source, into?)`, `requestPayment(amount, destination, from?)`. Omit it to target the main purse, so existing calls are unaffected. New `PurseId` type exported.
- **host-chat:** new message variants for the multi-device chat layer — chat-accept now carries the originating message id, and there are new variants for announcing and removing peer devices on the identity-level session, so all of a peer's devices can decrypt without a per-device envelope.
- **host-papp:** multi-device SSO. SSO pairing now runs the V2 multi-device handshake under the hood, so desktop and web hosts pair with the multi-device iOS/Android Polkadot Mobile builds through the same `createAuth` / `pappAdapter.sso` entry point — `pairingStatus`, `authenticate()`, `abortAuthentication()`, and the `StoredUserSession` returned to consumers are unchanged. The V2 protocol, codecs, and pairing state machine are SDK internals.
- **host-papp:** the SDK now persists the device identity and pairing-topic dedupe state itself, on the configured `StorageAdapter`, so no extra consumer wiring is needed across launches. Hosts that want a different identity backend (Electron Keychain, native secure storage) can override with an optional `deviceIdentity` factory on `createPappAdapter`.
- **host-papp:** `StoredUserSession` gains optional V2 fields for the user's identity chat public key and the peer device's statement account, so consumers building device-sync or chat-level features can read peer state straight off the session. A new optional `onAuthSuccess` hook on `createPappAdapter` fires after pairing with `{ session, identityChatPrivateKey }` for consumer-specific post-pairing work (telemetry, custom peer caches, device-sync seeding).
- **host-papp:** `createPappAdapter` now returns an `allowance` service. `pappAdapter.allowance.getBulletinSigner(sessionId, productId)` and `getStatementStoreProver(sessionId, productId)` hand back a `PolkadotSigner` / statement-store prover backed by a host-managed allowance slot — reusing a cached slot key or requesting one from the paired mobile app — so a product can sign Bulletin transactions and statement-store proofs without nominating a product account. Failures surface as `AllowanceError` (`NoSession | Rejected | NotAvailable | UnexpectedResponse`).
- **host-papp:** `UserSession` gains an `abortPendingRequests()` method that cancels the in-flight remote-signing request along with anything queued behind it — it drops the per-session request queue (rejecting the pending `signPayload` / `signRaw` tasks) and clears the session's outgoing on-chain batch, freeing the single-slot queue so the next request can proceed instead of waiting out the 180s timeout. See the [abort signing section](./docs/migration/v0.8.md#abort-in-flight-signing-usersessionabortpendingrequests) of the migration guide.
- **host-papp / host-papp-react-ui:** new `pappAdapter.identity.watchIdentity(accountId)` returns a reactive `Observable<Identity | null>` — it emits any cached identity first, then live on-chain updates. The `useIdentity` hook now subscribes to it, so a profile's name/avatar update live instead of being resolved once.
- **host-worker-sandbox:** opt-in ES module imports. Pass a `resolveModule` hook to `createSandbox` and worker code can use static `import` / `export` and dynamic `import()`; the host returns each module's source on demand. See the [v0.8 migration guide](./docs/migration/v0.8.md#es-module-imports-in-the-sandbox).
- **scale:** `Enum(inner, indexes?)` accepts an optional index array to pin each variant's wire index independent of declaration order.
- **statement-store:** `Session` gains `clearOutgoingBatch()` — it supersedes the in-flight outgoing request batch with an empty statement on the same request channel (reusing the current expiry, which the store accepts since it rejects only a strictly lower expiry), drops local outgoing state, and rejects all pending response waiters — including messages still queued from session initialization, and even if the superseding submission itself fails (the submission error is still surfaced to the caller).
- **statement-store:** new helpers for substrate slot-account sr25519 signing (`deriveSlotAccountPublicKey`, `signSlotAccountSecret`, `verifySlotAccountSignature`) plus WASM init helpers (`ensureSubstrateSr25519Ready`, `ensureSubstrateSlotSr25519Ready`). New published package `@novasamatech/substrate-slot-sr25519-wasm`. Existing `deriveSr25519PublicKey` / `signWithSr25519Secret` / `verifySr25519Signature` keep the same signatures and outputs.

### 🩹 Fixes

- **host-papp / host-chat:** consumer-info parsing tolerates both camelCase and snake_case `Resources.Consumers` metadata fields — the V2 multi-device runtime metadata emits camelCase, which previously crashed account-resource resolution.
- **scale:** `OptionBool` now uses canonical SCALE encoding (`true` → 1, `false` → 2); the previous build had the two swapped.

### ⚠️ Breaking Changes

Multi-device SSO migration is essentially two field renames (the auth surface is otherwise unchanged). The Host API protocol-alignment changes are wire-level — hosts and products must upgrade together. See the [v0.8 migration guide](./docs/migration/v0.8.md).

- **host-api:** the Host API protocol spec and the SDK were reconciled — see the [protocol-alignment section](./docs/migration/v0.8.md#host-api-protocol-alignment) of the migration guide for steps and examples.
- removed the deprecated `host_jsonrpc_message_send` / `host_jsonrpc_message_subscribe` methods; use the `remote_chain_*` methods instead. Their method ids are reserved as a gap, so every other method keeps the id it had in v0.7 — the removal doesn't shift the rest of the protocol.
- `OptionBool` encoding fix (see Fixes) inverts `true`/`false` relative to older builds — affects signing's `withSignedTransaction` and the custom renderer's `enabled` / `loading`.
- payment requests (`host_payment_balance_subscribe`, `host_payment_top_up`, `host_payment_request`) gained an optional purse selector field, changing their wire layout.
- renames — `StorageQueryItem.type` → `queryType`, `RemotePermission.WebRTC` → `WebRtc`, `AllocatableResource.BulletInAllowance` → `BulletinAllowance`.
- **host-api / host-api-wrapper:** the `host_theme_subscribe` payload changed shape (flat `light` / `dark` enum → `{ name, variant }` struct) and the variant casing changed (`light` / `dark` → `Light` / `Dark`). This is a wire-level change — hosts and products must upgrade together. See the [theme subscription section](./docs/migration/v0.8.md#theme-subscription-payload) of the migration guide.
- **host-api / product-react-renderer:** custom chat renderer design tokens were renamed to the new hierarchical design-system scale — `TypographyStyle` (e.g. `titleXL` → `headline.large`) and `ColorToken` (e.g. `textPrimary` → `fg.primary`). The wire indices are unchanged; only the token identifiers changed. Products that hard-code token strings in a custom renderer must update them — see the [design tokens section](./docs/migration/v0.8.md#custom-renderer-design-tokens).
- **host-chat:** the chat-accepted message payload changed shape (now carries the originating message id). Older clients on the V1 form will not decode.
- **host-papp:** `createPappAdapter` no longer accepts `metadata: string` (the V1 metadata URL) — host name / icon / platform now ride inside `hostMetadata` (sent inline with the V2 QR proposal).
- **host-papp:** `HostMetadata` reshape — was `{ hostVersion?, osType?, osVersion? }`, now `{ hostName?, hostVersion?, hostIcon?, platformType?, platformVersion?, custom? }`. Map `osType → platformType` and `osVersion → platformVersion` when upgrading.
- **host-papp:** the V1 SSO handshake is gone. Both ends must run the multi-device V2 handshake (Polkadot Mobile builds with multi-device support are V2). Persisted V1 SSO sessions don't migrate and are wiped on first read, so users need to re-pair.
- **host-papp:** `IdentityAdapter` gains a required `watchIdentity(accountId)` method. Only affects consumers that supply a custom `adapters.identities`; the default adapter already implements it.

### ❤️ Thank You

- Ilya Kalinin @kalininilya
- Sergey Zhuravlev @johnthecat
- Vitya Livshits @cuteWarmFrog
- Yanaty
- Den

## 0.7.9 (2026-05-15)

### 🚀 Features
Expand Down
50 changes: 25 additions & 25 deletions __tests__/hostApi/chatReactRenderer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,44 +111,44 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
{/* Box A: contentAlignment, background+Rounded shape, border, fillMaxWidth */}
<Box
contentAlignment="topStart"
background={{ color: 'backgroundSecondary', shape: { tag: 'Rounded', value: 8 } }}
border={{ width: 1, color: 'textTertiary', shape: undefined }}
background={{ color: 'bg.surface.container', shape: { tag: 'Rounded', value: 8 } }}
border={{ width: 1, color: 'fg.tertiary', shape: undefined }}
fillMaxWidth
>
<Text style="titleXL" color="textSecondary">
<Text style="title.medium.regular" color="fg.secondary">
Title
</Text>
</Box>
{/* Box B: background as plain ColorToken, width/height/minWidth/minHeight */}
<Box
contentAlignment="center"
background="backgroundPrimary"
background="bg.surface.main"
width={40}
height={40}
minWidth={20}
minHeight={20}
>
{/* Inner Box: Circle shape, fillMaxWidth + fillMaxHeight */}
<Box
background={{ color: 'backgroundTertiary', shape: { tag: 'Circle', value: undefined } }}
background={{ color: 'bg.surface.nested', shape: { tag: 'Circle', value: undefined } }}
fillMaxWidth
fillMaxHeight
/>
</Box>
{/* Row: verticalAlignment, horizontalArrangement, margin; covers bodyM/bodyS/caption + success/warning/error */}
<Row verticalAlignment="bottom" horizontalArrangement="spaceEvenly" margin={8}>
<Text style="bodyM" color="success">
<Text style="body.medium.regular" color="fg.success">
Item A
</Text>
<Spacer width={8} height={4} />
<Text style="bodyS" color="warning">
<Text style="body.small.regular" color="fg.warning">
Item B
</Text>
<Text style="caption" color="error">
<Text style="body.small.regular" color="fg.error">
Item C
</Text>
</Row>
<Text style="headline" color="textPrimary">
<Text style="headline.large" color="fg.primary">
Balance: 100 DOT
</Text>
{/* Spacer with fillMaxHeight */}
Expand Down Expand Up @@ -221,26 +221,26 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
const boxAMods = boxA.value.modifiers as RendererModifier[];
expect(boxAMods).toContainEqual({
tag: 'background',
value: { color: 'backgroundSecondary', shape: { tag: 'Rounded', value: 8 } },
value: { color: 'bg.surface.container', shape: { tag: 'Rounded', value: 8 } },
});
expect(boxAMods).toContainEqual({
tag: 'border',
value: { width: 1, color: 'textTertiary', shape: undefined },
value: { width: 1, color: 'fg.tertiary', shape: undefined },
});
expect(boxAMods).toContainEqual({ tag: 'fillWidth', value: true });

// Box A > Text: titleXL + textSecondary
// Box A > Text: title.medium.regular + fg.secondary
const boxAText = boxA.value.children[0];
expect(boxAText.tag).toBe('Text');
expect(boxAText.value.props.style).toBe('titleXL');
expect(boxAText.value.props.color).toBe('textSecondary');
expect(boxAText.value.props.style).toBe('title.medium.regular');
expect(boxAText.value.props.color).toBe('fg.secondary');
expect(boxAText.value.children[0]).toEqual({ tag: 'String', value: 'Title' });

// Box B: contentAlignment, background as plain color token, width/height/minWidth/minHeight
expect(boxB.tag).toBe('Box');
expect(boxB.value.props.contentAlignment).toBe('center');
const boxBMods = boxB.value.modifiers as RendererModifier[];
expect(boxBMods).toContainEqual({ tag: 'background', value: { color: 'backgroundPrimary', shape: undefined } });
expect(boxBMods).toContainEqual({ tag: 'background', value: { color: 'bg.surface.main', shape: undefined } });
expect(boxBMods).toContainEqual({ tag: 'width', value: 40 });
expect(boxBMods).toContainEqual({ tag: 'height', value: 40 });
expect(boxBMods).toContainEqual({ tag: 'minWidth', value: 20 });
Expand All @@ -252,7 +252,7 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
const innerBoxMods = innerBox.value.modifiers as RendererModifier[];
expect(innerBoxMods).toContainEqual({
tag: 'background',
value: { color: 'backgroundTertiary', shape: { tag: 'Circle', value: undefined } },
value: { color: 'bg.surface.nested', shape: { tag: 'Circle', value: undefined } },
});
expect(innerBoxMods).toContainEqual({ tag: 'fillWidth', value: true });
expect(innerBoxMods).toContainEqual({ tag: 'fillHeight', value: true });
Expand All @@ -267,8 +267,8 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
// Row > Text: bodyM + success
const rowTextA = rowNode.value.children[0];
expect(rowTextA.tag).toBe('Text');
expect(rowTextA.value.props.style).toBe('bodyM');
expect(rowTextA.value.props.color).toBe('success');
expect(rowTextA.value.props.style).toBe('body.medium.regular');
expect(rowTextA.value.props.color).toBe('fg.success');
expect(rowTextA.value.children[0]).toEqual({ tag: 'String', value: 'Item A' });

// Row > Spacer: width + height modifiers
Expand All @@ -281,21 +281,21 @@ describe('registerChatMessageRenderer + createProductChatManager integration', (
// Row > Text: bodyS + warning
const rowTextB = rowNode.value.children[2];
expect(rowTextB.tag).toBe('Text');
expect(rowTextB.value.props.style).toBe('bodyS');
expect(rowTextB.value.props.color).toBe('warning');
expect(rowTextB.value.props.style).toBe('body.small.regular');
expect(rowTextB.value.props.color).toBe('fg.warning');
expect(rowTextB.value.children[0]).toEqual({ tag: 'String', value: 'Item B' });

// Row > Text: caption + error
const rowTextC = rowNode.value.children[3];
expect(rowTextC.tag).toBe('Text');
expect(rowTextC.value.props.style).toBe('caption');
expect(rowTextC.value.props.color).toBe('error');
expect(rowTextC.value.props.style).toBe('body.small.regular');
expect(rowTextC.value.props.color).toBe('fg.error');
expect(rowTextC.value.children[0]).toEqual({ tag: 'String', value: 'Item C' });

// Text: headline + textPrimary
// Text: headline.large + fg.primary
expect(headlineText.tag).toBe('Text');
expect(headlineText.value.props.style).toBe('headline');
expect(headlineText.value.props.color).toBe('textPrimary');
expect(headlineText.value.props.style).toBe('headline.large');
expect(headlineText.value.props.color).toBe('fg.primary');
expect(headlineText.value.children[0]).toEqual({ tag: 'String', value: 'Balance: 100 DOT' });

// Spacer: fillMaxHeight
Expand Down
39 changes: 39 additions & 0 deletions __tests__/hostApi/payments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ describe('Host API: Payments', () => {

expect(received).toEqual([{ available: 100n }]);
});

it('should pass the selected purse to handler', async () => {
const { container, payments } = setup();
const handler = vi.fn<ContainerHandlerOf<typeof container.handlePaymentBalanceSubscribe>>(() => noop);
container.handlePaymentBalanceSubscribe(handler);

payments.subscribeBalance(noop, 7);

await delay(50);

expect(handler).toHaveBeenCalledWith({ purse: 7 }, expect.anything(), expect.anything());
});
});

describe('topUp', () => {
Expand Down Expand Up @@ -72,6 +84,21 @@ describe('Host API: Payments', () => {
);
});

it('should pass the selected purse (into) to handler', async () => {
const { container, payments } = setup();
const handler = vi.fn<ContainerHandlerOf<typeof container.handlePaymentTopUp>>((_params, { ok }) =>
ok(undefined),
);
container.handlePaymentTopUp(handler);

await payments.topUp(200n, { type: 'productAccount', derivationIndex: 2 }, 5);

expect(handler).toHaveBeenCalledWith(
{ into: 5, amount: 200n, source: { tag: 'ProductAccount', value: 2 } },
expect.anything(),
);
});

it('should reject with InsufficientFunds', async () => {
const { container, payments } = setup();

Expand Down Expand Up @@ -117,6 +144,18 @@ describe('Host API: Payments', () => {
expect(handler).toHaveBeenCalledWith({ amount: 300n, destination }, expect.anything());
});

it('should pass the selected purse (from) to handler', async () => {
const { container, payments } = setup();
const handler = vi.fn<ContainerHandlerOf<typeof container.handlePaymentRequest>>((_params, { ok }) =>
ok({ id: 'p-1' }),
);
container.handlePaymentRequest(handler);

await payments.requestPayment(300n, destination, 9);

expect(handler).toHaveBeenCalledWith({ from: 9, amount: 300n, destination }, expect.anything());
});

it('should reject with Rejected', async () => {
const { container, payments } = setup();

Expand Down
Loading