Skip to content

feat(mobile): welcome intro screen for first-time users (#452) - #520

Merged
Miracle656 merged 3 commits into
Miracle656:mainfrom
northvictor:feat/welcome-intro-screens
Jul 30, 2026
Merged

feat(mobile): welcome intro screen for first-time users (#452)#520
Miracle656 merged 3 commits into
Miracle656:mainfrom
northvictor:feat/welcome-intro-screens

Conversation

@northvictor

Copy link
Copy Markdown
Contributor

Summary

Ports the web wallet's landing/hero experience to mobile as an onboarding flow. First-time users see the branded intro before creating a wallet; returning users skip straight past it.

Changes

  • app/(onboarding)/welcome.tsx — Branded intro screen with Veil logo, tagline ("Your passkey is your wallet"), three feature highlights (no seed phrases, instant setup, Stellar network), and Get Started / Recover existing wallet actions. Styled to match the web wallet's dark theme and gold accent palette.
  • app/(onboarding)/_layout.tsx — Route group layout for the onboarding flow.
  • app/index.tsx — Entry point now checks AsyncStorage for invisible_wallet_address and veil_seen_welcome. Routes first-time users to /welcome, returning users directly to /dashboard.
  • package.json — Added @react-native-async-storage/async-storage for persistent onboarding state.

Flow

  1. First launchindex.tsx finds no wallet, no welcome-seen flag → redirects to /welcome
  2. Welcome screen → User taps "Get started" or "Recover" → flag is persisted → navigates to register/recover
  3. Returning userindex.tsx finds the wallet address → skips onboarding → redirects to /dashboard

Acceptance

  • First launch shows the branded intro screen
  • Tapping "Get started" marks the welcome as seen and navigates to registration
  • Returning users (with or without a wallet) skip the intro entirely
  • The welcome screen uses AsyncStorage so the flag survives app restarts

Refs

…56#452)

Port the web wallet's landing/hero experience to mobile as an onboarding
flow. First-time users see the branded intro before creating a wallet;
returning users skip straight past it.

- Add (onboarding)/welcome.tsx with logo, tagline, feature highlights,
  and Get Started / Recover actions
- Add (onboarding)/_layout.tsx for the onboarding route group
- Update app/index.tsx to check AsyncStorage for wallet state and route
  first-time users to welcome, returning users to dashboard
- Add @react-native-async-storage/async-storage dependency

Refs: Miracle656#452, backlog item 24
@northvictor
northvictor requested a review from Miracle656 as a code owner July 28, 2026 00:15
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

@northvictor is attempting to deploy a commit to the miracle656's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 28, 2026

Copy link
Copy Markdown

@northvictor Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Miracle656 Miracle656 mentioned this pull request Jul 28, 2026
2 tasks

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Verified locally — merges into main with zero conflicts and typechecks clean:

$ git merge origin/main    # 0 conflicts
$ npm run typecheck
TYPECHECK_EXIT=0

Nice piece of work. Two things I want to call out as right, because they're the kind of thing that usually goes wrong across parallel PRs:

  • app/index.tsx as a session-aware entry point is exactly what #507 left room for — its version is a placeholder redirect with the comment "Once the wallet/session logic lands this will branch between onboarding and the unlocked tab group." You've written that branch. Good instinct.
  • (onboarding)/_layout.tsx#509 is adding (onboarding)/restore.tsx and needs this layout to exist. You've unblocked another PR without being asked.

One bug

const wallet = await AsyncStorage.getItem(WALLET_KEY);
if (wallet) { router.replace("/dashboard"); return; }

const seenWelcome = await AsyncStorage.getItem(SEEN_WELCOME_KEY);
if (seenWelcome) { router.replace("/dashboard"); return; }   // ← here

router.replace("/welcome");

The second branch sends a user with no wallet to /dashboard, purely because they've seen the intro once. That state is reachable easily: open the app, see the welcome screen, tap through or background it without completing wallet creation, reopen. From then on they land on a dashboard with no wallet and no route back to onboarding — the welcome screen is unreachable because seenWelcome is set, and creation never happened.

seenWelcome should decide which onboarding entry they get, not whether onboarding is skipped:

if (wallet) { router.replace('/dashboard'); return; }
router.replace(seenWelcome ? '/create-wallet' : '/welcome');

Wallet presence is the only thing that should gate the dashboard.

Two smaller notes

The catch falls through to /welcome. Reasonable default, but it means a transient AsyncStorage error shows the intro to an existing wallet holder. Since the wallet check is the important one, consider retrying that read once before falling back, or at least distinguishing "no wallet" from "couldn't read".

if (!checking) return null; — between setChecking(false) and the router.replace taking effect, this renders nothing. In practice it's a frame or two, but returning the spinner until navigation actually happens avoids a flash of blank screen on a slow device. Minor.

Coordination note

app/index.tsx is the single most contended file in the mobile queue right now — #507, #516, #517 and this PR all touch it, and #516 currently deletes it. I've asked #516 not to, and pointed at this PR as the reason. Landing order is #507 first (it defines the route tree), then this.

Your change is a clean superset of #507's version, so the rebase should be small: keep your file, and make sure the routes you target (/dashboard, /welcome) match #507's tree — its dashboard lives at (tabs)/dashboard, which /dashboard resolves to correctly.

Fix the seenWelcome branch and I'm happy with this.

# Conflicts:
#	frontend/mobile/app/index.tsx
#	frontend/mobile/package.json
@northvictor

Copy link
Copy Markdown
Contributor Author

@Miracle656 Thank you for your review, I have implemented the changes you requested, please review and merge

Fix three defects that stopped this branch working, then reconcile with main:

- app/index.tsx did not compile: `if (attempt === retries) throw;` is a bare
  throw outside a catch binding (TS1109: Expression expected). The mobile
  typecheck job did not exist when this branch was opened, so CI never caught
  it. Rewritten to capture and rethrow the last error.
- welcome.tsx pushed /register, which is not a route. "Get started" therefore
  went nowhere. Repointed at /create-wallet, which is what index.tsx already
  falls through to.
- The entry gate read the wallet address from AsyncStorage under
  invisible_wallet_address, but lib/walletStore.ts keeps it in the
  Keychain/Keystore. A real wallet would never have been seen. Now reads
  through getWalletAddress(); veil_seen_welcome stays in AsyncStorage since it
  is presentation state, not a secret.

Also themed the entry spinner via useTheme instead of hardcoded hex, and
dropped an unused TEAL constant.

tsc clean; jest 10 suites / 174 tests; expo lint clean.
@Miracle656

Copy link
Copy Markdown
Owner

Merging — the intro screen and the first-launch-vs-returning-user split are what #452 asked for, and app/index.tsx had a comment saying exactly this branch was coming ("Once the wallet/session logic lands this will branch between onboarding and the unlocked tab group").

I had to fix three things first, all of which meant the flow couldn't work as written:

1. app/index.tsx didn't compile.

app/index.tsx(20,37): error TS1109: Expression expected

from

} catch {
  if (attempt === retries) throw;
}

A bare throw has no meaning outside a catch (e) binding — it needs an expression. Worth knowing why CI didn't tell you: the Mobile — typecheck & test job only exists as of #508, which merged after you opened this, so nothing on this branch ever ran tsc. Rewritten to capture the last error and rethrow it after the final attempt.

2. "Get started" led nowhere. welcome.tsx did router.push("/register"), and there is no /register route — not at the root, not in (onboarding). Repointed at /create-wallet, which is the route that exists and the one index.tsx already falls through to when veil_seen_welcome is set. Worth double-checking that's the destination you intended.

3. The entry gate could never see a wallet. It read invisible_wallet_address from AsyncStorage, but lib/walletStore.ts keeps the address in the Keychain/Keystore via expo-secure-store. So a user who had actually created a wallet would still be routed to onboarding on every launch. Now goes through getWalletAddress(). I left veil_seen_welcome in AsyncStorage — that one is presentation state rather than a secret, and keeping it out of SecureStore is right.

Smaller: themed the entry spinner through useTheme instead of a hardcoded #D4A843/#0B0B0F, and removed an unused TEAL constant.

Verified: tsc --noEmit clean, jest 10 suites / 174 tests, expo lint clean, CI green.

Two notes, neither blocking:

@Miracle656
Miracle656 merged commit 17571f0 into Miracle656:main Jul 30, 2026
10 of 13 checks passed
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.

24. Welcome / intro screens

3 participants