Skip to content

feat: seedphrase sign-in, instant account creation, and multi-method auth settings - #466

Merged
Ryanmello07 merged 11 commits into
urnetwork:mainfrom
Ryanmello07:feat/seedphrase-auth-android-upstream
Jul 30, 2026
Merged

feat: seedphrase sign-in, instant account creation, and multi-method auth settings#466
Ryanmello07 merged 11 commits into
urnetwork:mainfrom
Ryanmello07:feat/seedphrase-auth-android-upstream

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

Summary

Adds seedphrase-based authentication to the Android app, matching the iOS port (urnetwork/apple#331) and the corresponding server work (urnetwork/server#406):

  • Seedphrase sign-in + instant account creation across all 4 flavors (github, play, solana_dapp, ethos_dapp): new LoginSeedphrase, CreateNetworkInstant, and SeedphraseDisplayScreen screens, wired into each flavor's login flow.
  • Guest-mode removal: guest account creation, the guest onboarding sheet/overlay, and all associated guest-only UI branches are removed in favor of the seedphrase flow.
  • Sign-In Methods management: a rebuilt AddAuthMethodSheet (email/password, Google, Solana wallet) and a Settings "Sign-In Methods" list, matching iOS's AddAuthSheet.swift structure.
  • Seedphrase generate/regenerate: a Settings section above Sign-In Methods, since Api.addAuth doesn't return the generated phrase — only the dedicated generateSeedphrase/regenerateSeedphrase calls do.
  • Profile name-claim + rename: migrated off the legacy, cooldown-free NetworkUserUpdate endpoint onto changeNetworkName/claimNetworkName (matching iOS), with claim-vs-change UX and JWT-refresh-on-foreground so a rename is reflected without a fresh login.

Provenance

This is the squashed, upstream-facing recombination of four PRs merged into the fork's integration branch, each independently reviewed and CI-verified there (Ryanmello07/urnetwork-android):

  • build fixes #6 — seedphrase login, instant account creation, guest-mode removal (all 4 flavors)
  • Initial login screen #7 — Sign-In Methods list + AddAuthMethodSheet
  • V0 #8 — Profile name-claim + rename migration
  • Login password #9 — Generate/Regenerate Seedphrase Settings section + AddAuthMethodSheet fix (feature commits only; fork-internal CI-retrigger and sync-merge commits excluded)

Recombining onto upstream/main directly (rather than merging the fork's integration branch verbatim) surfaced only two conflicts, both mechanical: a fork-only CI workflow file that doesn't exist upstream (dropped, correctly), and an adjacent string-resource insertion in strings.xml (both sides' strings kept). No logic conflicts. Fork-internal planning/spec docs (docs/superpowers/...) were stripped as not relevant upstream.

Verification

No local Android build toolchain or Android SDK available in the environment this was prepared in, and urnetwork/android has no CI workflow configured to validate this automatically. However, the underlying code was already build-verified per-PR on the fork's own CI (all 4 flavors, github/play/solana_dapp/ethos_dapp) before each of #6/#7/#8/#9 merged there. The only new manual work in this recombination (the two conflicts above) has been checked by hand: strings.xml confirmed well-formed XML, and the dropped CI file is fork-only tooling with no code implications.

Process

Each of #6/#7/#8/#9 was built via a written design spec + implementation plan, executed task-by-task with a fresh implementer and independent review per task, plus a final whole-branch review before merging into the fork's integration branch. See the linked PRs for each one's individual review history.

Ryanmello07 and others added 11 commits July 21, 2026 21:30
feat: seedphrase login, instant account creation, remove guest mode (all 4 flavors)
PR2: Sign-In Methods list + AddAuthMethodSheet
feat: Profile name-claim + rename migration
Api.addAuth never returns the generated seedphrase, so this option could
add a seedphrase auth method the user could never see or use. The new
dedicated Seedphrase section in Settings (Generate/Regenerate) is the
only path that actually returns and displays the phrase.
The dialog was closing pendingSeedphraseAction = null synchronously on
tap, before generateSeedphrase/regenerateSeedphrase's async result comes
back. Any error (no connectivity, server rejection) set
seedphraseActionError, but that text lives inside the already-closed
dialog, so it was never shown -- a security-sensitive operation was
failing completely silently. Fixed by closing only via a
LaunchedEffect(generatedSeedphrase) once the call actually succeeds,
mirroring the remove-sign-in-method dialog's real behavior (it closes on
onSuccess, not eagerly). Flagged by the final whole-branch review.
Selecting the Leaderboard tab crashes the app before any row renders.

LeaderboardScreen keys its itemsIndexed block on entry.networkId, but the
server deliberately blanks that field for networks that have not opted
into the public leaderboard -- server's model/leaderboard_model.go does:

    if !earner.IsPublic {
        earner.NetworkId = ""
        earner.NetworkName = ""
    }

and network.leaderboard_public is `NOT NULL DEFAULT false`, so most of
the top 100 arrives with an empty id. Lazy list keys must be unique, so
the second private row throws:

    IllegalArgumentException: Key "" was already used. If you are using
    LazyColumn/Row please make sure you provide a unique key for each item

Replaying a representative top-100 payload through the current key
function yields 19 unique keys for 100 rows, with the first collision in
the opening entries -- which is why the screen dies on open rather than
on scroll. Private rows now fall back to their position, keeping the
stable network id as the key wherever the server does send one.

iOS does not hit this because LeaderboardView keys the same payload by
offset (`ForEach(Array(...enumerated()), id: \.offset)`).

Also hardens the profanity mask on the same screen, which indexes into
the name unguarded: an empty name throws NoSuchElementException on
first(), and a single-character name throws IllegalArgumentException
from repeat(-1). Two-character names are a quieter bug -- repeat(0)
leaves the name fully legible despite the profanity flag. Short names
are now masked whole.
Captured from a device tombstone:

  JNI DETECTED ERROR IN APPLICATION: CallStaticVoidMethod called with
  pending exception java.lang.NullPointerException: Attempt to invoke
  virtual method 'long com.bringyour.sdk.LeaderboardEarnersList.len()'
  on a null object reference
    at LeaderboardViewModel$getLeaderboard$2$1.result (LeaderboardViewModel.kt:199)

LeaderboardResult.Earners is a pointer in the sdk, so it is null whenever
the server does not populate it -- and the server leaves its earners slice
nil when the query returns no rows, which go marshals as `null` rather
than `[]`. An empty leaderboard therefore crashed the app.

It crashes rather than showing an error because the dereference happens
inside a gomobile callback: the exception cannot cross JNI, so ART aborts
the whole process. Any unchecked null in one of these callbacks is a hard
crash, not a caught error, which makes this a class of bug worth sweeping
rather than a single site.

Fixes all three in this file:
 - result.earners, the one in the tombstone. No earners is an empty
   leaderboard, not a failure.
 - result.networkRanking, dereferenced three times unguarded. The header
   already renders "-" for rank 0, so the defaults are the right no-data
   state.
 - result in the setNetworkLeaderboardPublic callback, which unlike
   getLeaderboard never null-checked it at all.

Note this is a different bug from the duplicate-key crash fixed earlier:
that one was real but downstream, since the fetch aborts before any row
renders. The earlier fix was never reached.

Verified locally: all four flavors compile.

(cherry picked from commit 4a2c63b)
Instant account creation created the account server side before the user
confirmed anything, then held the returned seedphrase and jwt in composition
state. A rotation, a back press, or a request that outlived its scope lost the
only copy of a phrase the server returns exactly once, stranding an account
nobody could sign into and spending the server's per-IP create budget on every
retry.

Move the create call and its result into CreateNetworkInstantViewModel and
persist the session before publishing the phrase, so the order is now
create -> log in -> show phrase -> finish. Both exits from the phrase screen go
forward into the app, and a failed finish leaves the phrase up to retry rather
than dropping it.

Guest accounts are gone, but the "?guest" deep link still called networkCreate
with no auth method, which the server reads as the seedphrase path: it minted a
permanent account and threw the generated phrase away. Route that link to the
instant account screen instead, and drop createGuestNetworkAndFinish and the
switchToGuestMode plumbing with it.

Also from the review:

- ByJwt.networkName is a gomobile-bound Go string and so is never null, which
  made the guest check a tautology. Every parsed jwt is a real account now.
- Skip handleLoginFlow's 2.75s of welcome-overlay delays on the seedphrase
  route, where nothing is animating. That window let a second tap start a
  duplicate login, and backing out of it left a persisted half-logged-in app.
- Mark the copied seedphrase sensitive so Android 13+ keeps it out of the
  clipboard preview, and turn off IME autocorrect on the entry field.
- Handle the (null, null) callback shape in add/remove/generate auth, and
  null-check result before dereferencing it in the four flavor login models.
- Hide Remove on the last remaining sign-in method; the server rejects it.
- Map a non-email userAuth to "phone" instead of the raw phone number, which
  was rendered as a method name and sent as an authType.
- Refresh the network user after backing out of the seedphrase display, so the
  row stops offering Generate for an account that already has a phrase.
- Trim network names before submitting, remember the parsed auth methods rather
  than re-walking the sdk list every recomposition, remember the
  GoogleSignInClient, and delete four orphaned guest strings and a dead setter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Ryanmello07
Ryanmello07 merged commit 8261fe5 into urnetwork:main Jul 30, 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