Skip to content

feat(ios,macos): seedphrase sign-in, instant account creation, multi-method auth settings#331

Open
Ryanmello07 wants to merge 122 commits into
urnetwork:mainfrom
Ryanmello07:feat/seedphrase-auth-upstream
Open

feat(ios,macos): seedphrase sign-in, instant account creation, multi-method auth settings#331
Ryanmello07 wants to merge 122 commits into
urnetwork:mainfrom
Ryanmello07:feat/seedphrase-auth-upstream

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

Summary

Ports the seedphrase-auth feature from our beta fork (Ryanmello07/urnetwork-ios#4, built against beta/custom-server) upstream. Adds:

  • Seedphrase login and instant (no-verification) account creation, with a seedphrase display/backup screen on both iOS and macOS.
  • A rebuilt Settings > Sign-In Methods section (shared between iOS and macOS) that lists and manages every bound auth method live from the server's auth_types array, with an AddAuthSheet supporting email/password, Apple, Google, and Solana wallet binding.
  • Profile's claim-vs-change network name flow now checks live auth_types instead of a frozen legacy field.
  • Guest mode removed (GuestModeSheet, GuestUpgradeViewModel, and the associated create-network guest path deleted).

This is the net diff of the beta PR, squashed into one commit — that branch's own history is ~70 commits, mostly iterative CI/build-error/type-checker-timeout fixes with no standalone review value upstream.

⚠️ Blocked on SDK upstream PR

This depends on urnetwork/sdk#132 (AuthTypes/WalletAddress fields and the seedphrase/wallet-auth endpoints this UI calls), which is still open. Package.swift here points at the stock sdk/main sibling checkout, so this won't build until #132 merges. Opening as a draft for that reason — will mark ready for review once the SDK PR lands and CI is green here.

Test plan

  • urnetwork/sdk#132 merges
  • CI green on this PR (iOS + macOS unsigned build)
  • Manual smoke test: instant account creation, seedphrase login, add/remove each auth method (email, Apple, Google, Solana wallet), network name claim/change for both seedphrase-only and wallet-only accounts

🤖 Generated with Claude Code

Ryanmello07 and others added 30 commits July 2, 2026 18:55
Updates the local binary target path from
to  so it resolves against the
Ryanmello07/urnetwork-sdk fork checked out next to this repo.
Builds the forked urnetwork-sdk for iOS, then archives and packages an
unsigned IPA on pushes to beta/custom-server and workflow_dispatch. Uploads
the IPA to a GitHub prerelease on the forked iOS repo.
Upstream renamed AuthNetworkClientArgs.Description to DeviceDescription
in the Go SDK specifically because 'description' collides with the
read-only NSObject.description property in the gomobile Swift bindings -
which is exactly the compile error this fixes ("cannot assign to
property: 'description' is immutable").
Ports Android's NetworkServerSelector.kt to iOS/macOS. Lets a user point
the app at a different network domain (self-hosted or alternate) instead
of the hardcoded ur.network, or set explicit API/connect url overrides.

- NetworkConfig.swift: names the official host/env/store/wallet/sso
  defaults that were previously inlined in DeviceManager with a
  "TODO: this should be moved into a config" comment. Values are
  unchanged - this only names them so a reset button has something to
  reset to. Note ios's wallet="circle" default is intentionally left
  alone (not copied from Android's "solana"/"ethos" flavor values,
  which govern something else - Solana wallet sign-in already works
  correctly here independent of this field).
- DeviceManager.swift: exposes the underlying SdkNetworkSpaceManager
  (previously a local var, only used once at init) plus current/
  configured api+connect url accessors, and an applyNetworkSpace(...)
  method that switches the active network space. No SDK/Go changes
  needed - NetworkSpaceManager/NetworkSpace already exist in the shared
  Go SDK and are used identically by Android.
- NetworkServerUtils.swift: Swift port of the Kotlin normalization
  helpers (host/url normalization, insecure-scheme detection, derived
  service url preview).
- NetworkServerSheet(+ViewModel): the settings sheet itself, modeled on
  the existing UpdateReferralNetworkSheet pattern already used elsewhere
  in this codebase.
- LoginInitialView.swift: adds a "Change Network API" entry point next
  to (but outside the iOS-only gating of) "Try Guest Mode", available
  on both iOS and macOS per direction - unlike Solana sign-in, which
  stays iOS-only since it needs a browser-based flow on desktop that
  isn't built yet.
…rnetwork-ios into beta/custom-server

# Conflicts:
#	app/network/Authenticate/LoginInitial/LoginInitialView.swift
UrTextField.placeholder is LocalizedStringKey, which only auto-converts
from string literals, not String variables. Wrap the three computed
placeholder values (officialHostName, derivedApiUrl, derivedConnectUrl)
explicitly.
The SDK build (make build_apple) already produces a macOS xcframework
slice (macos/arm64, macos/amd64), and the app project already declares
macosx in SUPPORTED_PLATFORMS, but the workflow never built or
published a macOS app. Add an unsigned macOS archive + zip package and
attach it to the prerelease alongside the iOS IPA.
The macOS SSOButtons variant showed the Solana sign-in button
unconditionally, unlike the iOS variant which only shows it when a
Solana wallet app is installed. Remove it from macOS entirely; wallet
address entry elsewhere (post-login wallet management) is unaffected.
Same NetworkClientInfo.Description -> DeviceDescription rename as
Android's SettingsViewModel fix. This one silently compiled (Swift's
NSObject.description is a valid read-only String getter), but returned
the object's default description instead of the actual device
description text in the Settings device list.
- apply(hostName:apiUrl:connectUrl:) now normalizes its own hostName
  parameter instead of reading viewModel.normalizedHostName, so the
  method behaves correctly regardless of view-model state (previously
  harmless only because every call site happened to pass a value
  already in sync with the view model).
- Fix a misleading error message shown when fetching the Solana
  wallet-auth challenge from the server fails, before any wallet
  interaction happens - it previously said 'error connecting to your
  wallet'.
NetworkServerSheet.swift doesn't reference any Sdk* types directly -
it takes plain String/closure parameters and delegates the actual SDK
calls to DeviceManager.
Same bug as Android's NetworkServerSelector (this file is an explicit
Swift port of that normalization logic, per its own doc comment):
normalizeNetworkHost preserved a trailing :port, which
derivedServiceUrl then prepended a service prefix to, producing
invalid hosts like 'api.192.168.1.5:8080'. Not flagged by Copilot on
this PR, but the identical root cause was flagged on the Android PR
and applies here too.
…ng Solana

