Skip to content

Pair a Blox via FxBlox Web (blox.fx.land) + web receiver - #114

Closed
ehsan6sha wants to merge 5 commits into
mainfrom
feat/blox-web-handoff
Closed

Pair a Blox via FxBlox Web (blox.fx.land) + web receiver#114
ehsan6sha wants to merge 5 commits into
mainfrom
feat/blox-web-handoff

Conversation

@ehsan6sha

Copy link
Copy Markdown
Member

Lets FxFiles hand pairing off to FxBlox Web (https://blox.fx.land) when the mobile FxBlox app isn't available, and receive the result on the web build.

  • lib/core/services/blox_pairing_links.dart (dart:io-free): buildBloxWebPairUrl (v1.1 fragment carrier: https://blox.fx.land/autopin-pair#token=…&endpoint=…&returnUrl=… — the cloud JWT never reaches a server/CDN log or Referer), buildBloxNativePairUrl (unchanged fxblox://…), kAutopinReturnTemplate (https fragment form with the four $placeholders FxBlox substitutes), parseAutopinCompleteParams (fragment-first). 32 tests.
  • Sender: kIsWeb → web URL; native tries fxblox:// first, then offers "Pair in browser"; desktop manual dialog gets the same button.
  • Return path: site/autopin-complete/index.html forwarder (open app or "Continue in web app"), native universal-link arm /autopin-complete (+ AASA path, Android app-link data), web receiver captureAutopinReturn() before runApp (stash + replaceState), post-login hand-off to the new dart:io-free web_blox_pairing_screen.dart (paired state, validate + persist, Pair/Unpair, reveal/copy secret), Settings "My Devices".
  • Docs: docs/AUTOPIN-HANDOFF.md (v1.1 spec), architecture.md §2, README.

Verified: Flutter 3.41.9 — new tests 52/52; flutter analyze on touched files clean except a pre-existing unused import in blox_pairing_screen.dart:13; flutter build web --release -t lib/main_web.dart --base-href /app/ --pwa-strategy=none --no-wasm-dry-run builds. Not verified: a real round-trip (blox.fx.land /autopin-pair ships with functionland/fxblox-web).

🤖 Generated with Claude Code

https://claude.ai/code/session_01QyQw3WtVXDTtvJKN7ykczw

ehsan6sha and others added 5 commits August 27, 2026 09:55
…tive)

Adds lib/core/services/blox_pairing_links.dart (dart:io-free): the
buildBloxWebPairUrl / buildBloxNativePairUrl builders, the v1 return
template in the https FRAGMENT form (the bearer secret never reaches a
server), and parseAutopinCompleteParams(Uri) which reads the fragment
first, then the query, plus AutopinCompleteParams validation (non-empty
secret, per-field length caps, no control chars). The four
$placeholders stay literal (raw strings). Unit-tested.

Sender (blox_pairing_screen.dart): builds the blox.fx.land URL with the
SAME params; on kIsWeb uses it directly; on native tries fxblox:// first
and, when launchUrl returns false or throws, offers "Pair in browser"
instead of the old "app not installed" dead end. Desktop's manual
pairing dialog gains a "Pair in browser" button.

Receiver (deep_link_service.dart): /autopin-complete arm in the
universal-link handler reusing _handleAutoPinComplete, which now uses
the shared parser + validation. Android app-link <data> for
/autopin-complete and the AASA path so the OS opens FxFiles directly
(both preserve the fragment).

Contract: docs/AUTOPIN-HANDOFF.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyQw3WtVXDTtvJKN7ykczw
captureAutopinReturn() runs in main_web.dart BEFORE runApp: it reads
#/autopin-complete?secret=... (or the bare-fragment / query forms) from
the location, stashes the params (memory + sessionStorage so a refresh
mid-sign-in does not lose them) and history.replaceState-strips them to
#/ so the secret leaves the address bar and the logged-out router
redirect cannot drop it. The web home's post-login init takes the
pending return and navigates to /blox-pairing with the params as
go_router extra (never a query). /autopin-complete also exists as a
router fallback (logged out: the redirect parks the params; signed in:
the screen persists them then cleans the URL).

New dart:io-free lib/web/screens/web_blox_pairing_screen.dart: shows the
paired state from SecureStorageKeys.blox*, validates + writes incoming
params, "Pair Blox" opens buildBloxWebPairUrl(...) in the same tab,
"Unpair" clears the keys, reveal/copy of the secret behind a confirm,
and the inline LAN-gateway limitation note. Settings gains a
"My Devices" section linking to /blox-pairing.

The pure parsing/stripping/session-encoding lives in
web_autopin_return_logic.dart (VM-tested) behind the same
conditional-export pattern as web_hosted_oauth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyQw3WtVXDTtvJKN7ykczw
site/autopin-complete/index.html (cloned from nft-claim): reads
secret/hardwareId/bloxPeerId/bloxName from the fragment (query
fallback); on mobile UA auto-tries fxfiles://autopin-complete?... with
the 2.5 s document.hidden fallback (store links only, never a second
navigation); otherwise "Open in FxFiles" + "Continue in web app" ->
https://files.fx.land/app/#/autopin-complete?... (params stay in the
hash, client-side).

Docs: docs/AUTOPIN-HANDOFF.md (copy of the v1 contract), architecture.md
section 2 (entry points + URL schemes incl. the web hand-off and the
web LAN limitation), README web section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyQw3WtVXDTtvJKN7ykczw
From the advisor review of the hand-off:
- Web fallback route: after persisting the /autopin-complete?secret=...
  params, REPLACE the route (context.replace) instead of go() so the
  secret-carrying history entry is overwritten and Back cannot re-run
  the persist.
- Forwarder (site/autopin-complete): history.replaceState-strip the
  fragment as soon as the params are captured so the secret does not
  linger in history / a shared URL; a reload shows "incomplete or
  already used".
- Shared isAutopinReturnRoutePath() (tolerates a trailing slash) used
  by the parser, the web strip logic and the native universal-link
  arm; tests added for the trailing-slash forms.

Not changed (contract): the outbound token stays in the query per
AUTOPIN-HANDOFF.md v1; the fragment-form recommendation is recorded in
the status doc for the spec owner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyQw3WtVXDTtvJKN7ykczw
…MENT (v1.1)

buildBloxWebPairUrl now emits
  https://blox.fx.land/autopin-pair#token=<t>&endpoint=<e>&returnUrl=<r>
(no query) so the cloud JWT never reaches the blox.fx.land server / CDN
logs, a Referer header, or synced history. The native fxblox:// URL is
unchanged (query; OS-routed, no server). Call sites are untouched (they
use the builder). Contract bumped to v1.1 in docs/AUTOPIN-HANDOFF.md
with the receiver rule: read location.hash first, the v1 query form
stays accepted as a fallback. architecture.md updated. Tests updated +
a new "no query / JWT not before the #" test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyQw3WtVXDTtvJKN7ykczw
@ehsan6sha

Copy link
Copy Markdown
Member Author

Superseded by #115, which is rebuilt on the current main. This branch was created before ~79 commits landed, so merging it would have reverted them — deleting test files and rolling back web/pkg/fula_flutter_bg.wasm, windows/fula_flutter.dll and web/pkg/VERSION. #115 leaves those byte-identical to main and deletes nothing.

@ehsan6sha ehsan6sha closed this Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant