[object Object]

← back to Costa Rica App

costa-rica-app: Sign in with Apple — expo-apple-authentication button on login, usesAppleSignIn + plugin, api.apple()

2d0b15d4c5963153d8d113d7f0a3d80f51c6ed11 · 2026-08-07 10:18:51 -0700 · Steve Abrams

Files touched

Diff

commit 2d0b15d4c5963153d8d113d7f0a3d80f51c6ed11
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 10:18:51 2026 -0700

    costa-rica-app: Sign in with Apple — expo-apple-authentication button on login, usesAppleSignIn + plugin, api.apple()
---
 app.json      |  3 ++-
 app/login.tsx | 36 +++++++++++++++++++++++++++++++++++-
 package.json  |  1 +
 src/api.ts    |  1 +
 4 files changed, 39 insertions(+), 2 deletions(-)

diff --git a/app.json b/app.json
index a43cfcc..ce74a36 100644
--- a/app.json
+++ b/app.json
@@ -9,6 +9,7 @@
     "newArchEnabled": true,
     "ios": {
       "bundleIdentifier": "com.abrams.costarica",
+      "usesAppleSignIn": true,
       "supportsTablet": true,
       "infoPlist": {
         "ITSAppUsesNonExemptEncryption": false,
@@ -19,7 +20,7 @@
       "package": "com.abrams.costarica",
       "permissions": ["ACCESS_FINE_LOCATION"]
     },
-    "plugins": ["expo-router", "expo-secure-store"],
+    "plugins": ["expo-router", "expo-secure-store", "expo-apple-authentication"],
     "extra": {
       "apiBase": "https://costarica.agentabrams.com",
       "eas": { "projectId": "TO_BE_SET_BY_eas_init" }
diff --git a/app/login.tsx b/app/login.tsx
index b00b8b5..69cf9b4 100644
--- a/app/login.tsx
+++ b/app/login.tsx
@@ -1,5 +1,6 @@
 import { useState } from 'react';
-import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
+import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, Platform } from 'react-native';
+import * as AppleAuthentication from 'expo-apple-authentication';
 import { router } from 'expo-router';
 import { api, setToken } from '../src/api';
 
@@ -21,9 +22,39 @@ export default function Login() {
       router.back();
     } catch (e: any) { Alert.alert('Error', e.message); } finally { setBusy(false); }
   }
+  async function appleSignIn() {
+    try {
+      const cred = await AppleAuthentication.signInAsync({
+        requestedScopes: [
+          AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
+          AppleAuthentication.AppleAuthenticationScope.EMAIL,
+        ],
+      });
+      const fullName = [cred.fullName?.givenName, cred.fullName?.familyName].filter(Boolean).join(' ');
+      const r = await api.apple(cred.identityToken!, fullName || undefined);
+      await setToken(r.token);
+      router.back();
+    } catch (e: any) {
+      if (e.code === 'ERR_REQUEST_CANCELED') return;
+      Alert.alert('Apple sign-in failed', e.message);
+    }
+  }
+
   return (
     <View style={s.wrap}>
       <Text style={s.h}>{mode === 'login' ? 'Welcome back' : 'Create account'}</Text>
+      {Platform.OS === 'ios' && (
+        <>
+          <AppleAuthentication.AppleAuthenticationButton
+            buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
+            buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
+            cornerRadius={10}
+            style={{ height: 48, marginBottom: 14 }}
+            onPress={appleSignIn}
+          />
+          <View style={s.orRow}><View style={s.line} /><Text style={s.or}>or</Text><View style={s.line} /></View>
+        </>
+      )}
       {mode === 'register' && <TextInput style={s.i} placeholder="Full name" value={name} onChangeText={setName} />}
       <TextInput style={s.i} placeholder="Email" autoCapitalize="none" keyboardType="email-address" value={email} onChangeText={setEmail} />
       {mode === 'register' && <TextInput style={s.i} placeholder="Phone (+506…)" keyboardType="phone-pad" value={phone} onChangeText={setPhone} />}
@@ -44,4 +75,7 @@ const s = StyleSheet.create({
   cta: { backgroundColor: '#0a7d55', height: 52, borderRadius: 12, alignItems: 'center', justifyContent: 'center', marginTop: 6 },
   ctaTxt: { color: '#fff', fontSize: 16, fontWeight: '700' },
   switch: { color: '#0a7d55', textAlign: 'center', marginTop: 18 },
+  orRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 14 },
+  line: { flex: 1, height: 1, backgroundColor: '#d8ddd6' },
+  or: { color: '#6b756e', marginHorizontal: 10, fontSize: 13 },
 });
diff --git a/package.json b/package.json
index 9ac980b..d15b490 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
   },
   "dependencies": {
     "expo": "^52.0.0",
+    "expo-apple-authentication": "~7.1.0",
     "expo-router": "~4.0.0",
     "expo-constants": "~17.0.0",
     "expo-linking": "~7.0.0",
diff --git a/src/api.ts b/src/api.ts
index 3aaaa50..f98a1cc 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -28,6 +28,7 @@ async function req(path: string, opts: RequestInit = {}) {
 export const api = {
   register: (b: any) => req('/auth/register', { method: 'POST', body: JSON.stringify(b) }),
   login: (email: string, password: string) => req('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }),
+  apple: (identity_token: string, full_name?: string) => req('/auth/apple', { method: 'POST', body: JSON.stringify({ identity_token, full_name }) }),
   me: () => req('/me'),
   listings: (q: Record<string, string> = {}) => req('/listings?' + new URLSearchParams(q).toString()),
   listing: (slug: string) => req(`/listings/${slug}`),

← e816038 costa-rica-app: Expo/EAS native scaffold — Router nav, JWT a  ·  back to Costa Rica App  ·  costa-rica-app: Contacts screen — import address book (expo- 2368baa →