refactor(auth): remove all Google/Apple social login code

No external auth providers to keep authentication fully self-sovereign
and avoid dependency on third-party services. Removes Google Sign-In,
Apple Sign-In components, utilities, endpoints, translations, and
mobile dependencies across all apps and shared packages.

Google/Apple integrations for data sync (Contacts import, Calendar sync)
are intentionally preserved as they serve a different purpose.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-26 09:12:30 +01:00
parent 30a0a651ac
commit 2d11ba6248
46 changed files with 499 additions and 2253 deletions

View file

@ -70,28 +70,30 @@ STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_KEY
STRIPE_WEBHOOK_SECRET=whsec_103705b73101b783a91305a9ec272834df6a096ffb2c2566b1c899318a156b03
# Stripe Product & Price IDs (ManaCore Unified Plans - Live)
# Plus: 4.99€/month, 49.99€/year - 100 credits
# Plus: 1.00€/month, 9.60€/year - 100 credits (1 Mana = 1 Cent)
STRIPE_PLUS_PRODUCT_ID=prod_TzNUGcq9qx9rRT
STRIPE_PLUS_PRICE_MONTHLY=price_1T1OkKAZjQCYS0ZJ88m0shoN
STRIPE_PLUS_PRICE_YEARLY=price_1T1OkLAZjQCYS0ZJ4IdMzVyJ
STRIPE_PLUS_PRICE_MONTHLY=price_1TEt04AZjQCYS0ZJUuUWt3yg
STRIPE_PLUS_PRICE_YEARLY=price_1TEt05AZjQCYS0ZJYPqiqQMm
# Pro: 11.99€/month, 119.99€/year - 500 credits
# Pro: 5.00€/month, 48.00€/year - 500 credits (1 Mana = 1 Cent)
STRIPE_PRO_PRODUCT_ID=prod_TzNUgWeBjT35qn
STRIPE_PRO_PRICE_MONTHLY=price_1T1OkLAZjQCYS0ZJvyPM7Wop
STRIPE_PRO_PRICE_YEARLY=price_1T1OkLAZjQCYS0ZJDbZeuOOu
STRIPE_PRO_PRICE_MONTHLY=price_1TEt05AZjQCYS0ZJjmjsUdkJ
STRIPE_PRO_PRICE_YEARLY=price_1TEt05AZjQCYS0ZJtaT4UGsA
# Ultra: 24.99€/month, 249.99€/year - 2000 credits
# Ultra: 20.00€/month, 192.00€/year - 2000 credits (1 Mana = 1 Cent)
STRIPE_ULTRA_PRODUCT_ID=prod_TzNUE5pTbTDdbp
STRIPE_ULTRA_PRICE_MONTHLY=price_1T1OkMAZjQCYS0ZJYCJNZtg8
STRIPE_ULTRA_PRICE_YEARLY=price_1T1OkMAZjQCYS0ZJvCvR6Ve6
STRIPE_ULTRA_PRICE_MONTHLY=price_1TEt06AZjQCYS0ZJdyTDjfUk
STRIPE_ULTRA_PRICE_YEARLY=price_1TEt06AZjQCYS0ZJ6Wq3gNpM
# Credit Packs (One-time purchases)
STRIPE_CREDITS_100_PRODUCT_ID=prod_TzNUvyjD4hrcUR
STRIPE_CREDITS_100_PRICE=price_1T1OkNAZjQCYS0ZJP6XQ33F7
STRIPE_CREDITS_500_PRODUCT_ID=prod_TzNUzzub5HM70Q
STRIPE_CREDITS_500_PRICE=price_1T1OkNAZjQCYS0ZJGH9TKmqa
STRIPE_CREDITS_1000_PRODUCT_ID=prod_TzNUvB4LT4PCCe
STRIPE_CREDITS_1000_PRICE=price_1T1OkNAZjQCYS0ZJvc6HTfB5
# Mana Tränke (One-time purchases, 1 Mana = 1.4 Cent)
STRIPE_POTION_SMALL_PRODUCT_ID=prod_UDKn8rXX0Crz0T
STRIPE_POTION_SMALL_PRICE=price_1TEu8UAZjQCYS0ZJUGnsu9SH
STRIPE_POTION_MEDIUM_PRODUCT_ID=prod_UDKnANMuSvWMIE
STRIPE_POTION_MEDIUM_PRICE=price_1TEu8UAZjQCYS0ZJQr2FbDm0
STRIPE_POTION_LARGE_PRODUCT_ID=prod_UDKnTxFN6xD0ID
STRIPE_POTION_LARGE_PRICE=price_1TEu8VAZjQCYS0ZJDX6i2jwv
STRIPE_POTION_HUGE_PRODUCT_ID=prod_UDKncb3tyAlGKy
STRIPE_POTION_HUGE_PRICE=price_1TEu8VAZjQCYS0ZJ7AO86Jrt
# Customer Portal Configuration
STRIPE_PORTAL_CONFIG_ID=bpc_1T1PFdAZjQCYS0ZJEhF9ob7q
@ -191,10 +193,6 @@ PICTURE_STORAGE_PUBLIC_URL=http://localhost:9000/picture-storage
PICTURE_APP_ID=picture-app
PICTURE_MANA_CORE_SERVICE_KEY=
# OAuth (optional - leave empty to disable)
PICTURE_GOOGLE_CLIENT_ID=
PICTURE_APPLE_CLIENT_ID=
# ============================================
# NUTRIPHI PROJECT
# ============================================

View file

@ -57,8 +57,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -64,8 +64,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -48,8 +48,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -55,8 +55,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -54,8 +54,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -49,8 +49,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -33,8 +33,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect="/dashboard"
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -1,522 +0,0 @@
# Social Authentication Setup Guide for Manadeck
This guide explains how to set up Google Sign-In and Apple Sign-In for the Manadeck mobile app.
## Overview
Manadeck uses Mana Core authentication middleware which supports:
- Email/Password authentication
- Google Sign-In (Android & iOS)
- Apple Sign-In (iOS only)
All authentication methods issue JWT tokens that work with your Supabase backend and respect Row-Level Security (RLS) policies.
## Prerequisites
### Required Accounts and Credentials
1. **Google Cloud Console Account**
- OAuth 2.0 credentials for Android and iOS
- Web Client ID (for Android)
- iOS Client ID (for iOS)
2. **Apple Developer Account**
- Sign in with Apple capability enabled
- App ID configured with Sign in with Apple
3. **Mana Core Backend**
- Backend must support `/v1/auth/google-signin` and `/v1/auth/apple-signin` endpoints
- Backend URL configured in `.env.local`
## Installation
The required dependencies are already installed:
- `@react-native-google-signin/google-signin@^14.0.1`
- `expo-apple-authentication@~8.0.7`
- `base64-js`
## Configuration Steps
### 1. Google Cloud Console Setup
#### Create OAuth Credentials
1. Go to [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project or select existing project
3. Navigate to **APIs & Services → Credentials**
#### For Android:
1. Click **Create Credentials → OAuth 2.0 Client ID**
2. Select **Android** as application type
3. Fill in:
- **Package name**: `com.tilljs.manadeck`
- **SHA-1 certificate fingerprint**: Get from `cd android && ./gradlew signingReport`
4. Click **Create**
#### For iOS:
1. Click **Create Credentials → OAuth 2.0 Client ID**
2. Select **iOS** as application type
3. Fill in:
- **Bundle ID**: `com.tilljs.manadeck`
4. Click **Create**
5. **Save the iOS Client ID** (format: `XXXXX.apps.googleusercontent.com`)
#### For Web (Required for Android):
1. Click **Create Credentials → OAuth 2.0 Client ID**
2. Select **Web application** as application type
3. Fill in:
- **Name**: Manadeck Web (for Android)
4. Click **Create**
5. **Save the Web Client ID** (format: `XXXXX.apps.googleusercontent.com`)
### 2. Apple Developer Console Setup
#### Enable Sign in with Apple
1. Go to [Apple Developer Portal](https://developer.apple.com/account)
2. Navigate to **Certificates, Identifiers & Profiles**
3. Click **Identifiers** in the sidebar
4. Find and select your App ID: `com.tilljs.manadeck`
5. In the **Capabilities** section, find **Sign in with Apple**
6. Check the **Sign in with Apple** checkbox
7. Click **Save**
#### Configure in Xcode (Optional but Recommended)
1. Open your iOS project:
```bash
cd /Users/tillschneider/Documents/__00__Code/manadeck/apps/mobile
open ios/manadeck.xcworkspace
```
2. Select your project target in the left sidebar
3. Go to **Signing & Capabilities** tab
4. Click **+ Capability** button
5. Add **Sign in with Apple**
6. Ensure proper signing team is selected (Team ID: QP3GLU8PH3)
### 3. Update Environment Variables
Edit `/Users/tillschneider/Documents/__00__Code/manadeck/apps/mobile/.env.local`:
```bash
# Existing Supabase Configuration
EXPO_PUBLIC_SUPABASE_URL=https://vksoodohrbjwyloitvsz.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_2ndX-kBHFpbDlL_ZeeOnfQ_ZlLI8ONk
# Existing Backend API Configuration
EXPO_PUBLIC_API_URL=https://manadeck-backend-pduya7fsoq-ey.a.run.app
# Google OAuth Configuration (ADD THESE)
# Web Client ID (used for Android authentication)
EXPO_PUBLIC_GOOGLE_CLIENT_ID=YOUR_WEB_CLIENT_ID.apps.googleusercontent.com
# iOS Client ID (used for iOS authentication)
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=YOUR_IOS_CLIENT_ID.apps.googleusercontent.com
```
**Replace the placeholder values with your actual OAuth Client IDs from Google Cloud Console.**
### 4. Update app.json
The `app.json` has already been configured with:
```json
{
"expo": {
"ios": {
"config": {
"googleSignIn": {
"reservedClientId": "com.googleusercontent.apps.PLACEHOLDER_IOS_CLIENT_ID"
}
},
"usesAppleSignIn": true
},
"plugins": [
"expo-router",
"@react-native-google-signin/google-signin",
"expo-apple-authentication"
]
}
}
```
**Important**: Replace `PLACEHOLDER_IOS_CLIENT_ID` in `app.json` with your actual iOS Client ID (without the `com.googleusercontent.apps.` prefix).
For example, if your iOS Client ID is `123456789-abc123.apps.googleusercontent.com`, the `reservedClientId` should be:
```json
"reservedClientId": "com.googleusercontent.apps.123456789-abc123"
```
### 5. Rebuild Native Code
After updating `app.json` and environment variables, rebuild the native code:
```bash
cd /Users/tillschneider/Documents/__00__Code/manadeck/apps/mobile
# For iOS
npx expo prebuild --clean
npx expo run:ios
# For Android
npx expo prebuild --clean
npx expo run:android
```
### 6. **IMPORTANT**: Manually Add iOS URL Scheme
**⚠️ Critical Step**: The Expo plugin may not automatically add the Google Sign-In URL scheme to iOS. You must verify and add it manually.
#### Verify and Add URL Scheme to Info.plist
1. Open `ios/manadeck/Info.plist`
2. Find the `CFBundleURLTypes` array
3. Add a new URL scheme entry for Google Sign-In:
```xml
<key>CFBundleURLTypes</key>
<array>
<!-- Existing URL schemes -->
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>manadeck</string>
<string>com.tilljs.manadeck</string>
</array>
</dict>
<!-- Add this new entry for Google Sign-In -->
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.111768794939-cgen6eklloo7k8vppcaq01o8r8nd7anb</string>
</array>
</dict>
</array>
```
**Replace** `111768794939-cgen6eklloo7k8vppcaq01o8r8nd7anb` with your actual iOS Client ID.
#### Why This is Required
Without this URL scheme, Google Sign-In will fail with:
```
Your app is missing support for the following URL schemes:
com.googleusercontent.apps.XXXXX
```
iOS uses this URL scheme to redirect back to your app after Google authentication.
## Backend Requirements
Your Mana Core backend must support the following endpoints:
### POST `/v1/auth/google-signin`
**⚠️ Important**: The backend expects `deviceInfo` as a **nested object**, not spread at the root level.
Request body:
```json
{
"token": "GOOGLE_ID_TOKEN",
"deviceInfo": {
"deviceId": "device-uuid",
"deviceName": "iPhone 15 Pro",
"deviceType": "ios"
}
}
```
Response:
```json
{
"appToken": "JWT_TOKEN",
"refreshToken": "REFRESH_TOKEN"
}
```
### POST `/v1/auth/apple-signin`
**⚠️ Important**: The backend expects `deviceInfo` as a **nested object**, not spread at the root level.
Request body:
```json
{
"token": "APPLE_IDENTITY_TOKEN",
"deviceInfo": {
"deviceId": "device-uuid",
"deviceName": "iPhone 15 Pro",
"deviceType": "ios"
}
}
```
Response:
```json
{
"appToken": "JWT_TOKEN",
"refreshToken": "REFRESH_TOKEN"
}
```
**Common Error**: If you send device info spread at root level instead of nested:
```json
// ❌ WRONG - Will fail with "Complete device information is required"
{
"token": "...",
"deviceId": "...",
"deviceName": "...",
"deviceType": "ios"
}
// ✅ CORRECT - deviceInfo as nested object
{
"token": "...",
"deviceInfo": {
"deviceId": "...",
"deviceName": "...",
"deviceType": "ios"
}
}
```
## Testing
### Test on iOS
1. Build and run:
```bash
cd /Users/tillschneider/Documents/__00__Code/manadeck/apps/mobile
npx expo run:ios
```
2. Test Google Sign-In:
- Tap "Mit Google anmelden"
- Should show Google account picker
- Select account
- Should authenticate and navigate to home
3. Test Apple Sign-In:
- Tap "Mit Apple anmelden"
- Should show Face ID/Touch ID prompt
- Authenticate
- Should navigate to home
### Test on Android
1. Build and run:
```bash
npx expo run:android
```
2. Test Google Sign-In:
- Tap "Mit Google anmelden"
- Should show Google account picker
- Select account
- Should authenticate and navigate to home
3. Apple Sign-In won't show on Android (iOS only)
## Project Structure
The social authentication implementation includes:
```
manadeck/apps/mobile/
├── services/
│ └── authService.ts # Extended with Google/Apple methods
├── store/
│ └── authStore.ts # Updated with social sign-in actions
├── components/
│ └── auth/
│ ├── GoogleSignInButton.tsx # Google Sign-In button component
│ └── AppleSignInButton.tsx # Apple Sign-In button component
├── app/
│ └── (auth)/
│ └── login.tsx # Updated with social buttons
├── app.json # Configured with plugins
└── .env.local # OAuth credentials
```
## Troubleshooting
### Google Sign-In Issues
#### "Your app is missing support for the following URL schemes" on iOS
**Cause**: Google Sign-In URL scheme not added to Info.plist
**Solution**:
1. Open `ios/manadeck/Info.plist`
2. Add the Google Sign-In URL scheme to `CFBundleURLTypes`:
```xml
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.YOUR_IOS_CLIENT_ID</string>
</array>
</dict>
```
3. Replace `YOUR_IOS_CLIENT_ID` with your actual iOS Client ID
4. Rebuild the app: `npx expo run:ios`
#### "DEVELOPER_ERROR" on Android
**Cause**: SHA-1 fingerprint mismatch or incorrect web client ID
**Solution**:
```bash
# Get your SHA-1
cd android && ./gradlew signingReport
# Add SHA-1 to Google Cloud Console
# Use Web Client ID (not Android Client ID) in EXPO_PUBLIC_GOOGLE_CLIENT_ID
```
#### "Sign-in failed" on iOS
**Cause**: Incorrect iOS Client ID or bundle identifier mismatch
**Solution**:
- Verify `EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID` matches Google Cloud Console
- Verify bundle identifier in `app.json` matches Google Cloud Console
- Update `reservedClientId` in `app.json` with correct iOS Client ID
#### "Play Services not available"
**Cause**: Google Play Services missing or outdated on Android
**Solution**:
- Update Google Play Services on device
- Test on device with Google Play Store installed
- Use emulator with Google Play
### Apple Sign-In Issues
#### Button doesn't appear
**Cause**: Platform is not iOS
**Solution**: Apple Sign-In only works on iOS 13+
#### "Operation canceled" every time
**Cause**: Sign in with Apple not enabled in developer portal
**Solution**:
- Enable capability in Apple Developer Portal
- Add capability in Xcode
- Ensure `usesAppleSignIn: true` in `app.json`
### Backend Connection Issues
#### "Complete device information is required"
**Cause**: Device info sent incorrectly (spread at root instead of nested object)
**Solution**:
Ensure your authService sends `deviceInfo` as a nested object:
```typescript
// ✅ Correct
body: JSON.stringify({
token: idToken,
deviceInfo, // Nested object
})
// ❌ Wrong
body: JSON.stringify({
token: idToken,
...deviceInfo, // Spread at root - will fail
})
```
#### "Network error" or timeout
**Cause**: Backend URL incorrect or unreachable
**Solution**:
- Verify `EXPO_PUBLIC_API_URL` is correct
- Test backend health endpoint
- Check network connectivity
- Ensure HTTPS is used (not HTTP)
#### "Invalid token" or "Authentication failed"
**Cause**: Backend endpoints not implemented or configured incorrectly
**Solution**:
- Verify backend supports `/v1/auth/google-signin` and `/v1/auth/apple-signin`
- Check backend logs for detailed error messages
- Ensure backend validates tokens correctly with Google/Apple
## Security Considerations
1. **Never commit credentials**: Keep `.env.local` in `.gitignore`
2. **Use HTTPS only**: Always use HTTPS for backend communication
3. **Validate tokens on backend**: Always validate social tokens on the backend
4. **Device binding**: Tokens are bound to device IDs for security
5. **Secure storage**: Tokens stored using platform-specific secure storage
## Production Setup
### For Production Release
**Android Production SHA-1:**
When ready to publish to Google Play, get your production SHA-1:
```bash
# For EAS builds
eas credentials -p android
# Or if using your own keystore
keytool -list -v -keystore /path/to/release.keystore -alias YOUR_KEY_ALIAS
```
Add the production SHA-1 to your Android OAuth client in Google Cloud Console.
**iOS Production:**
The same iOS Client ID works for both development and production.
## Additional Resources
- [React Native Google Sign-In Documentation](https://github.com/react-native-google-signin/google-signin)
- [Expo Apple Authentication Documentation](https://docs.expo.dev/versions/latest/sdk/apple-authentication/)
- [Google OAuth 2.0 Documentation](https://developers.google.com/identity/protocols/oauth2)
- [Apple Sign In Guidelines](https://developer.apple.com/sign-in-with-apple/)
## Quick Reference
### Your Credentials (Fill in as you obtain them)
```
Web OAuth Client ID: ________________________________.apps.googleusercontent.com
iOS OAuth Client ID: ________________________________.apps.googleusercontent.com
Android SHA-1 (Debug): ________________________________
Android SHA-1 (Prod): ________________________________
Apple Team ID: QP3GLU8PH3
Bundle ID: com.tilljs.manadeck
Package Name: com.tilljs.manadeck
```
### Important Commands
```bash
# Get Android SHA-1
cd android && ./gradlew signingReport
# Rebuild native code
npx expo prebuild --clean
# Run on iOS
npx expo run:ios
# Run on Android
npx expo run:android
# Build with EAS
eas build --profile development
```
## Support
For issues or questions:
1. Check the troubleshooting section above
2. Review backend logs for detailed error messages
3. Consult Google Cloud Console and Apple Developer Portal documentation
4. Check that all credentials are correctly configured
---
**Integration completed successfully!** Social authentication is now available in the Manadeck mobile app.

View file

@ -36,13 +36,7 @@
"NSSpeechRecognitionUsageDescription": "Diese App verwendet Spracherkennung, um Sprachaufnahmen in Text umzuwandeln.",
"NSDocumentsFolderUsageDescription": "Diese App benötigt Zugriff auf Dokumente, um Lernmaterialien zu importieren."
},
"appleTeamId": "QP3GLU8PH3",
"config": {
"googleSignIn": {
"reservedClientId": "com.googleusercontent.apps.111768794939-cgen6eklloo7k8vppcaq01o8r8nd7anb"
}
},
"usesAppleSignIn": true
"appleTeamId": "QP3GLU8PH3"
},
"android": {
"adaptiveIcon": {
@ -63,10 +57,6 @@
"projectId": "6cb9cf81-a4d5-4c72-b57d-1be3da8eba35"
}
},
"plugins": [
"expo-router",
"@react-native-google-signin/google-signin",
"expo-apple-authentication"
]
"plugins": ["expo-router"]
}
}

View file

@ -8,8 +8,6 @@ import { Button } from '../../components/ui/Button';
import { Input } from '../../components/ui/Input';
import { Card } from '../../components/ui/Card';
import { useThemeColors } from '~/utils/themeUtils';
import { GoogleSignInButton } from '../../components/auth/GoogleSignInButton';
import { AppleSignInButton } from '../../components/auth/AppleSignInButton';
import { spacing } from '~/utils/spacing';
export default function LoginScreen() {
@ -18,8 +16,7 @@ export default function LoginScreen() {
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
const colors = useThemeColors();
const { signIn, signInWithGoogle, signInWithApple, isLoading, error, clearError } =
useAuthStore();
const { signIn, isLoading, error, clearError } = useAuthStore();
const validateForm = () => {
const newErrors: { email?: string; password?: string } = {};
@ -52,28 +49,6 @@ export default function LoginScreen() {
}
};
const handleGoogleSignIn = async (idToken: string) => {
try {
clearError();
await signInWithGoogle(idToken);
router.replace('/(tabs)');
} catch (err: any) {
// Error is already handled in GoogleSignInButton
throw err;
}
};
const handleAppleSignIn = async (identityToken: string) => {
try {
clearError();
await signInWithApple(identityToken);
router.replace('/(tabs)');
} catch (err: any) {
// Error is already handled in AppleSignInButton
throw err;
}
};
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }} edges={['top']}>
<KeyboardAvoidingView
@ -140,37 +115,6 @@ export default function LoginScreen() {
Anmelden
</Button>
{/* Social Sign-In Divider */}
<View
style={{ marginVertical: spacing.xl, flexDirection: 'row', alignItems: 'center' }}
>
<View style={{ flex: 1, height: 1, backgroundColor: colors.border }} />
<Text
style={{
marginHorizontal: spacing.lg,
color: colors.mutedForeground,
fontSize: 14,
}}
>
oder
</Text>
<View style={{ flex: 1, height: 1, backgroundColor: colors.border }} />
</View>
{/* Social Sign-In Buttons */}
<View style={{ gap: spacing.content.small }}>
<GoogleSignInButton
onSignIn={handleGoogleSignIn}
onSignInSuccess={() => console.log('Google sign-in successful')}
onSignInError={(error) => console.error('Google sign-in error:', error)}
/>
<AppleSignInButton
onSignIn={handleAppleSignIn}
onSignInSuccess={() => console.log('Apple sign-in successful')}
onSignInError={(error) => console.error('Apple sign-in error:', error)}
/>
</View>
<View
style={{
marginTop: spacing.xl,

View file

@ -1,109 +0,0 @@
import React, { useState } from 'react';
import { TouchableOpacity, StyleSheet, Alert, Platform, ActivityIndicator } from 'react-native';
import * as AppleAuthentication from 'expo-apple-authentication';
import { router } from 'expo-router';
import { Text } from '~/components/ui/Text';
import { useThemeColors } from '~/utils/themeUtils';
import { Ionicons } from '@expo/vector-icons';
interface AppleSignInButtonProps {
onSignInSuccess?: () => void;
onSignInError?: (error: string) => void;
onSignIn: (identityToken: string) => Promise<void>;
}
export const AppleSignInButton: React.FC<AppleSignInButtonProps> = ({
onSignInSuccess,
onSignInError,
onSignIn,
}) => {
const colors = useThemeColors();
const [isLoading, setIsLoading] = useState(false);
// Apple Sign-In is iOS only
if (Platform.OS !== 'ios') {
return null;
}
const handleAppleSignIn = async () => {
setIsLoading(true);
try {
// Trigger Apple Sign-In
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
const identityToken = credential.identityToken;
if (!identityToken) {
Alert.alert('Anmeldung fehlgeschlagen', 'Kein Identity Token von Apple erhalten');
if (onSignInError) {
onSignInError('Kein Identity Token erhalten');
}
return;
}
console.log('Got Apple identity token');
// Send to backend for validation
await onSignIn(identityToken);
if (onSignInSuccess) {
onSignInSuccess();
}
} catch (error: any) {
if (error.code === 'ERR_REQUEST_CANCELED') {
console.log('User cancelled Apple sign-in');
} else {
console.error('Apple Sign-In Error:', error);
const errorMessage = error.message || 'Anmeldung mit Apple fehlgeschlagen';
Alert.alert('Anmeldefehler', errorMessage);
if (onSignInError) {
onSignInError(errorMessage);
}
}
} finally {
setIsLoading(false);
}
};
return (
<TouchableOpacity
style={[styles.button, { backgroundColor: '#000000' }]}
onPress={handleAppleSignIn}
disabled={isLoading}
activeOpacity={0.7}
>
{isLoading ? (
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<>
<Ionicons name="logo-apple" size={20} color="#FFFFFF" style={styles.icon} />
<Text style={styles.buttonText}>Mit Apple anmelden</Text>
</>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 16,
borderRadius: 8,
minHeight: 52,
},
icon: {
marginRight: 12,
},
buttonText: {
fontSize: 16,
fontWeight: '600',
color: '#FFFFFF',
},
});

View file

@ -1,120 +0,0 @@
import React, { useEffect, useState } from 'react';
import { TouchableOpacity, StyleSheet, Alert, Platform, ActivityIndicator } from 'react-native';
import { GoogleSignin, statusCodes } from '@react-native-google-signin/google-signin';
import { router } from 'expo-router';
import { Text } from '~/components/ui/Text';
import { useThemeColors } from '~/utils/themeUtils';
import { Ionicons } from '@expo/vector-icons';
interface GoogleSignInButtonProps {
onSignInSuccess?: () => void;
onSignInError?: (error: string) => void;
onSignIn: (idToken: string) => Promise<void>;
}
export const GoogleSignInButton: React.FC<GoogleSignInButtonProps> = ({
onSignInSuccess,
onSignInError,
onSignIn,
}) => {
const colors = useThemeColors();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
// Configure Google Sign-In
GoogleSignin.configure({
webClientId: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID,
iosClientId: process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID,
scopes: ['profile', 'email'],
offlineAccess: false,
});
}, []);
const handleGoogleSignIn = async () => {
setIsLoading(true);
try {
// Check Google Play Services (Android only)
if (Platform.OS === 'android') {
await GoogleSignin.hasPlayServices();
}
// Trigger Google Sign-In
await GoogleSignin.signIn();
// Get ID token
const tokens = await GoogleSignin.getTokens();
const idToken = tokens.idToken;
console.log('Got Google ID token');
// Send to backend for validation
await onSignIn(idToken);
if (onSignInSuccess) {
onSignInSuccess();
}
} catch (error: any) {
console.error('Google Sign-In Error:', error);
// Handle specific error codes
if (error.code === statusCodes.SIGN_IN_CANCELLED) {
console.log('User cancelled sign-in');
} else if (error.code === statusCodes.IN_PROGRESS) {
console.log('Sign-in already in progress');
} else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
Alert.alert(
'Google Play Services',
'Google Play Services ist nicht verfügbar oder veraltet. Bitte aktualisieren Sie es.'
);
if (onSignInError) {
onSignInError('Google Play Services nicht verfügbar');
}
} else {
const errorMessage = error.message || 'Anmeldung mit Google fehlgeschlagen';
Alert.alert('Anmeldefehler', errorMessage);
if (onSignInError) {
onSignInError(errorMessage);
}
}
} finally {
setIsLoading(false);
}
};
return (
<TouchableOpacity
style={[styles.button, { backgroundColor: '#FFFFFF', borderColor: colors.border }]}
onPress={handleGoogleSignIn}
disabled={isLoading}
activeOpacity={0.7}
>
{isLoading ? (
<ActivityIndicator size="small" color="#4285F4" />
) : (
<>
<Ionicons name="logo-google" size={20} color="#4285F4" style={styles.icon} />
<Text style={[styles.buttonText, { color: colors.foreground }]}>Mit Google anmelden</Text>
</>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 16,
borderRadius: 8,
borderWidth: 1,
minHeight: 52,
},
icon: {
marginRight: 12,
},
buttonText: {
fontSize: 16,
fontWeight: '600',
},
});

View file

@ -22,13 +22,11 @@
"@expo/vector-icons": "^15.0.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-google-signin/google-signin": "^14.0.2",
"@react-native-segmented-control/segmented-control": "2.5.7",
"@react-navigation/native": "^7.0.3",
"base64-js": "^1.5.1",
"class-variance-authority": "^0.7.1",
"expo": "54.0.13",
"expo-apple-authentication": "~8.0.7",
"expo-blur": "~15.0.7",
"expo-build-properties": "~1.0.9",
"expo-constants": "~18.0.9",

View file

@ -223,42 +223,6 @@ export const authService = {
}
},
signInWithGoogle: async (
idToken: string
): Promise<{ success: boolean; user?: ManaUser; error?: string }> => {
try {
const result = await _sharedAuth.signInWithGoogle(idToken);
if (!result.success) {
return { success: false, error: result.error || 'Google sign-in failed' };
}
const userData = await _sharedAuth.getUserFromToken();
return { success: true, user: toManaUser(userData) || undefined };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
},
signInWithApple: async (
identityToken: string
): Promise<{ success: boolean; user?: ManaUser; error?: string }> => {
try {
const result = await _sharedAuth.signInWithApple(identityToken);
if (!result.success) {
return { success: false, error: result.error || 'Apple sign-in failed' };
}
const userData = await _sharedAuth.getUserFromToken();
return { success: true, user: toManaUser(userData) || undefined };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
},
onTokenRefresh: null as ((userData: { id: string; email: string; role: string }) => void) | null,
};

View file

@ -11,8 +11,6 @@ interface AuthState {
initialize: () => Promise<void>;
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string, username: string) => Promise<void>;
signInWithGoogle: (idToken: string) => Promise<void>;
signInWithApple: (identityToken: string) => Promise<void>;
signOut: () => Promise<void>;
resetPassword: (email: string) => Promise<void>;
updateProfile: (updates: {
@ -110,44 +108,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
}
},
signInWithGoogle: async (idToken: string) => {
try {
set({ isLoading: true, error: null });
const result = await authService.signInWithGoogle(idToken);
if (!result.success) {
throw new Error(result.error || 'Google sign-in failed');
}
set({ user: result.user || null });
} catch (error: any) {
set({ error: error.message || 'Google sign-in failed' });
throw error;
} finally {
set({ isLoading: false });
}
},
signInWithApple: async (identityToken: string) => {
try {
set({ isLoading: true, error: null });
const result = await authService.signInWithApple(identityToken);
if (!result.success) {
throw new Error(result.error || 'Apple sign-in failed');
}
set({ user: result.user || null });
} catch (error: any) {
set({ error: error.message || 'Apple sign-in failed' });
throw error;
} finally {
set({ isLoading: false });
}
},
signOut: async () => {
try {
set({ isLoading: true, error: null });

View file

@ -140,8 +140,6 @@ export const authService = createAuthService({
refresh: '/api/v1/auth/refresh',
validate: '/api/v1/auth/validate',
forgotPassword: '/api/v1/auth/forgot-password',
googleSignIn: '/api/v1/auth/google-signin',
appleSignIn: '/api/v1/auth/apple-signin',
credits: '/api/v1/credits/balance',
},
});

View file

@ -33,8 +33,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect="/decks"
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -51,8 +51,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -53,8 +53,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -44,8 +44,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -2,25 +2,17 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { LoginPage, setGoogleClientId } from '@manacore/shared-auth-ui';
import { LoginPage } from '@manacore/shared-auth-ui';
import { getLoginTranslations } from '@manacore/shared-i18n';
import PictureLogo from '$lib/components/branding/PictureLogo.svelte';
import AppSlider from '$lib/components/AppSlider.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { APP_VERSION, BUILD_TIME } from '$lib/version';
import { onMount } from 'svelte';
import { PUBLIC_GOOGLE_CLIENT_ID, PUBLIC_APPLE_CLIENT_ID } from '$env/static/public';
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
onMount(() => {
if (PUBLIC_GOOGLE_CLIENT_ID) {
setGoogleClientId(PUBLIC_GOOGLE_CLIENT_ID);
}
});
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
@ -29,16 +21,6 @@
return authStore.resendVerificationEmail(email);
}
async function handleSignInWithGoogle() {
// TODO: Implement OAuth with Mana Core Auth when ready
return { success: false, error: 'Google Sign-In not yet implemented' };
}
async function handleSignInWithApple() {
// TODO: Implement OAuth with Mana Core Auth when ready
return { success: false, error: 'Apple Sign-In not yet implemented' };
}
// Read verification status from query params (set after email verification)
const verified = $derived($page.url.searchParams.get('verified') === 'true');
const initialEmail = $derived($page.url.searchParams.get('email') || '');
@ -54,11 +36,7 @@
primaryColor="#3b82f6"
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
onSignInWithGoogle={PUBLIC_GOOGLE_CLIENT_ID ? handleSignInWithGoogle : undefined}
onSignInWithApple={PUBLIC_APPLE_CLIENT_ID ? handleSignInWithApple : undefined}
{goto}
enableGoogle={!!PUBLIC_GOOGLE_CLIENT_ID}
enableApple={!!PUBLIC_APPLE_CLIENT_ID}
successRedirect="/app/gallery"
registerPath="/auth/signup"
forgotPasswordPath="/auth/forgot-password"

View file

@ -53,8 +53,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -35,8 +35,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -41,8 +41,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -59,8 +59,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -59,8 +59,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -56,8 +56,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -54,8 +54,6 @@
onSignIn={handleSignIn}
onResendVerification={handleResendVerification}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"

View file

@ -1,71 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { initializeAppleAuth, signInWithApple, waitForAppleAuth } from '../utils/appleAuth';
interface Props {
onError?: (error: Error) => void;
}
let { onError }: Props = $props();
let isLoading = $state(false);
let error = $state<string | null>(null);
let sdkLoaded = $state(false);
async function handleAppleSignIn() {
isLoading = true;
error = null;
try {
await signInWithApple();
} catch (err) {
console.error('Error initiating Apple Sign-In:', err);
error = err instanceof Error ? err.message : 'Failed to initiate Apple Sign-In';
onError?.(err instanceof Error ? err : new Error('Unknown error during Apple Sign-In'));
isLoading = false;
}
}
onMount(async () => {
try {
await waitForAppleAuth();
const initialized = initializeAppleAuth();
if (initialized) {
sdkLoaded = true;
} else {
console.warn('Apple Sign-In not configured - hiding button');
}
} catch (err) {
console.error('Error loading Apple Sign-In:', err);
}
});
</script>
{#if sdkLoaded}
<div class="space-y-3">
{#if error}
<div class="rounded-xl bg-red-500/20 border border-red-500/30 p-3 text-sm text-red-500">
{error}
</div>
{/if}
<button
onclick={handleAppleSignIn}
disabled={isLoading}
class="flex h-14 w-full items-center justify-center gap-2 rounded-xl bg-black border border-gray-800 px-4 font-medium text-white transition-all hover:bg-gray-900 disabled:opacity-50"
>
{#if isLoading}
<div
class="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent"
></div>
{:else}
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path
d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"
/>
</svg>
{/if}
<span>{isLoading ? 'Signing in...' : 'Continue with Apple'}</span>
</button>
</div>
{/if}

View file

@ -1,84 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { initializeGoogleAuth, renderGoogleButton, waitForGoogleAuth } from '../utils/googleAuth';
interface Props {
onSuccess: (idToken: string) => Promise<void>;
onError?: (error: Error) => void;
}
let { onSuccess, onError }: Props = $props();
let buttonContainer: HTMLDivElement;
let isLoading = $state(false);
let error = $state<string | null>(null);
async function handleGoogleSignIn(idToken: string) {
isLoading = true;
error = null;
try {
await onSuccess(idToken);
} catch (err) {
console.error('Error during Google Sign-In:', err);
error = err instanceof Error ? err.message : 'Google Sign-In failed';
onError?.(err instanceof Error ? err : new Error('Unknown error during Google Sign-In'));
} finally {
isLoading = false;
}
}
onMount(async () => {
try {
await waitForGoogleAuth();
initializeGoogleAuth(handleGoogleSignIn);
if (buttonContainer) {
renderGoogleButton(buttonContainer, {
type: 'standard',
theme: 'outline',
size: 'large',
text: 'signin_with',
shape: 'pill',
});
}
} catch (err) {
console.error('Error initializing Google Sign-In:', err);
error = 'Failed to load Google Sign-In';
}
});
</script>
{#if error}
<div class="rounded-xl bg-red-500/20 border border-red-500/30 p-3 text-sm text-red-500 mb-2">
{error}
</div>
{/if}
<div
bind:this={buttonContainer}
class="relative w-full google-btn-wrapper"
style="min-height: 56px;"
>
{#if isLoading}
<div
class="absolute inset-0 flex items-center justify-center rounded-xl bg-white/80 dark:bg-black/80 backdrop-blur-sm z-10"
>
<div
class="h-6 w-6 animate-spin rounded-full border-2 border-indigo-500 border-t-transparent"
></div>
</div>
{/if}
</div>
<style>
:global(.google-btn-wrapper > div) {
width: 100% !important;
height: 56px !important;
}
:global(.google-btn-wrapper iframe) {
height: 56px !important;
border-radius: 0.75rem !important;
}
</style>

View file

@ -4,34 +4,12 @@ export { default as RegisterPage } from './pages/RegisterPage.svelte';
export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.svelte';
// Components
export { default as GoogleSignInButton } from './components/GoogleSignInButton.svelte';
export { default as AppleSignInButton } from './components/AppleSignInButton.svelte';
export { default as GuestWelcomeModal } from './components/GuestWelcomeModal.svelte';
export { default as AuthGateModal } from './components/AuthGateModal.svelte';
export { default as SessionExpiredBanner } from './components/SessionExpiredBanner.svelte';
export { default as AuthGate } from './components/AuthGate.svelte';
// Utilities
export {
setGoogleClientId,
initializeGoogleAuth,
renderGoogleButton,
isGoogleAuthLoaded,
waitForGoogleAuth,
} from './utils/googleAuth';
export {
setAppleConfig,
initializeAppleAuth,
signInWithApple,
parseAppleAuthorizationResponse,
getStoredReturnUrl,
clearAppleSignInSession,
isAppleAuthLoaded,
waitForAppleAuth,
type AppleAuthorizationResponse,
} from './utils/appleAuth';
export {
shouldShowGuestWelcome,
markGuestWelcomeSeen,

View file

@ -2,9 +2,6 @@
import type { Component, Snippet } from 'svelte';
import type { AuthResult } from '../types';
import { Check, Warning, Eye, EyeSlash, SignIn, Sun, Moon } from '@manacore/shared-icons';
import GoogleSignInButton from '../components/GoogleSignInButton.svelte';
import AppleSignInButton from '../components/AppleSignInButton.svelte';
/** Translation strings for the login page */
export interface LoginTranslations {
title: string;
@ -26,9 +23,7 @@
emailInvalid: string;
passwordRequired: string;
signInFailed: string;
googleSignInFailed: string;
signInSuccess: string;
googleSignInSuccess: string;
emailVerified?: string;
emailNotVerified?: string;
resendVerification?: string;
@ -56,9 +51,7 @@
emailInvalid: 'Please enter a valid email address',
passwordRequired: 'Password is required',
signInFailed: 'Sign in failed',
googleSignInFailed: 'Google sign in failed',
signInSuccess: 'Successfully signed in. Redirecting...',
googleSignInSuccess: 'Successfully signed in with Google. Redirecting...',
emailVerified: 'Email successfully verified! Please sign in.',
emailNotVerified: 'Email not verified.',
resendVerification: 'Resend verification email',
@ -71,12 +64,8 @@
logo: Component<{ size?: number; color?: string }>;
primaryColor: string;
onSignIn: (email: string, password: string) => Promise<AuthResult>;
onSignInWithGoogle?: (idToken: string) => Promise<AuthResult>;
onSignInWithApple?: (identityToken: string) => Promise<AuthResult>;
onResendVerification?: (email: string) => Promise<AuthResult>;
goto: (path: string) => void;
enableGoogle?: boolean;
enableApple?: boolean;
successRedirect?: string;
registerPath?: string;
forgotPasswordPath?: string;
@ -102,12 +91,8 @@
logo: Logo,
primaryColor,
onSignIn,
onSignInWithGoogle,
onSignInWithApple,
onResendVerification,
goto,
enableGoogle = false,
enableApple = false,
successRedirect = '/dashboard',
registerPath = '/register',
forgotPasswordPath = '/forgot-password',
@ -269,23 +254,6 @@
}
}
async function handleGoogleSuccess(idToken: string) {
if (!onSignInWithGoogle) return;
loading = true;
clearError();
const result = await onSignInWithGoogle(idToken);
loading = false;
if (result.success) {
showSuccess = true;
successAnnouncement = t.googleSignInSuccess;
setTimeout(() => goto(successRedirect), 600);
} else {
setError(result.error || t.googleSignInFailed, 'general');
}
}
function skipToForm() {
if (emailInput) emailInput.focus();
}
@ -519,20 +487,6 @@
</button>
</form>
{#if enableGoogle || enableApple}
<div class="divider">
<span>{t.orDivider}</span>
</div>
<div class="social-buttons">
{#if enableGoogle && onSignInWithGoogle}
<GoogleSignInButton onSuccess={handleGoogleSuccess} />
{/if}
{#if enableApple}
<AppleSignInButton />
{/if}
</div>
{/if}
<p class="register-link">
{t.noAccount}
<button type="button" onclick={() => goto(registerPath)} style:color={primaryColor}>
@ -973,38 +927,6 @@
cursor: not-allowed;
}
.divider {
display: flex;
align-items: center;
gap: 1rem;
margin: 1.25rem 0;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: currentColor;
opacity: 0.2;
}
.divider span {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
}
.light .divider span {
color: rgba(0, 0, 0, 0.5);
}
.social-buttons {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.register-link {
text-align: center;
font-size: 0.875rem;

View file

@ -24,21 +24,6 @@ export interface AuthUIConfig {
/** Redirect path after successful login (default: '/dashboard') */
successRedirect?: string;
/** Enable Google Sign-In */
enableGoogle?: boolean;
/** Enable Apple Sign-In */
enableApple?: boolean;
/** Google OAuth Client ID (required if enableGoogle is true) */
googleClientId?: string;
/** Apple OAuth Service ID (required if enableApple is true) */
appleClientId?: string;
/** Apple OAuth Redirect URI */
appleRedirectUri?: string;
}
/**
@ -47,8 +32,6 @@ export interface AuthUIConfig {
export interface AuthServiceInterface {
signIn(email: string, password: string): Promise<AuthResult>;
signUp(email: string, password: string): Promise<AuthResult>;
signInWithGoogle?(idToken: string): Promise<AuthResult>;
signInWithApple?(identityToken: string): Promise<AuthResult>;
forgotPassword(email: string): Promise<AuthResult>;
}

View file

@ -1,216 +0,0 @@
/**
* Apple Sign-In integration for web
* Uses redirect flow (not popup)
*/
// TypeScript definitions for Apple ID SDK
declare global {
interface Window {
AppleID?: {
auth: {
init: (config: AppleIDInitConfig) => void;
signIn: () => Promise<AppleIDSignInResponse>;
};
};
}
}
interface AppleIDInitConfig {
clientId: string;
scope: string;
redirectURI: string;
state?: string;
nonce?: string;
usePopup?: boolean;
responseType?: string;
responseMode?: string;
}
interface AppleIDSignInResponse {
authorization: {
code: string;
id_token?: string;
state?: string;
};
user?: {
email?: string;
name?: {
firstName?: string;
lastName?: string;
};
};
}
export interface AppleAuthorizationResponse {
code: string;
id_token?: string;
state?: string;
user?: string;
}
let appleClientId: string | null = null;
let appleRedirectUri: string | null = null;
/**
* Set Apple Sign-In configuration
*/
export function setAppleConfig(clientId: string, redirectUri: string) {
appleClientId = clientId;
appleRedirectUri = redirectUri;
}
/**
* Check if running in browser
*/
function isBrowser(): boolean {
return typeof window !== 'undefined';
}
/**
* Initialize Apple ID SDK
*/
export function initializeAppleAuth(): boolean {
if (!isBrowser() || !window.AppleID) {
console.warn('Apple ID SDK not loaded');
return false;
}
if (!appleClientId || !appleRedirectUri) {
console.error('Apple Sign-In not configured. Call setAppleConfig() first.');
return false;
}
try {
window.AppleID.auth.init({
clientId: appleClientId,
scope: 'name email',
redirectURI: appleRedirectUri,
state: generateState(),
usePopup: false,
responseType: 'code id_token',
responseMode: 'form_post',
});
console.log('Apple ID SDK initialized successfully');
return true;
} catch (error) {
console.error('Error initializing Apple ID SDK:', error);
return false;
}
}
/**
* Initiate Apple Sign-In (redirect flow)
*/
export async function signInWithApple(): Promise<void> {
if (!isBrowser()) {
throw new Error('Apple Sign-In only available in browser');
}
if (!window.AppleID) {
throw new Error('Apple ID SDK not loaded');
}
try {
const returnTo = window.location.pathname + window.location.search;
sessionStorage.setItem('apple_signin_return_to', returnTo);
await window.AppleID.auth.signIn();
} catch (error) {
console.error('Error initiating Apple Sign-In:', error);
throw error;
}
}
/**
* Parse Apple authorization response from URL
*/
export function parseAppleAuthorizationResponse(
urlParams: URLSearchParams
): AppleAuthorizationResponse | null {
const code = urlParams.get('code');
const id_token = urlParams.get('id_token');
const state = urlParams.get('state');
const user = urlParams.get('user');
const error = urlParams.get('error');
if (error) {
console.error('Apple Sign-In error:', error);
return null;
}
const storedState = sessionStorage.getItem('apple_signin_state');
if (state !== storedState) {
console.error('State mismatch - possible CSRF attack');
return null;
}
if (!id_token && !code) {
console.error('No id_token or authorization code in Apple response');
return null;
}
return {
code: code || '',
id_token: id_token || undefined,
state: state || undefined,
user: user || undefined,
};
}
/**
* Generate random state for CSRF protection
*/
function generateState(): string {
const state = Math.random().toString(36).substring(2, 15);
if (isBrowser()) {
sessionStorage.setItem('apple_signin_state', state);
}
return state;
}
/**
* Get stored return URL
*/
export function getStoredReturnUrl(): string {
if (!isBrowser()) return '/dashboard';
return sessionStorage.getItem('apple_signin_return_to') || '/dashboard';
}
/**
* Clear Apple Sign-In session data
*/
export function clearAppleSignInSession() {
if (!isBrowser()) return;
sessionStorage.removeItem('apple_signin_state');
sessionStorage.removeItem('apple_signin_return_to');
}
/**
* Check if Apple ID SDK is loaded
*/
export function isAppleAuthLoaded(): boolean {
return isBrowser() && !!window.AppleID?.auth;
}
/**
* Wait for Apple ID SDK to load
*/
export function waitForAppleAuth(timeout = 10000): Promise<void> {
return new Promise((resolve, reject) => {
if (isAppleAuthLoaded()) {
resolve();
return;
}
const startTime = Date.now();
const interval = setInterval(() => {
if (isAppleAuthLoaded()) {
clearInterval(interval);
resolve();
} else if (Date.now() - startTime > timeout) {
clearInterval(interval);
reject(new Error('Apple ID SDK failed to load'));
}
}, 100);
});
}

View file

@ -1,174 +0,0 @@
/**
* Google Identity Services integration
* Provides helper functions for Google Sign-In on web
*/
// TypeScript definitions for Google Identity Services
declare global {
interface Window {
google?: {
accounts: {
id: {
initialize: (config: GoogleIdConfiguration) => void;
prompt: (momentListener?: (notification: PromptMomentNotification) => void) => void;
renderButton: (parent: HTMLElement, options: GsiButtonConfiguration) => void;
disableAutoSelect: () => void;
storeCredential: (credential: { id: string; password: string }) => void;
cancel: () => void;
onGoogleLibraryLoad: () => void;
revoke: (hint: string, callback: (done: RevocationResponse) => void) => void;
};
};
};
}
}
interface GoogleIdConfiguration {
client_id: string;
callback: (response: CredentialResponse) => void;
auto_select?: boolean;
cancel_on_tap_outside?: boolean;
context?: 'signin' | 'signup' | 'use';
ux_mode?: 'popup' | 'redirect';
login_uri?: string;
native_callback?: (response: { id: string; password: string }) => void;
itp_support?: boolean;
}
interface CredentialResponse {
credential: string;
select_by: string;
clientId?: string;
}
interface GsiButtonConfiguration {
type?: 'standard' | 'icon';
theme?: 'outline' | 'filled_blue' | 'filled_black';
size?: 'large' | 'medium' | 'small';
text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin';
shape?: 'rectangular' | 'pill' | 'circle' | 'square';
logo_alignment?: 'left' | 'center';
width?: string;
locale?: string;
}
interface PromptMomentNotification {
isDisplayMoment: () => boolean;
isDisplayed: () => boolean;
isNotDisplayed: () => boolean;
getNotDisplayedReason: () => string;
isSkippedMoment: () => boolean;
getSkippedReason: () => string;
isDismissedMoment: () => boolean;
getDismissedReason: () => string;
getMomentType: () => 'display' | 'skipped' | 'dismissed';
}
interface RevocationResponse {
successful: boolean;
error?: string;
}
let googleClientId: string | null = null;
/**
* Set Google Client ID for initialization
*/
export function setGoogleClientId(clientId: string) {
googleClientId = clientId;
}
/**
* Initialize Google Identity Services
*/
export function initializeGoogleAuth(callback: (idToken: string) => void) {
if (typeof window === 'undefined') {
console.warn('Google Auth: Cannot initialize on server-side');
return;
}
if (!window.google) {
console.warn('Google Identity Services not loaded yet');
return;
}
if (!googleClientId) {
console.error('Google Client ID not configured. Call setGoogleClientId() first.');
return;
}
try {
window.google.accounts.id.initialize({
client_id: googleClientId,
callback: (response: CredentialResponse) => {
callback(response.credential);
},
auto_select: false,
cancel_on_tap_outside: true,
ux_mode: 'popup',
});
} catch (error) {
console.error('Error initializing Google Auth:', error);
}
}
/**
* Render Google Sign-In button
*/
export function renderGoogleButton(
element: HTMLElement,
options?: Partial<GsiButtonConfiguration>
) {
if (typeof window === 'undefined' || !window.google) {
console.warn('Google Identity Services not available');
return;
}
const defaultOptions: GsiButtonConfiguration = {
type: 'standard',
theme: 'outline',
size: 'large',
text: 'signin_with',
shape: 'rectangular',
logo_alignment: 'left',
};
try {
window.google.accounts.id.renderButton(element, {
...defaultOptions,
...options,
});
} catch (error) {
console.error('Error rendering Google button:', error);
}
}
/**
* Check if Google Identity Services is loaded
*/
export function isGoogleAuthLoaded(): boolean {
return typeof window !== 'undefined' && !!window.google?.accounts?.id;
}
/**
* Wait for Google Identity Services to load
*/
export function waitForGoogleAuth(timeout = 10000): Promise<void> {
return new Promise((resolve, reject) => {
if (isGoogleAuthLoaded()) {
resolve();
return;
}
const startTime = Date.now();
const interval = setInterval(() => {
if (isGoogleAuthLoaded()) {
clearInterval(interval);
resolve();
} else if (Date.now() - startTime > timeout) {
clearInterval(interval);
reject(new Error('Google Identity Services failed to load'));
}
}, 100);
});
}

View file

@ -55,8 +55,6 @@ const DEFAULT_ENDPOINTS: AuthEndpoints = {
forgotPassword: '/api/v1/auth/forgot-password',
resetPassword: '/api/v1/auth/reset-password',
resendVerification: '/api/v1/auth/resend-verification',
googleSignIn: '/api/v1/auth/google-signin',
appleSignIn: '/api/v1/auth/apple-signin',
credits: '/api/v1/credits/balance',
// Better Auth native endpoints for SSO
getSession: '/api/auth/get-session',
@ -360,76 +358,6 @@ export function createAuthService(config: AuthServiceConfig) {
return { appToken, refreshToken, userData };
},
/**
* Sign in with Google
*/
async signInWithGoogle(idToken: string): Promise<AuthResult> {
const result = await service.signInWithSocial(idToken, endpoints.googleSignIn);
trackAuth(result.success ? 'login' : 'login_failed', { method: 'google' });
return result;
},
/**
* Sign in with Apple
*/
async signInWithApple(identityToken: string): Promise<AuthResult> {
const result = await service.signInWithSocial(identityToken, endpoints.appleSignIn);
trackAuth(result.success ? 'login' : 'login_failed', { method: 'apple' });
return result;
},
/**
* Internal: Sign in with social provider
*/
async signInWithSocial(token: string, endpoint: string): Promise<AuthResult> {
try {
const storage = getStorageAdapter();
const deviceAdapter = getDeviceAdapter();
const deviceInfo = await deviceAdapter.getDeviceInfo();
const response = await fetch(`${baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, deviceInfo }),
});
if (!response.ok) {
const errorData = await response.json();
return { success: false, error: errorData.message || 'Social sign in failed' };
}
const responseData = await response.json();
const { appToken, refreshToken } = responseData;
// Extract email from response or token
let email = responseData.email;
if (!email && appToken) {
const userData = getUserFromToken(appToken);
email = userData?.email;
}
// Store tokens
const storagePromises = [
storage.setItem(storageKeys.APP_TOKEN, appToken),
storage.setItem(storageKeys.REFRESH_TOKEN, refreshToken),
];
if (email) {
storagePromises.push(storage.setItem(storageKeys.USER_EMAIL, email));
}
await Promise.all(storagePromises);
return { success: true };
} catch (error) {
console.error('Error with social sign in:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error during social sign in',
};
}
},
/**
* Get the current app token
*/

View file

@ -130,8 +130,6 @@ export interface AuthEndpoints {
forgotPassword: string;
resetPassword: string;
resendVerification: string;
googleSignIn: string;
appleSignIn: string;
credits: string;
/** Better Auth native endpoint for SSO session check */
getSession: string;

View file

@ -19,9 +19,7 @@
"emailInvalid": "Bitte gib eine gültige E-Mail-Adresse ein",
"passwordRequired": "Passwort ist erforderlich",
"signInFailed": "Anmeldung fehlgeschlagen",
"googleSignInFailed": "Google-Anmeldung fehlgeschlagen",
"signInSuccess": "Erfolgreich angemeldet. Weiterleitung...",
"googleSignInSuccess": "Erfolgreich mit Google angemeldet. Weiterleitung...",
"emailVerified": "E-Mail erfolgreich bestätigt! Bitte melde dich an.",
"emailNotVerified": "E-Mail nicht bestätigt.",
"resendVerification": "Bestätigungs-E-Mail erneut senden",

View file

@ -19,9 +19,7 @@
"emailInvalid": "Please enter a valid email address",
"passwordRequired": "Password is required",
"signInFailed": "Sign in failed",
"googleSignInFailed": "Google sign in failed",
"signInSuccess": "Successfully signed in. Redirecting...",
"googleSignInSuccess": "Successfully signed in with Google. Redirecting...",
"emailVerified": "Email successfully verified! Please sign in.",
"emailNotVerified": "Email not verified.",
"resendVerification": "Resend verification email",

View file

@ -19,9 +19,7 @@
"emailInvalid": "Por favor ingresa un correo electrónico válido",
"passwordRequired": "La contraseña es obligatoria",
"signInFailed": "Error al iniciar sesión",
"googleSignInFailed": "Error al iniciar sesión con Google",
"signInSuccess": "Sesión iniciada correctamente. Redirigiendo...",
"googleSignInSuccess": "Sesión iniciada con Google correctamente. Redirigiendo...",
"emailVerified": "¡Correo verificado exitosamente! Por favor inicia sesión.",
"emailNotVerified": "Correo no verificado.",
"resendVerification": "Reenviar correo de verificación",

View file

@ -19,9 +19,7 @@
"emailInvalid": "Veuillez entrer une adresse email valide",
"passwordRequired": "Le mot de passe est requis",
"signInFailed": "Échec de la connexion",
"googleSignInFailed": "Échec de la connexion Google",
"signInSuccess": "Connexion réussie. Redirection...",
"googleSignInSuccess": "Connexion Google réussie. Redirection...",
"emailVerified": "Email vérifié avec succès ! Veuillez vous connecter.",
"emailNotVerified": "Email non vérifié.",
"resendVerification": "Renvoyer l'email de vérification",

View file

@ -34,9 +34,7 @@ export interface AuthTranslations {
emailInvalid: string;
passwordRequired: string;
signInFailed: string;
googleSignInFailed: string;
signInSuccess: string;
googleSignInSuccess: string;
emailVerified?: string;
emailNotVerified?: string;
resendVerification?: string;

View file

@ -19,9 +19,7 @@
"emailInvalid": "Inserisci un indirizzo email valido",
"passwordRequired": "La password è obbligatoria",
"signInFailed": "Accesso fallito",
"googleSignInFailed": "Accesso con Google fallito",
"signInSuccess": "Accesso effettuato. Reindirizzamento...",
"googleSignInSuccess": "Accesso con Google effettuato. Reindirizzamento...",
"emailVerified": "Email verificata con successo! Effettua l'accesso.",
"emailNotVerified": "Email non verificata.",
"resendVerification": "Invia di nuovo l'email di verifica",

1001
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -213,8 +213,6 @@ const APP_CONFIGS = [
vars: {
PUBLIC_BACKEND_URL: (env) => env.PICTURE_BACKEND_URL || 'http://localhost:3003',
PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
PUBLIC_GOOGLE_CLIENT_ID: (env) => env.PICTURE_GOOGLE_CLIENT_ID || '',
PUBLIC_APPLE_CLIENT_ID: (env) => env.PICTURE_APPLE_CLIENT_ID || '',
PUBLIC_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_PICTURE || '',
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
},