Skip to content

feat(sign): prepareSign() — pre-warm a Yivi session for one-tap app open on iOS#103

Merged
rubenhensen merged 1 commit into
mainfrom
feat/prepare-sign-prewarm
Jul 15, 2026
Merged

feat(sign): prepareSign() — pre-warm a Yivi session for one-tap app open on iOS#103
rubenhensen merged 1 commit into
mainfrom
feat/prepare-sign-prewarm

Conversation

@rubenhensen

Copy link
Copy Markdown
Contributor

Why

On iOS a Yivi Universal Link only opens the app when the navigation happens inside a genuine user gesture. Today the signing session is started as part of encrypt() — i.e. after the user taps send — so the app deep-link URL doesn't exist yet at tap time. There's nothing to navigate to synchronously, so the tap falls back to opening open.yivi.app in Safari instead of the app.

This adds the primitive needed to fix that in the website: start the session before the tap, so the URL is ready and a single tap can open the app.

What

PostGuard.prepareSign(opts) starts the Yivi signing session ahead of time and returns:

  • mobileUrl: Promise<string> — the Yivi app deep-link, resolved as soon as Yivi shows its mobile app button. Captured via a small passive yivi-core state plugin that reads the ShowingYiviButton payload's mobile field — no DOM scraping. Put it on an <a href> so one tap opens the app. Mobile-only (desktop uses the QR flow), so race with a timeout there.
  • keys: Promise<SigningKeys> — resolves on disclosure; pass into encrypt({ signingKeys }).
  • cancel() — aborts the in-flight session.

The disclosure is identity-bound (sender email + optional attributes) and independent of the files/recipients, so the resolved keys are valid for whatever is ultimately encrypted.

Supporting changes:

  • EncryptInput gains an optional signingKeys field. When supplied, Sealed.getSigningKeys() uses it directly and never starts a second Yivi session (the sign.element widget is never touched). The internal pipeline already threaded signingKeys; this just surfaces it publicly.
  • resolveSigningKeysFromYivi gains a runtime arg (onMobileUrl, signal) for the URL capture and AbortSignal cancellation.
  • Exports PrepareSignOptions, PreparedSign, SigningKeys.

Usage sketch (website side, follows separately)

// when the compose form becomes valid:
const prep = pg.prepareSign({ element: '#hidden-yivi', attributes: SIGN_ATTRS, includeSender: true });
const href = await prep.mobileUrl;         // put on the "Send" <a href>
// user taps the anchor -> Yivi app opens (one gesture)
const signingKeys = await prep.keys;       // resolves after disclosure
await pg.encrypt({ files, recipients, sign, signingKeys }).upload({ notify });

Tests

  • prepareSign returns { mobileUrl, keys, cancel }; rejects both keys and mobileUrl with YiviSessionError when no DOM; rejects immediately on an already-aborted signal.
  • encrypt({ signingKeys }) short-circuits session resolution (no "requires a DOM" throw when keys are supplied).
  • Full suite: 209 passing; tsc --noEmit clean; tsdown build clean.

The runtime mobileUrl capture path needs a browser + live Yivi server to exercise end-to-end; that happens in the website wiring PR and an on-device iOS check.

…app open

On iOS a Yivi Universal Link only opens the app when the navigation happens
inside a genuine user gesture. If the signing session is started on tap (as
part of encrypt()), the app deep-link URL doesn't exist yet, so the tap can't
navigate synchronously and falls back to Safari.

prepareSign() starts the Yivi signing session ahead of time and returns:
  - mobileUrl: Promise<string> — the app deep-link, resolved as soon as Yivi
    shows its mobile button (captured via a passive yivi-core state plugin,
    not DOM scraping), so a caller can put it on an <a href> for a one-tap open;
  - keys: Promise<SigningKeys> — resolved on disclosure, to feed into encrypt();
  - cancel() — aborts the in-flight session.

The disclosure is identity-bound (sender email + optional attributes) and
independent of the files/recipients, so the keys are reusable. EncryptInput now
accepts a `signingKeys` field; when supplied, Sealed.getSigningKeys() uses it
directly and never starts a second session (the internal pipeline already
threaded signingKeys). Adds AbortSignal support to the Yivi resolver.

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — prepareSign() is a clean, well-scoped addition.

Rule check (Haiku fan-out + manual verification): no violations.

  • Conventional PR title (feat(sign):) ✓
  • Tests present for the new surface: handle shape, no-DOM rejection of both keys and mobileUrl, already-aborted signal, and encrypt({ signingKeys }) short-circuit ✓
  • No vi.mock hoisting trap (only vi.stubGlobal) ✓
  • Client-side JWT trust boundary untouched by this diff ✓

Review notes (all confirmed correct, nothing blocking):

  • mobileUrl.catch(() => {}) prevents an unhandled rejection on the desktop path where nobody awaits the URL; the failure still surfaces via keys.
  • MobileUrlCapture fires at most once (fired guard) and has no start, so yivi-core never drives it — passive listener only.
  • Abort wiring is sound: pre-aborted signal short-circuits before any widget construction, and the runtime listener uses { once: true }.
  • signingKeys short-circuit in Sealed.getSigningKeys() returns before any Yivi session starts.

Full suite green (209/209), tsc --noEmit and tsdown clean. The runtime mobileUrl capture path still needs the on-device iOS check noted in the PR — tracked for the website wiring PR.

@rubenhensen
rubenhensen merged commit f8bd16b into main Jul 15, 2026
5 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@dobby-coder
dobby-coder Bot deleted the feat/prepare-sign-prewarm branch July 16, 2026 20:01
dobby-coder Bot added a commit to encryption4all/postguard-docs that referenced this pull request Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant