mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 17:41:09 +02:00
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:
parent
30a0a651ac
commit
2d11ba6248
46 changed files with 499 additions and 2253 deletions
|
|
@ -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
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -57,8 +57,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -64,8 +64,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -55,8 +55,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -54,8 +54,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -49,8 +49,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect="/dashboard"
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect="/decks"
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -51,8 +51,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -53,8 +53,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -53,8 +53,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -59,8 +59,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -59,8 +59,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -54,8 +54,6 @@
|
|||
onSignIn={handleSignIn}
|
||||
onResendVerification={handleResendVerification}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
1001
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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 || '',
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue