Skip to content

Add a Sign in with Apple button element - #133

Draft
simonhamp wants to merge 3 commits into
feat/custom-iconsfrom
feat/sign-in-with-apple-button
Draft

simonhamp wants to merge 3 commits into
feat/custom-iconsfrom
feat/sign-in-with-apple-button

Conversation

@simonhamp

@simonhamp simonhamp commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Adds <native:sign-in-with-apple-button>, which is SwiftUI's own SignInWithAppleButton on iOS. Without it, apps fake the button with a <native:button> and the apple.logo symbol, and App Review is strict about the logo, title, font and proportions.

<native:sign-in-with-apple-button :nonce="hash('sha256', $appleNonce)" class="w-full" />

Tapping it runs the Apple ID request, as SwiftUI's button always does. The answer comes back as a native event, which a component listens for:

#[On(SignInWithAppleCompleted::class)]
public function signedInWithApple(string $userId, ?string $identityToken = null, ?string $nonce = null): void {}

#[On(SignInWithAppleFailed::class)]
public function appleSignInFailed(string $code, string $message = ''): void {}

This needs NativePHP/mobile-air#545. SwiftUI's button is UIKit underneath, and core's keyboard-dismiss tap gestures cancelled the touch in any UIKit control, so without #545 the button draws but a tap does nothing.

Props

  • label: sign-in (default), continue or sign-up, Apple's three titles. Anything else throws, so label="Sign in with Apple" fails loudly instead of quietly drawing the default.
  • button-style: auto (default), black, white or white-outline. auto is black in light mode and white in dark mode. It's named like the date picker's picker-style so it doesn't read as a CSS style attribute.
  • nonce: sent to Apple as given and echoed on the completed event. Apps pass the SHA-256 of a raw nonce they keep, so the raw value never sits in the rendered tree.
  • scopes: any of email and full-name. None by default.
  • state: echoed on both events.
  • corner-radius, or a rounded-* class. SwiftUI's button can only be clipped, so this rounds the corners more than Apple does, up to a capsule, but can't square them.
  • height or h-*, 44 by default. Apple scales the title and logo with it.
  • disabled, a11y-label and a11y-hint. The system button looks the same when disabled, so the renderer dims it to 40%.
  • @press fires as the request starts, for clearing an earlier error.

Colours, borders and padding are dropped, as they are for <native:button>.

Events

Native\Mobile\UI\Events\SignInWithAppleCompleted has userId, identityToken, authorizationCode, email, givenName, familyName, nonce, state and realUserStatus. SignInWithAppleFailed has code (canceled, failed, invalid_response, not_handled, not_interactive or unknown), message and state. The renderer builds the payloads by hand and core binds them to the constructors by name, so a test reads the Swift and checks every key against the constructor.

The app still needs the com.apple.developer.applesignin entitlement. Core only takes entitlements from plugins, and mobile-ui shouldn't add it to every app, so it's left to the app.

Android

Apple has no button on Android, so the renderer draws nothing. If the tag has children, they render instead as an Android-only fallback. iOS ignores them.

Stacked on #132

This is based on feat/custom-icons because the devnull app tracks that branch. Nothing here uses custom icons, so it can move onto main if #132 changes course.

Screenshots

iOS 26.2 simulator, with #545 applied.

Light Dark

Taller with rounded-full, the system corners at 30pt (a radius of 0 can't square them), disabled, and white-outline with rounded-full:

The devnull app's onboarding after a tap. The button ran Apple's request, the simulator answered with error 1000 (simulator builds get no entitlements), and the app showed its message from SignInWithAppleFailed. The GitHub button above is a <native:pressable> drawn to match Apple's.

Light Dark

The test app on an API 35 emulator. Only the fallback child draws, and its press reaches PHP:

Testing

  • tests/SignInWithAppleButtonTest.php has 27 tests: Blade attributes reach the wire, the press handler rides in the props rather than on the node, every spelling of each label, style and scope, the errors for unknown values, nonce and state, the radius from a class and from the attribute, layout and dropped styling, children kept for Android, the fluent builder, the manifest entry, and the event payload keys against the event constructors. The suite gives 311 passed.
  • The Swift typechecks against core with no new errors.
  • On the simulator with #545: every variant draws in light and dark, a tap fires @press and runs the request, SignInWithAppleFailed arrives with the state echoed, and a disabled button ignores taps. A successful sign-in needs a real device with the entitlement, which I haven't done.
  • Android: the renderer compiled in a Gradle debug build and ran on an API 35 emulator.

Docs: NativePHP/nativephp.com#562

🤖 Generated with Claude Code

<native:sign-in-with-apple-button> draws Apple's own button on iOS, so
apps don't have to fake it with a <native:button> and an Apple logo. A
tap only calls @press. The app starts the sign-in from there with
whatever auth plugin it uses, so the element doesn't own any of it.

Props: label (sign-in, continue, sign-up), button-style (auto, black,
white, white-outline), corner-radius (or a rounded-* class), height
(44 by default) and disabled.

iOS wraps UIKit's ASAuthorizationAppleIDButton rather than SwiftUI's
SignInWithAppleButton. The SwiftUI button's only initialiser runs the
Apple ID request itself on every tap, so it can't be used as a plain
tap target. The UIKit button only draws: the tap goes through a SwiftUI
Button, because core's keyboard-dismiss tap gestures cancel touches in
UIKit controls, so a target-action never fires.

Android has no Apple button, so it draws nothing unless the tag has
children, which render there as a fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simonhamp and others added 2 commits October 16, 2026 21:06
The element drew UIKit's ASAuthorizationAppleIDButton and only fired
@press, because SwiftUI's button always runs the Apple ID request
itself. Now it is SwiftUI's button, and the element runs the request.

New props feed the request: scopes (email, full-name), nonce (raw; the
button sends Apple its SHA-256 hex) and state. The answer goes to PHP
as a native event, Native\Mobile\UI\Events\SignInWithAppleCompleted or
SignInWithAppleFailed, which components listen for with #[On]. @press
now fires as the request starts.

SwiftUI's button is UIKit underneath, so inside an EDGE screen it needs
NativePHP/mobile-air#545, which stops core's keyboard-dismiss taps
cancelling UIKit controls. Without it a tap does nothing.

The corner radius is now a clip, so it can round the corners more than
the system does but can't square them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button hashed a raw nonce itself, so the raw value had to sit in
the rendered tree. Apps now pass the SHA-256 of a raw nonce they keep in
memory, the button sends it to Apple unchanged and the completed event
echoes it back. The raw value stays out of the UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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