Bittensor sign-in was still signing a static hardcoded message
('Welcome to URnetwork') via the ur.io/wallet-connect bridge - the same
replay-risk this project fixed for Solana. Give it the identical
server-issued-challenge flow:

- prepareBittensorChallenge() mirrors prepareSolanaChallenge(), fetching
  a fresh challenge via the same AuthWalletChallenge SDK API with
  blockchain="bittensor".
- handleBittensorSignIn() now fetches the challenge before opening the
  wallet-connect bridge, instead of using the static welcomeMessage.
- handleBittensorWalletResult() gained the same isSigningForCreateNetwork
  gate handleSolanaWalletResult() already had, so a fresh challenge gets
  signed before creating a network from a wallet login too.
- Fixed a real pre-existing bug in the .create branch of
  handleAuthLoginResult(): it unconditionally re-fetched a Solana
  challenge and re-presented the Solana sheet regardless of which wallet
  the user actually authenticated with, so a Bittensor login landing in
  .create would have incorrectly tried to re-sign via Solana. It now
  branches on connectWalletProviderViewModel.connectedWalletProvider.

CAVEAT: the exact blockchain identifier string the backend's
/auth/wallet-challenge endpoint expects for Bittensor is unverified from
this client-side change alone - "bittensor" was chosen by symmetry with
the existing "solana" value sent for that wallet. This needs to be
confirmed against the live server (or with the backend team) before
this is considered production-ready; if the identifier is wrong, the
challenge fetch will fail closed (clear network error, sign-in blocked)
rather than silently falling back to an insecure client-side message.
…server#402

The 'bittensor' blockchain string sent to /auth/wallet-challenge was
previously an educated guess (by symmetry with Solana's 'solana').
urnetwork/server#402 has since been extended to accept it (and 'tao'/
'TAO', case-insensitively) - this is now a confirmed contract, not an
assumption.
The ad and tracker blocker upstream merge added .bittensor to the
ConnectedWalletProvider enum but the isWalletAppInstalled switch wasn't
updated. Bittensor wallets are browser extensions (not native apps) so
return true like the macOS codepath — the ur.io/wallet-connect bridge
handles detection.
…tion, bittensor switch

- Guard against empty messageTemplate in prepareSolanaChallenge and
  prepareBittensorChallenge (return false instead of silently accepting).
- Preserve IPv6 literals when stripping ports in normalizeNetworkHost:
  bracketed [2001:db8::1]:8080 keeps the address, bare IPv6 is not
  mangled, IPv4 host:port continues to strip correctly.
- Add .bittensor case to isWalletAppInstalled switch (browser extension
  wallets are always reachable via the ur.io bridge).

Copilot review on apple#327.
Ryanmello07 and others added 26 commits July 18, 2026 00:39
…igninWithSolanaSheet, isSigningMessage, claimSeekerTokenMessage)
…Locally hack

- Replace parseAuthMethods to read from networkUser.authTypes array
- Remove @published var hasSeedphraseLocally from SettingsViewModel
- Remove error-substring detection for 'already exists'
- Remove hasSeedphrase() helper, replace with authTypesContains()
- Seedphrase section reads from authTypes to show generate vs regenerate
- macOS form updated with same authTypes closure pattern
…ations

- Added networkUserViewModel reference to ViewModel
- Refresh NetworkUser data after generate/regenerate/removeAuth
- Pass networkUserViewModel through to ViewModel init
The SDK's new AuthTypes field is a Go pointer (*StringList), which
gomobile bridges to Swift as SdkStringList?. Update the seedphrase
detection and auth-methods parsing helpers to safely unwrap it
instead of assuming a non-optional value, matching the pattern
already used in DnsSettingsStore/BlockActionsStore.
Not meant to ship — was accidentally committed alongside the auth_types refactor.
The seedphrase-display/success-handling flow was wrapped in #if os(iOS)
only, with no macOS branch. macOS users creating an instant account got
no seedphrase display and no success handling at all -- the account was
created server-side but the app just sat on the create screen.

Use .sheet(item:) + .interactiveDismissDisabled(true) on macOS, since
.fullScreenCover isn't available there.
Three issues in the same file:

- None of AddAuthSheet's four success paths (wallet, Apple, Google,
  generic addAuth) called refreshNetworkUser() after linking a new auth
  method, so Settings kept showing the old method list until an
  unrelated refresh happened. Now takes a NetworkUserViewModel and
  refreshes after each successful add.
- pollForWalletConnection() polled for up to 60s with no cancellation
  wired to sheet dismissal, and the try? around Task.sleep silently
  swallowed cancellation errors even if something did cancel it. Now
  stores the connect Task, cancels it (and clears the pending wallet
  signature handler) in onDisappear, and the poll loop checks
  Task.isCancelled explicitly instead of relying on the swallowed throw.
- Removed handleWalletAuth(), a function with zero call sites anywhere
  in the repo -- superseded by the connectWallet/signWalletChallenge flow.
