Pattern · Auth and Onboarding
NewEmail Link Sign In SwiftUI Block
Passwordless sign-in by email: send a link, check your inbox with Open Mail and a Resend countdown, or enter a code instead.
An email field that checks itself and Continue with email, which runs your closure through working, done and failed. The card then turns to Check your inbox with the address, Open Mail and Resend on a countdown, plus an optional six-digit code that fills from AutoFill and submits itself. Links that open your app sign the person in through `onLink`.
- SwiftUI
- iOS 17.0+
- magic-link
$199 once, every piece included. See how installing works · Sign in
01
What's included
SwiftUI source you own, built on Apple frameworks only.
- An email field that checks on submit, then live, with AutoFill
- Continue with email shows working, a check, or your error's message
- Check your inbox with Open Mail and a Resend countdown
- An optional six-digit code that fills from AutoFill and submits itself
- Sign-in links handled on screen, plus
.onSignInLinkfor your root
02
Implementation notes
How it behaves, what it handles, and the decisions behind it.
One card holds the email field, Continue with email in the accent and a line saying no password is needed. The email stays quiet until Continue is tapped, then says what's wrong under the field and keeps checking live as it's corrected; Return continues. Continue runs your closure: a spinner takes the label's place, a check follows, and a thrown error's message shows under the button. Once the link is on its way the card turns to the sent state: an open envelope, "Check your inbox" and the address, how long the link works, Open Mail in the accent, and "Didn't get it?" with Resend counting down from 0:30 before it can be tapped, then confirming "Sent". "Enter a code instead" opens six boxes over one real field, so the keyboard offers the code from Mail; the code submits itself once full, and a wrong one shows your error, clears the boxes and keeps focus. When the person taps the link and your app opens with it, the block runs onLink with the URL, shows progress on Open Mail, and checks. "Use a different email" under the card goes back to the field. Show the block redacted while a session restores and it draws grey shapes in its own layout.
Notes
MagicLinkSignIn(email:resendAfter:linkLifetime:onSendLink:onVerifyCode:onLink:linkMatches:onOpenMail:onSuccess:). OnlyonSendLinkis required. It is a section: no navigation stack, background, padding or scroll view of its own, so place it in your own column, such as aScrollViewwith.padding(.horizontal, 20), or a sheet.onSendLinkreceives the trimmed email. It also runs for Resend. Return to show the sent state, throw to fail: the block shows a thrownLocalizedError'serrorDescription, or "Couldn't send the link. Try again." ACancellationErrorgoes back to rest quietly.onVerifyCodereceivesMagicLinkSignIn.CodeAttempt(the address and the six digits). Passing it shows "Enter a code instead" in the sent state; throw to refuse the code.onLinkreceives a URL the app was opened with while the block is on screen: a custom-scheme URL throughonOpenURL, or a universal link throughNSUserActivityTypeBrowsingWeb.linkMatchesdecides which URLs are sign-in links (every URL by default). Exchange the link's token with your server inside it; throw to show "That link didn't work. It may have expired, so send a new one."- For a link that opens a closed app, the block isn't on screen yet. Attach
.onSignInLink(matching:perform:)at your root, which listens for both kinds of link and hands you the URL. - After a link or code succeeds the block holds its check for about a second, then calls
onSuccess. Move to your app there. onOpenMailreplaces Open Mail's default, which opens the Mail app withopenURLandmessage://. If no app takes that URL, the button folds away.emailis aBinding<String>to prefill a returning person's address.resendAftersets the countdown (30 seconds by default).linkLifetimeadds "It works for 15 minutes." to the sent line; nil leaves it out.- Loading: apply
.redacted(reason: .placeholder)and the block draws its field and button as grey shapes, breathing unless Reduce Motion is on. - Fields: the email is
.emailAddresswith the email keyboard and Return reading Continue; the code is.oneTimeCodeon the number pad. - Haptics: success when a link or code signs in, an error when anything fails, a warning when a submit is refused.
- VoiceOver reads the email with its error as a hint, the code field as one field with its digits spoken one by one, and the countdown as text. It announces "Check your inbox" with the address, "Sent again", refusals, failures and "Signed in". Every control is at least 44 points tall. At accessibility sizes the sent line and errors wrap and the code boxes share the width.
- Reduce Motion turns every change into a short fade.
MagicLinkSignInExampleat the end ofMagicLinkSignIn.swiftis Fieldnote's sign-in screen from the preview: the app's name, "Sign in" and a line under it, then the block. Its stand-in auth accepts the code 246810 and refuses any other, and treats links whose path starts with/authas sign-in links.
Theming
- Reads
SPThemefrom@Environment(\.spTheme)and requires thedesign-systemitem. - The card is
color.surfacewithradius.lgand 16 point padding (space.lg). - The field and code boxes are 52 points tall on
fillwithradius.mdand a 1 pointseparatorhairline that turns totextPrimarywhen focused and todangeron an error. The label istype.subheadintextSecondary, inputtype.bodyintextPrimary, the prompttextTertiary. Code digits use the display face at 22 points, semibold at most, with tabular figures. - Continue with email and Open Mail are the accent, one at a time:
accentwithaccentOnintype.headline,radius.md, 52 points. - The sent state's envelope is a 28 point symbol in
textSecondary, its titletype.title3intextPrimary, its linetype.subheadintextSecondarywith the address intextPrimary. The countdown istextTertiarywith tabular figures; Resend, "Enter a code instead" and "Use a different email" aretextPrimarysemibold. The hairline isSPHairline. Errors aretype.footnoteindanger. - States move with
motion.snappy, the sent state withmotion.smooth, the buttons' press withmotion.standard.
Requirements
- Associated Domains with an
applinks:entry for a universal sign-in link, or a URL type for a custom scheme, so the link opens your app.
03
Keep building
Pieces that work well next to this one.