feat(mobile): welcome intro screen for first-time users (#452) - #520
Conversation
…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 is attempting to deploy a commit to the miracle656's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@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! 🚀 |
Miracle656
left a comment
There was a problem hiding this comment.
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.tsxas 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.tsxand 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
|
@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.
|
Merging — the intro screen and the first-launch-vs-returning-user split are what #452 asked for, and I had to fix three things first, all of which meant the flow couldn't work as written: 1. from } catch {
if (attempt === retries) throw;
}A bare 2. "Get started" led nowhere. 3. The entry gate could never see a wallet. It read Smaller: themed the entry spinner through Verified: Two notes, neither blocking:
|
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 forinvisible_wallet_addressandveil_seen_welcome. Routes first-time users to/welcome, returning users directly to/dashboard.package.json— Added@react-native-async-storage/async-storagefor persistent onboarding state.Flow
index.tsxfinds no wallet, no welcome-seen flag → redirects to/welcomeindex.tsxfinds the wallet address → skips onboarding → redirects to/dashboardAcceptance
Refs
frontend/wallet/app/page.tsx