macOS's Sign-In Methods section (net-new in this PR) only ever rendered
the single legacy authType/userAuth fields and had no per-method Remove
button -- the confirmation dialog for it was already wired in
SettingsView.swift but nothing on macOS ever triggered it. A user with
Google+Apple+Seedphrase all linked saw only one method on macOS and
couldn't remove any of them there, while iOS correctly listed and could
remove each one.

Extracted authTypesContains()/parseAuthMethods()/methodDisplayName()
(previously private to SettingsForm-iOS, and partly reimplemented inline
on macOS) into a shared AuthMethods.swift, and rebuilt macOS's Sign-In
Methods section to ForEach over authTypes with a Remove button, matching
iOS.
ProfileView branched on the legacy singular authType field to choose
claimNetworkName() vs saveNetworkName(). AddAuth never updates that
field server-side (only RemoveAuth's COALESCE does), so an account
created as seedphrase-only keeps authType == "seedphrase" forever, even
after the user binds a real email/Google/Apple identity -- Profile would
keep calling the first-time claim flow indefinitely instead of switching
to the normal rename flow once a verified identity exists.

Replaced the authType: String? param with needsNameClaim: Bool, computed
from the live authTypes array as "no email/google/apple bound yet",
matching the server's own requireEmailOrSsoBound gate that both
claim-name and change-name are subject to.
isSeedphraseValid only checked the raw string was non-empty after
trimming, so a single stray character enabled the Sign In button and
hit the server with obviously-invalid input. validateWordCount() already
computed the real 12/24-word check but only drove a warning label, never
the actual submit gate. Now isSeedphraseValid uses the same word-count
check.
…etwork

createInstantAccount's callback never checked result.verificationRequired,
unlike sibling createNetwork on the same shared NetworkCreateCallback/
result type. If the server ever returns verificationRequired for an
instant-account response, the code previously fell through to a generic
"No seedphrase in result" error instead of a clear message.
These two new macOS buttons applied .disabled(isLoginActionInFlight) but,
unlike sibling Apple/Auth-Code buttons in the same view, never dimmed on
that state -- they went non-interactive while staying at full opacity,
giving no visual cue.
executeGenerateSeedphrase, executeRegenerateSeedphrase, and
executeRemoveAuth each fired a refreshNetworkUser() network round-trip
unconditionally after their do/catch block, so a caught failure still
triggered a redundant fetch of the user record that couldn't have
changed. Scoped the refresh to the success path in each.
The needsNameClaim computation (added in 8cb76da) checked authTypes for
email/google/apple but missed "phone" -- a distinct auth_type value from
"email" server-side, and a reachable sign-up/login path in this app
(LoginInitialView/CreateNetworkView both offer "Email or phone number").
A phone-authenticated account was incorrectly treated as having no
identity method bound, routing it to the claim-name flow instead of the
normal change-name flow. Found in re-review before this landed anywhere.
…tity for name claim

ProfileView reads the displayed name from the cached JWT, which the
change/claim-name calls don't reissue, so the screen (and anywhere
else the JWT-derived name is read) kept showing the old name until
the SDK's next background refresh. Force a refresh on success, the
same way SubscriptionBalanceViewModel does after a balance change.

Also recognize a bound wallet ("solana") as a valid identity method
for needsNameClaim, matching the backend's requireVerifiedIdentityBound
change that now accepts wallet-bound users for claim/change-name.
Cold launch already gets a fresh JWT (the SDK's token manager
refreshes once immediately on start), but resuming from the
background doesn't -- the token manager only refreshes on its own
schedule (~14 days out from expiration) or when explicitly asked. Add
a scenePhase hook to force a refresh on foreground so a network name
(or anything else JWT-derived) changed from another device or the web
dashboard shows up without needing a full logout/login.
…d multi-method auth settings

Adds a full seedphrase-based auth flow alongside the existing
email/SSO/wallet paths:

