The OAuth entry screen for the Ambasdr mobile app — an atmospheric Skia gradient, four brand-aligned OAuth pills, and a stubbed handoff to Gerald’s Auth0 wiring. First screen in the onboarding v2 redesign.
Three layered atoms compose one screen.
Single Skia canvas: charcoal→forest linear gradient + three slowly drifting Gaussian-blurred blobs + a 4% film-grain Turbulence overlay. One GPU pass per frame. Respects prefers-reduced-motion.
Frost-fill pills with brand-colored Ionicons left-anchored via absolute positioning so the label dead-centers. LinkedIn / Google / Instagram / TikTok.
At app/(auth)/login.tsx. Composes the gradient, a frost-tinted three-plane Logomark, a Bebas Neue billboard headline, the OAuth pill stack, and an interactive legal fine-print block.
Eight new files plus a route-redirect tweak. All under mobile/.
| Path | Role |
|---|---|
app/(auth)/_layout.tsx |
Headerless Stack for the auth flow. |
app/(auth)/login.tsx |
The login screen itself. Composes the three atoms. |
app/index.tsx |
Root redirect: / → /(auth)/login. |
components/auth/AuthGradientBackground.tsx |
Atmospheric Skia gradient with stage palette. |
components/auth/OAuthButton.tsx |
Frost-fill OAuth pill atom. |
stories/auth/*.stories.tsx |
Storybook stories: Auth/Login, Auth/AuthGradientBackground, Auth/OAuthButton. |
__tests__/auth/*.test.tsx |
39 unit tests covering rendering, interaction, accessibility, reduced motion, and brand-color regression. |
Three layers, one Skia canvas, one GPU pass per frame.
Diagonal LinearGradient from colors.ink (#1C1C1C charcoal) to colors.sage500 (#1F3D34 deep forest). Anchors top-left to bottom-right at viewport dimensions.
Each blob is a Circle with a BlurMask (Gaussian, style="normal"). The three blobs sit at distinct base positions and drift in slow circular orbits while breathing in opacity.
Turbulence shader with freqX/freqY=0.8, octaves=2, tileWidth/tileHeight=4, blended at 4% multiply opacity. Static (no animation) so it reads as material texture, not flicker.
The auth surface uses the welcome stage from STAGE_PALETTES: ink → sage500 base + three blobs in sage400 (upper-right emerald glow), sage300 (lower-left mint haze), and frost (center breathing point). The StagePalette type is shaped so later onboarding steps can brighten as the user progresses.
Each blob has its own drift period, breath period, and phase offset. All four numbers are coprime so the composition never repeats identically.
| Blob | Base position | Radius / Blur | Drift / Breath | Opacity range |
|---|---|---|---|---|
| Upper-right emerald | (0.75, 0.28) |
220 / 80 |
32s / 18s |
0.50–0.75 |
| Lower-left sage haze | (0.18, 0.78) |
240 / 90 |
40s / 24s |
0.40–0.60 |
| Center frost breath | (0.50, 0.50) |
180 / 100 |
28s / 14s |
0.15–0.28 |
A single master clock loops 0 → 1 over 120 seconds. Each blob derives its drift angle and breath phase from that clock modulo its own period. The Y delta on every orbit uses width (not height) as the radius scalar — gives a circular orbit in screen-px instead of an aspect-stretched ellipse, which reads as subtle drift rather than obvious vertical motion on a tall viewport.
When useReducedMotion() is true, the master clock pins at zero. Blobs hold their base positions and mid-breath opacity. The gradient stays on screen; only the animation is suppressed.
One atom, four providers, brand-colored icons.
Frost-fill pill, 56pt tall, full pill-radius. The icon sits in an absolutely positioned View at left: 24 so the label can dead-center across the full pill width — matching the AllTrails / Auth0 SDK convention rather than a flow-laid icon+label row.
| Provider | Ionicon | Icon color | Label |
|---|---|---|---|
logo-linkedin |
#0A66C2 (LinkedIn blue) |
Continue with LinkedIn | |
logo-google |
colors.ink (single-glyph fallback) |
Continue with Google | |
logo-instagram |
#E1306C (Instagram pink) |
Continue with Instagram | |
| TikTok | logo-tiktok |
#000000 (TikTok black) |
Continue with TikTok |
The Google “G” brand color is a four-color sweep (red / yellow / green / blue). A single-glyph icon font like Ionicons can’t render multi-color. We pick colors.ink over a single-stop approximation so we don’t misrepresent the brand.
accessibilityRole="button" + accessibilityLabel matches the visible label.accessibilityState={ disabled } exposed to the a11y tree.android_ripple={ color: colors.line, borderless: false } for Android press feedback (iOS uses the native Pressable dim).style array (not the function form) so the alignSelf: stretch width resolves correctly inside flex parents.The three atoms composed into one route at app/(auth)/login.tsx.
72pt Logomark with a frost-tinted three-plane hierarchy (#FAF8F3 / rgba(250,248,243,0.85) / rgba(250,248,243,0.55)). Below it, a Bebas Neue billboard headline: “Sign up or log into your ambasdr.” at 40pt / lineHeight 42 / letterSpacing 0.6, matching StepHeader.tsx’s rhythm per the project typography canon.
Four OAuthButton rows in a 12pt-gap column. Below them, a 11pt fine-print block with three inline legal links rendered as Text onPress with accessibilityRole="link". Underlined affordance, real onPress handlers, stubbed routes for now.
“By continuing to use Ambasdr, you agree to our Terms of Service and Privacy Policy. Personal data added to Ambasdr is public by default — refer to our Privacy FAQ to make changes.”
The wording is ported verbatim from the AllTrails reference screenshot Lennox shared, with every “AllTrails” replaced by “Ambasdr”. The three links use testIDs legal-link-terms, legal-link-privacy, and legal-link-privacy-faq.
All interactive handlers on this screen are UI-only logs. Real auth wiring is Gerald’s lane.
Every tappable surface logs its intended action and either advances the route or no-ops. None of them touch tokens, sessions, or backend APIs yet.
// On press of any OAuth button: console.log(`[oauth-stub] provider=${provider}`); router.replace("/(onboarding)/username");
The route forward to /(onboarding)/username is temporary — it’s the current name-collection step. When the onboarding v2 single-field name step lands, the redirect target moves with it.
// On press of any legal link span: console.log(`[legal-stub] route=/legal/${route}`); // no navigation — pure log until pages are ported
(home) and (onboarding) after sign-in./legal/terms, /legal/privacy, and /legal/privacy-faq routes (existing web legal pages have a mobile-ready content source).39 unit tests across three files. Full suite stays green at 199 tests.
| File | Count | Coverage |
|---|---|---|
__tests__/auth/OAuthButton.test.tsx |
17 |
Per-provider rendering & copy, a11y role + label, brand-color regression, disabled state behavior + a11y exposure, custom testID override. |
__tests__/auth/AuthGradientBackground.test.tsx |
8 |
Default + explicit stage rendering, style override, reduced-motion path, STAGE_PALETTES.welcome value assertions. |
__tests__/auth/login.test.tsx |
14 |
All 4 OAuth providers render & advance, headline + accessibilityRole="header", fine-print copy with Ambasdr substitution, 3 legal links with role=link, legal-stub press logs route & does NOT navigate, OAuth-stub press logs provider trace, reduced-motion path. |
Skia primitives are aliased to bare RN Views; react-native-reanimated uses the package’s shipped mock entry. useReducedMotion is mocked at the suite level so reduced-motion paths can be flipped per-test. The mock pattern matches MeshGradientBackground.test.tsx from the shimmer animation PR.
How this work landed on staging-mobile.
/ambasdr-review running first. Reverted on staging-mobile via commit 5f4937d.staging-mobile tip. Survived a two-pass /ambasdr-review (the second pass cleared H1 + H2 blockers and lifted Design / Documentation / Test Coverage to A). Merged via a5649fb.Net change at merge: 14 files, +899 / −160 lines, including the deletion of app/(onboarding)/welcome.tsx + its test and story (the onboarding v2 design drops the welcome interstitial).
First screen in the onboarding v2 redesign sequence. Stacked PRs from here flesh out the form steps.
New ProgressBar (N/7 denominator), FooterButton, SageCurveBackground, OAuthButton refinements, FileContextSheet (bottom sheet for the Knowledge step). Storybook-first.
Steps 1–4 (Name → Role → Handle → Photo), then Steps 5–6 (Socials with bubble pattern + Knowledge with bottom-sheet file context), then end-of-flow (Generating → Preview → Published → Notifications).
Once the full flow lands on staging-mobile, it ships to main as a single squashed PR — one logical “Onboarding v2 redesign” unit on the production history.