Skip to content

Blox pairing hand-off to blox.fx.land (rebuilt on current main) - #115

Merged
ehsan6sha merged 5 commits into
mainfrom
feat/blox-web-handoff-v2
Aug 28, 2026
Merged

Blox pairing hand-off to blox.fx.land (rebuilt on current main)#115
ehsan6sha merged 5 commits into
mainfrom
feat/blox-web-handoff-v2

Conversation

@ehsan6sha

Copy link
Copy Markdown
Member

Rebuilt on top of the current main (e94b769). Supersedes #114, which had been branched before ~79 commits landed and would have reverted them — deleted test files and rolled back web/pkg/fula_flutter_bg.wasm, windows/fula_flutter.dll and web/pkg/VERSION. Those artifacts are now byte-identical to main and nothing is deleted.

What this adds

FxFiles already pairs with a Blox: blox_pairing_screen.dart builds fxblox://autopin-pair?token=&endpoint=&returnUrl= and, if the native app is not installed, dead-ends on a snackbar. The Blox web app (blox.fx.land) has no URL scheme, so that hand-off can never reach it.

This adds the web arm of the same flow: an https://blox.fx.land/autopin-pair?... link alongside the existing scheme, a receiver for the return, and the static /autopin-complete forwarder. The return params travel in the URL fragment, so the pairing secret is never sent to a server or written into the address bar.

Resolved against main rather than reimposed

Three files had moved under me and were resolved on main's terms:

  • web_home_screen.dart — main removed _completeHostedOauthIfAny; only the Blox hand-off call was re-applied, not the deleted OAuth code.
  • web_settings_screen.dart — main reorganised settings behind a More expander (Settings: profile + billing on top, everything else behind More #113), so the Blox entry was added inside _moreSection instead of restoring the old flat list. The old _aiConnectionsSection was dropped since main now covers it with _integrationsSection.
  • main_web.dart — both imports kept.

Verified

  • flutter analyze on the changed files: no issues
  • flutter test on both suites: 52 passed
  • 0 commits behind main; diff touches only hand-off files (+2270 / -33), no deletions

🤖 Generated with Claude Code

ehsan6sha and others added 5 commits August 27, 2026 19:19
…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
the location, stashes the params (memory + sessionStorage so a refresh
mid-sign-in does not lose them) and history.replaceState-strips them to
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
ehsan6sha merged commit 06e55df into main Aug 28, 2026
3 of 4 checks passed
@ehsan6sha
ehsan6sha deleted the feat/blox-web-handoff-v2 branch August 28, 2026 14:36
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