- Seedphrase login and instant (no-verification) account creation,
  with a seedphrase display/backup screen on both iOS and macOS.
- A rebuilt Settings > Sign-In Methods section (shared between iOS and
  macOS) that lists and manages every bound auth method live from the
  server's auth_types array, with an AddAuthSheet supporting
  email/password, Apple, Google, and Solana wallet binding.
- Profile's claim-vs-change network name flow now checks live
  auth_types instead of a frozen legacy field, so it correctly detects
  when a user still needs to claim a name.
- Guest mode removed (GuestModeSheet, GuestUpgradeViewModel, and the
  associated create-network guest path deleted).

This is the exact diff behind #4, squashed
into one commit for this upstream PR (that branch's own history is
~70 commits, mostly iterative CI/build-error/type-checker-timeout
fixes with no standalone review value).

Depends on urnetwork/sdk#132 (AuthTypes/WalletAddress fields and the
seedphrase/wallet-auth endpoints this UI calls) -- opening as a draft
until that lands, since this won't build against a Package.swift
pointed at stock upstream sdk/main until then.
…-auth-upstream

Brings ~25 real fix commits from the actively-developed fork branch
into the upstream-facing PR (urnetwork#331) that had drifted behind since its
initial squash: JWT-refresh-on-foreground timing, stale UI after
network name change, phone auth in name-claim identity check, macOS
Sign-In Methods list/remove parity, several AddAuthSheet bugs (stale
UI, uncancelled wallet poll, dead code), and SwiftUI type-checker
splits/syntax fixes in SettingsView.

Only conflict was LoginInitialView.swift: both branches had already
independently landed the identical "dim disabled macOS buttons"
opacity fix -- pure duplicate content differing only by a blank line,
resolved by keeping one copy.
Not for upstream
Comment thread app/URnetworkSdk/Package.swift Outdated
Comment on lines +4 to +6
// see https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages
// Points to the local checkout of the forked SDK:
// https://github.com/Ryanmello07/urnetwork-sdk
// Build the XCFramework from that repo before using this package.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Undo changes, not for upstream use

Comment thread app/URnetworkSdk/Package.swift Outdated
Comment on lines +16 to +18
path: "../../../sdk/build/apple/URnetworkSdk.xcframework"
path: "../../../urnetwork-sdk/build/apple/URnetworkSdk.xcframework"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Undo changes, not for upstream use

var connectedWalletProvider: ConnectedWalletProvider? = nil

let welcomeMessage = "Welcome to URnetwork"
let claimSeekerTokenMessage = "Claim point multiplier by holding Seeker Pre-order or Saga Genesis token"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We should follow upstream and remove seeker token stuff

Ryanmello07 and others added 2 commits July 21, 2026 19:45
Addresses the three review comments left on this PR before upstream
submission:

- app/URnetworkSdk/Package.swift: restore upstream's original doc
  comment and xcframework path (../../../sdk/... instead of the
  fork's ../../../urnetwork-sdk/...), undoing local dev pointers to
  the forked SDK checkout.
- Remove the fork-only "Claim Seeker Token" flow from Settings:
  drop ConnectWalletProviderViewModel.claimSeekerTokenMessage, the
  SolanaSignMessageSheet .sheet(...) block in SettingsView wired to
  it, and the associated presentSigninWithSolanaSheet/
  isSigningMessage/setIsSigningMessage state in SettingsViewModel.
  The unrelated login-flow usage of SolanaSignMessageSheet and the
  handleWalletDeepLink/.onOpenURL wallet AddAuth deep-link handling
  in Settings are untouched.
Leftover from the removed fork-only Settings seeker-token sheet
(SettingsView.swift/SettingsViewModel.swift, previous commit) -- this
string-table entry had no remaining code reference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtgqtCmKJRXdsQ5ktiqwkg
@Ryanmello07
Ryanmello07 marked this pull request as ready for review July 23, 2026 22:49
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.

2 participants