mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21: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_WEBHOOK_SECRET=whsec_103705b73101b783a91305a9ec272834df6a096ffb2c2566b1c899318a156b03
|
||||||
|
|
||||||
# Stripe Product & Price IDs (ManaCore Unified Plans - Live)
|
# 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_PRODUCT_ID=prod_TzNUGcq9qx9rRT
|
||||||
STRIPE_PLUS_PRICE_MONTHLY=price_1T1OkKAZjQCYS0ZJ88m0shoN
|
STRIPE_PLUS_PRICE_MONTHLY=price_1TEt04AZjQCYS0ZJUuUWt3yg
|
||||||
STRIPE_PLUS_PRICE_YEARLY=price_1T1OkLAZjQCYS0ZJ4IdMzVyJ
|
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_PRODUCT_ID=prod_TzNUgWeBjT35qn
|
||||||
STRIPE_PRO_PRICE_MONTHLY=price_1T1OkLAZjQCYS0ZJvyPM7Wop
|
STRIPE_PRO_PRICE_MONTHLY=price_1TEt05AZjQCYS0ZJjmjsUdkJ
|
||||||
STRIPE_PRO_PRICE_YEARLY=price_1T1OkLAZjQCYS0ZJDbZeuOOu
|
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_PRODUCT_ID=prod_TzNUE5pTbTDdbp
|
||||||
STRIPE_ULTRA_PRICE_MONTHLY=price_1T1OkMAZjQCYS0ZJYCJNZtg8
|
STRIPE_ULTRA_PRICE_MONTHLY=price_1TEt06AZjQCYS0ZJdyTDjfUk
|
||||||
STRIPE_ULTRA_PRICE_YEARLY=price_1T1OkMAZjQCYS0ZJvCvR6Ve6
|
STRIPE_ULTRA_PRICE_YEARLY=price_1TEt06AZjQCYS0ZJ6Wq3gNpM
|
||||||
|
|
||||||
# Credit Packs (One-time purchases)
|
# Mana Tränke (One-time purchases, 1 Mana = 1.4 Cent)
|
||||||
STRIPE_CREDITS_100_PRODUCT_ID=prod_TzNUvyjD4hrcUR
|
STRIPE_POTION_SMALL_PRODUCT_ID=prod_UDKn8rXX0Crz0T
|
||||||
STRIPE_CREDITS_100_PRICE=price_1T1OkNAZjQCYS0ZJP6XQ33F7
|
STRIPE_POTION_SMALL_PRICE=price_1TEu8UAZjQCYS0ZJUGnsu9SH
|
||||||
STRIPE_CREDITS_500_PRODUCT_ID=prod_TzNUzzub5HM70Q
|
STRIPE_POTION_MEDIUM_PRODUCT_ID=prod_UDKnANMuSvWMIE
|
||||||
STRIPE_CREDITS_500_PRICE=price_1T1OkNAZjQCYS0ZJGH9TKmqa
|
STRIPE_POTION_MEDIUM_PRICE=price_1TEu8UAZjQCYS0ZJQr2FbDm0
|
||||||
STRIPE_CREDITS_1000_PRODUCT_ID=prod_TzNUvB4LT4PCCe
|
STRIPE_POTION_LARGE_PRODUCT_ID=prod_UDKnTxFN6xD0ID
|
||||||
STRIPE_CREDITS_1000_PRICE=price_1T1OkNAZjQCYS0ZJvc6HTfB5
|
STRIPE_POTION_LARGE_PRICE=price_1TEu8VAZjQCYS0ZJDX6i2jwv
|
||||||
|
STRIPE_POTION_HUGE_PRODUCT_ID=prod_UDKncb3tyAlGKy
|
||||||
|
STRIPE_POTION_HUGE_PRICE=price_1TEu8VAZjQCYS0ZJ7AO86Jrt
|
||||||
|
|
||||||
# Customer Portal Configuration
|
# Customer Portal Configuration
|
||||||
STRIPE_PORTAL_CONFIG_ID=bpc_1T1PFdAZjQCYS0ZJEhF9ob7q
|
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_APP_ID=picture-app
|
||||||
PICTURE_MANA_CORE_SERVICE_KEY=
|
PICTURE_MANA_CORE_SERVICE_KEY=
|
||||||
|
|
||||||
# OAuth (optional - leave empty to disable)
|
|
||||||
PICTURE_GOOGLE_CLIENT_ID=
|
|
||||||
PICTURE_APPLE_CLIENT_ID=
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# NUTRIPHI PROJECT
|
# NUTRIPHI PROJECT
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect="/dashboard"
|
successRedirect="/dashboard"
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
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.",
|
"NSSpeechRecognitionUsageDescription": "Diese App verwendet Spracherkennung, um Sprachaufnahmen in Text umzuwandeln.",
|
||||||
"NSDocumentsFolderUsageDescription": "Diese App benötigt Zugriff auf Dokumente, um Lernmaterialien zu importieren."
|
"NSDocumentsFolderUsageDescription": "Diese App benötigt Zugriff auf Dokumente, um Lernmaterialien zu importieren."
|
||||||
},
|
},
|
||||||
"appleTeamId": "QP3GLU8PH3",
|
"appleTeamId": "QP3GLU8PH3"
|
||||||
"config": {
|
|
||||||
"googleSignIn": {
|
|
||||||
"reservedClientId": "com.googleusercontent.apps.111768794939-cgen6eklloo7k8vppcaq01o8r8nd7anb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"usesAppleSignIn": true
|
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
|
|
@ -63,10 +57,6 @@
|
||||||
"projectId": "6cb9cf81-a4d5-4c72-b57d-1be3da8eba35"
|
"projectId": "6cb9cf81-a4d5-4c72-b57d-1be3da8eba35"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": ["expo-router"]
|
||||||
"expo-router",
|
|
||||||
"@react-native-google-signin/google-signin",
|
|
||||||
"expo-apple-authentication"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import { Button } from '../../components/ui/Button';
|
||||||
import { Input } from '../../components/ui/Input';
|
import { Input } from '../../components/ui/Input';
|
||||||
import { Card } from '../../components/ui/Card';
|
import { Card } from '../../components/ui/Card';
|
||||||
import { useThemeColors } from '~/utils/themeUtils';
|
import { useThemeColors } from '~/utils/themeUtils';
|
||||||
import { GoogleSignInButton } from '../../components/auth/GoogleSignInButton';
|
|
||||||
import { AppleSignInButton } from '../../components/auth/AppleSignInButton';
|
|
||||||
import { spacing } from '~/utils/spacing';
|
import { spacing } from '~/utils/spacing';
|
||||||
|
|
||||||
export default function LoginScreen() {
|
export default function LoginScreen() {
|
||||||
|
|
@ -18,8 +16,7 @@ export default function LoginScreen() {
|
||||||
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
|
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
|
||||||
const colors = useThemeColors();
|
const colors = useThemeColors();
|
||||||
|
|
||||||
const { signIn, signInWithGoogle, signInWithApple, isLoading, error, clearError } =
|
const { signIn, isLoading, error, clearError } = useAuthStore();
|
||||||
useAuthStore();
|
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: { email?: string; password?: string } = {};
|
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 (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }} edges={['top']}>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
|
|
@ -140,37 +115,6 @@ export default function LoginScreen() {
|
||||||
Anmelden
|
Anmelden
|
||||||
</Button>
|
</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
|
<View
|
||||||
style={{
|
style={{
|
||||||
marginTop: spacing.xl,
|
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",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"@react-native-community/netinfo": "^11.4.1",
|
"@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-native-segmented-control/segmented-control": "2.5.7",
|
||||||
"@react-navigation/native": "^7.0.3",
|
"@react-navigation/native": "^7.0.3",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"expo": "54.0.13",
|
"expo": "54.0.13",
|
||||||
"expo-apple-authentication": "~8.0.7",
|
|
||||||
"expo-blur": "~15.0.7",
|
"expo-blur": "~15.0.7",
|
||||||
"expo-build-properties": "~1.0.9",
|
"expo-build-properties": "~1.0.9",
|
||||||
"expo-constants": "~18.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,
|
onTokenRefresh: null as ((userData: { id: string; email: string; role: string }) => void) | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ interface AuthState {
|
||||||
initialize: () => Promise<void>;
|
initialize: () => Promise<void>;
|
||||||
signIn: (email: string, password: string) => Promise<void>;
|
signIn: (email: string, password: string) => Promise<void>;
|
||||||
signUp: (email: string, password: string, username: 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>;
|
signOut: () => Promise<void>;
|
||||||
resetPassword: (email: string) => Promise<void>;
|
resetPassword: (email: string) => Promise<void>;
|
||||||
updateProfile: (updates: {
|
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 () => {
|
signOut: async () => {
|
||||||
try {
|
try {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,6 @@ export const authService = createAuthService({
|
||||||
refresh: '/api/v1/auth/refresh',
|
refresh: '/api/v1/auth/refresh',
|
||||||
validate: '/api/v1/auth/validate',
|
validate: '/api/v1/auth/validate',
|
||||||
forgotPassword: '/api/v1/auth/forgot-password',
|
forgotPassword: '/api/v1/auth/forgot-password',
|
||||||
googleSignIn: '/api/v1/auth/google-signin',
|
|
||||||
appleSignIn: '/api/v1/auth/apple-signin',
|
|
||||||
credits: '/api/v1/credits/balance',
|
credits: '/api/v1/credits/balance',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect="/decks"
|
successRedirect="/decks"
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,17 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { locale } from 'svelte-i18n';
|
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 { getLoginTranslations } from '@manacore/shared-i18n';
|
||||||
import PictureLogo from '$lib/components/branding/PictureLogo.svelte';
|
import PictureLogo from '$lib/components/branding/PictureLogo.svelte';
|
||||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import { APP_VERSION, BUILD_TIME } from '$lib/version';
|
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
|
// Get translations based on current locale
|
||||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (PUBLIC_GOOGLE_CLIENT_ID) {
|
|
||||||
setGoogleClientId(PUBLIC_GOOGLE_CLIENT_ID);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleSignIn(email: string, password: string) {
|
async function handleSignIn(email: string, password: string) {
|
||||||
return authStore.signIn(email, password);
|
return authStore.signIn(email, password);
|
||||||
}
|
}
|
||||||
|
|
@ -29,16 +21,6 @@
|
||||||
return authStore.resendVerificationEmail(email);
|
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)
|
// Read verification status from query params (set after email verification)
|
||||||
const verified = $derived($page.url.searchParams.get('verified') === 'true');
|
const verified = $derived($page.url.searchParams.get('verified') === 'true');
|
||||||
const initialEmail = $derived($page.url.searchParams.get('email') || '');
|
const initialEmail = $derived($page.url.searchParams.get('email') || '');
|
||||||
|
|
@ -54,11 +36,7 @@
|
||||||
primaryColor="#3b82f6"
|
primaryColor="#3b82f6"
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
onSignInWithGoogle={PUBLIC_GOOGLE_CLIENT_ID ? handleSignInWithGoogle : undefined}
|
|
||||||
onSignInWithApple={PUBLIC_APPLE_CLIENT_ID ? handleSignInWithApple : undefined}
|
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={!!PUBLIC_GOOGLE_CLIENT_ID}
|
|
||||||
enableApple={!!PUBLIC_APPLE_CLIENT_ID}
|
|
||||||
successRedirect="/app/gallery"
|
successRedirect="/app/gallery"
|
||||||
registerPath="/auth/signup"
|
registerPath="/auth/signup"
|
||||||
forgotPasswordPath="/auth/forgot-password"
|
forgotPasswordPath="/auth/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
forgotPasswordPath="/forgot-password"
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,6 @@
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onResendVerification={handleResendVerification}
|
onResendVerification={handleResendVerification}
|
||||||
{goto}
|
{goto}
|
||||||
enableGoogle={false}
|
|
||||||
enableApple={false}
|
|
||||||
successRedirect={redirectTo}
|
successRedirect={redirectTo}
|
||||||
registerPath="/register"
|
registerPath="/register"
|
||||||
forgotPasswordPath="/forgot-password"
|
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';
|
export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.svelte';
|
||||||
|
|
||||||
// Components
|
// 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 GuestWelcomeModal } from './components/GuestWelcomeModal.svelte';
|
||||||
export { default as AuthGateModal } from './components/AuthGateModal.svelte';
|
export { default as AuthGateModal } from './components/AuthGateModal.svelte';
|
||||||
export { default as SessionExpiredBanner } from './components/SessionExpiredBanner.svelte';
|
export { default as SessionExpiredBanner } from './components/SessionExpiredBanner.svelte';
|
||||||
export { default as AuthGate } from './components/AuthGate.svelte';
|
export { default as AuthGate } from './components/AuthGate.svelte';
|
||||||
|
|
||||||
// Utilities
|
// 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 {
|
export {
|
||||||
shouldShowGuestWelcome,
|
shouldShowGuestWelcome,
|
||||||
markGuestWelcomeSeen,
|
markGuestWelcomeSeen,
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
import type { Component, Snippet } from 'svelte';
|
import type { Component, Snippet } from 'svelte';
|
||||||
import type { AuthResult } from '../types';
|
import type { AuthResult } from '../types';
|
||||||
import { Check, Warning, Eye, EyeSlash, SignIn, Sun, Moon } from '@manacore/shared-icons';
|
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 */
|
/** Translation strings for the login page */
|
||||||
export interface LoginTranslations {
|
export interface LoginTranslations {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -26,9 +23,7 @@
|
||||||
emailInvalid: string;
|
emailInvalid: string;
|
||||||
passwordRequired: string;
|
passwordRequired: string;
|
||||||
signInFailed: string;
|
signInFailed: string;
|
||||||
googleSignInFailed: string;
|
|
||||||
signInSuccess: string;
|
signInSuccess: string;
|
||||||
googleSignInSuccess: string;
|
|
||||||
emailVerified?: string;
|
emailVerified?: string;
|
||||||
emailNotVerified?: string;
|
emailNotVerified?: string;
|
||||||
resendVerification?: string;
|
resendVerification?: string;
|
||||||
|
|
@ -56,9 +51,7 @@
|
||||||
emailInvalid: 'Please enter a valid email address',
|
emailInvalid: 'Please enter a valid email address',
|
||||||
passwordRequired: 'Password is required',
|
passwordRequired: 'Password is required',
|
||||||
signInFailed: 'Sign in failed',
|
signInFailed: 'Sign in failed',
|
||||||
googleSignInFailed: 'Google sign in failed',
|
|
||||||
signInSuccess: 'Successfully signed in. Redirecting...',
|
signInSuccess: 'Successfully signed in. Redirecting...',
|
||||||
googleSignInSuccess: 'Successfully signed in with Google. Redirecting...',
|
|
||||||
emailVerified: 'Email successfully verified! Please sign in.',
|
emailVerified: 'Email successfully verified! Please sign in.',
|
||||||
emailNotVerified: 'Email not verified.',
|
emailNotVerified: 'Email not verified.',
|
||||||
resendVerification: 'Resend verification email',
|
resendVerification: 'Resend verification email',
|
||||||
|
|
@ -71,12 +64,8 @@
|
||||||
logo: Component<{ size?: number; color?: string }>;
|
logo: Component<{ size?: number; color?: string }>;
|
||||||
primaryColor: string;
|
primaryColor: string;
|
||||||
onSignIn: (email: string, password: string) => Promise<AuthResult>;
|
onSignIn: (email: string, password: string) => Promise<AuthResult>;
|
||||||
onSignInWithGoogle?: (idToken: string) => Promise<AuthResult>;
|
|
||||||
onSignInWithApple?: (identityToken: string) => Promise<AuthResult>;
|
|
||||||
onResendVerification?: (email: string) => Promise<AuthResult>;
|
onResendVerification?: (email: string) => Promise<AuthResult>;
|
||||||
goto: (path: string) => void;
|
goto: (path: string) => void;
|
||||||
enableGoogle?: boolean;
|
|
||||||
enableApple?: boolean;
|
|
||||||
successRedirect?: string;
|
successRedirect?: string;
|
||||||
registerPath?: string;
|
registerPath?: string;
|
||||||
forgotPasswordPath?: string;
|
forgotPasswordPath?: string;
|
||||||
|
|
@ -102,12 +91,8 @@
|
||||||
logo: Logo,
|
logo: Logo,
|
||||||
primaryColor,
|
primaryColor,
|
||||||
onSignIn,
|
onSignIn,
|
||||||
onSignInWithGoogle,
|
|
||||||
onSignInWithApple,
|
|
||||||
onResendVerification,
|
onResendVerification,
|
||||||
goto,
|
goto,
|
||||||
enableGoogle = false,
|
|
||||||
enableApple = false,
|
|
||||||
successRedirect = '/dashboard',
|
successRedirect = '/dashboard',
|
||||||
registerPath = '/register',
|
registerPath = '/register',
|
||||||
forgotPasswordPath = '/forgot-password',
|
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() {
|
function skipToForm() {
|
||||||
if (emailInput) emailInput.focus();
|
if (emailInput) emailInput.focus();
|
||||||
}
|
}
|
||||||
|
|
@ -519,20 +487,6 @@
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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">
|
<p class="register-link">
|
||||||
{t.noAccount}
|
{t.noAccount}
|
||||||
<button type="button" onclick={() => goto(registerPath)} style:color={primaryColor}>
|
<button type="button" onclick={() => goto(registerPath)} style:color={primaryColor}>
|
||||||
|
|
@ -973,38 +927,6 @@
|
||||||
cursor: not-allowed;
|
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 {
|
.register-link {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|
|
||||||
|
|
@ -24,21 +24,6 @@ export interface AuthUIConfig {
|
||||||
|
|
||||||
/** Redirect path after successful login (default: '/dashboard') */
|
/** Redirect path after successful login (default: '/dashboard') */
|
||||||
successRedirect?: string;
|
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 {
|
export interface AuthServiceInterface {
|
||||||
signIn(email: string, password: string): Promise<AuthResult>;
|
signIn(email: string, password: string): Promise<AuthResult>;
|
||||||
signUp(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>;
|
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',
|
forgotPassword: '/api/v1/auth/forgot-password',
|
||||||
resetPassword: '/api/v1/auth/reset-password',
|
resetPassword: '/api/v1/auth/reset-password',
|
||||||
resendVerification: '/api/v1/auth/resend-verification',
|
resendVerification: '/api/v1/auth/resend-verification',
|
||||||
googleSignIn: '/api/v1/auth/google-signin',
|
|
||||||
appleSignIn: '/api/v1/auth/apple-signin',
|
|
||||||
credits: '/api/v1/credits/balance',
|
credits: '/api/v1/credits/balance',
|
||||||
// Better Auth native endpoints for SSO
|
// Better Auth native endpoints for SSO
|
||||||
getSession: '/api/auth/get-session',
|
getSession: '/api/auth/get-session',
|
||||||
|
|
@ -360,76 +358,6 @@ export function createAuthService(config: AuthServiceConfig) {
|
||||||
return { appToken, refreshToken, userData };
|
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
|
* Get the current app token
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,6 @@ export interface AuthEndpoints {
|
||||||
forgotPassword: string;
|
forgotPassword: string;
|
||||||
resetPassword: string;
|
resetPassword: string;
|
||||||
resendVerification: string;
|
resendVerification: string;
|
||||||
googleSignIn: string;
|
|
||||||
appleSignIn: string;
|
|
||||||
credits: string;
|
credits: string;
|
||||||
/** Better Auth native endpoint for SSO session check */
|
/** Better Auth native endpoint for SSO session check */
|
||||||
getSession: string;
|
getSession: string;
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@
|
||||||
"emailInvalid": "Bitte gib eine gültige E-Mail-Adresse ein",
|
"emailInvalid": "Bitte gib eine gültige E-Mail-Adresse ein",
|
||||||
"passwordRequired": "Passwort ist erforderlich",
|
"passwordRequired": "Passwort ist erforderlich",
|
||||||
"signInFailed": "Anmeldung fehlgeschlagen",
|
"signInFailed": "Anmeldung fehlgeschlagen",
|
||||||
"googleSignInFailed": "Google-Anmeldung fehlgeschlagen",
|
|
||||||
"signInSuccess": "Erfolgreich angemeldet. Weiterleitung...",
|
"signInSuccess": "Erfolgreich angemeldet. Weiterleitung...",
|
||||||
"googleSignInSuccess": "Erfolgreich mit Google angemeldet. Weiterleitung...",
|
|
||||||
"emailVerified": "E-Mail erfolgreich bestätigt! Bitte melde dich an.",
|
"emailVerified": "E-Mail erfolgreich bestätigt! Bitte melde dich an.",
|
||||||
"emailNotVerified": "E-Mail nicht bestätigt.",
|
"emailNotVerified": "E-Mail nicht bestätigt.",
|
||||||
"resendVerification": "Bestätigungs-E-Mail erneut senden",
|
"resendVerification": "Bestätigungs-E-Mail erneut senden",
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@
|
||||||
"emailInvalid": "Please enter a valid email address",
|
"emailInvalid": "Please enter a valid email address",
|
||||||
"passwordRequired": "Password is required",
|
"passwordRequired": "Password is required",
|
||||||
"signInFailed": "Sign in failed",
|
"signInFailed": "Sign in failed",
|
||||||
"googleSignInFailed": "Google sign in failed",
|
|
||||||
"signInSuccess": "Successfully signed in. Redirecting...",
|
"signInSuccess": "Successfully signed in. Redirecting...",
|
||||||
"googleSignInSuccess": "Successfully signed in with Google. Redirecting...",
|
|
||||||
"emailVerified": "Email successfully verified! Please sign in.",
|
"emailVerified": "Email successfully verified! Please sign in.",
|
||||||
"emailNotVerified": "Email not verified.",
|
"emailNotVerified": "Email not verified.",
|
||||||
"resendVerification": "Resend verification email",
|
"resendVerification": "Resend verification email",
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@
|
||||||
"emailInvalid": "Por favor ingresa un correo electrónico válido",
|
"emailInvalid": "Por favor ingresa un correo electrónico válido",
|
||||||
"passwordRequired": "La contraseña es obligatoria",
|
"passwordRequired": "La contraseña es obligatoria",
|
||||||
"signInFailed": "Error al iniciar sesión",
|
"signInFailed": "Error al iniciar sesión",
|
||||||
"googleSignInFailed": "Error al iniciar sesión con Google",
|
|
||||||
"signInSuccess": "Sesión iniciada correctamente. Redirigiendo...",
|
"signInSuccess": "Sesión iniciada correctamente. Redirigiendo...",
|
||||||
"googleSignInSuccess": "Sesión iniciada con Google correctamente. Redirigiendo...",
|
|
||||||
"emailVerified": "¡Correo verificado exitosamente! Por favor inicia sesión.",
|
"emailVerified": "¡Correo verificado exitosamente! Por favor inicia sesión.",
|
||||||
"emailNotVerified": "Correo no verificado.",
|
"emailNotVerified": "Correo no verificado.",
|
||||||
"resendVerification": "Reenviar correo de verificación",
|
"resendVerification": "Reenviar correo de verificación",
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@
|
||||||
"emailInvalid": "Veuillez entrer une adresse email valide",
|
"emailInvalid": "Veuillez entrer une adresse email valide",
|
||||||
"passwordRequired": "Le mot de passe est requis",
|
"passwordRequired": "Le mot de passe est requis",
|
||||||
"signInFailed": "Échec de la connexion",
|
"signInFailed": "Échec de la connexion",
|
||||||
"googleSignInFailed": "Échec de la connexion Google",
|
|
||||||
"signInSuccess": "Connexion réussie. Redirection...",
|
"signInSuccess": "Connexion réussie. Redirection...",
|
||||||
"googleSignInSuccess": "Connexion Google réussie. Redirection...",
|
|
||||||
"emailVerified": "Email vérifié avec succès ! Veuillez vous connecter.",
|
"emailVerified": "Email vérifié avec succès ! Veuillez vous connecter.",
|
||||||
"emailNotVerified": "Email non vérifié.",
|
"emailNotVerified": "Email non vérifié.",
|
||||||
"resendVerification": "Renvoyer l'email de vérification",
|
"resendVerification": "Renvoyer l'email de vérification",
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,7 @@ export interface AuthTranslations {
|
||||||
emailInvalid: string;
|
emailInvalid: string;
|
||||||
passwordRequired: string;
|
passwordRequired: string;
|
||||||
signInFailed: string;
|
signInFailed: string;
|
||||||
googleSignInFailed: string;
|
|
||||||
signInSuccess: string;
|
signInSuccess: string;
|
||||||
googleSignInSuccess: string;
|
|
||||||
emailVerified?: string;
|
emailVerified?: string;
|
||||||
emailNotVerified?: string;
|
emailNotVerified?: string;
|
||||||
resendVerification?: string;
|
resendVerification?: string;
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@
|
||||||
"emailInvalid": "Inserisci un indirizzo email valido",
|
"emailInvalid": "Inserisci un indirizzo email valido",
|
||||||
"passwordRequired": "La password è obbligatoria",
|
"passwordRequired": "La password è obbligatoria",
|
||||||
"signInFailed": "Accesso fallito",
|
"signInFailed": "Accesso fallito",
|
||||||
"googleSignInFailed": "Accesso con Google fallito",
|
|
||||||
"signInSuccess": "Accesso effettuato. Reindirizzamento...",
|
"signInSuccess": "Accesso effettuato. Reindirizzamento...",
|
||||||
"googleSignInSuccess": "Accesso con Google effettuato. Reindirizzamento...",
|
|
||||||
"emailVerified": "Email verificata con successo! Effettua l'accesso.",
|
"emailVerified": "Email verificata con successo! Effettua l'accesso.",
|
||||||
"emailNotVerified": "Email non verificata.",
|
"emailNotVerified": "Email non verificata.",
|
||||||
"resendVerification": "Invia di nuovo l'email di verifica",
|
"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: {
|
vars: {
|
||||||
PUBLIC_BACKEND_URL: (env) => env.PICTURE_BACKEND_URL || 'http://localhost:3003',
|
PUBLIC_BACKEND_URL: (env) => env.PICTURE_BACKEND_URL || 'http://localhost:3003',
|
||||||
PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
|
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_UMAMI_WEBSITE_ID: (env) => env.UMAMI_WEBSITE_ID_PICTURE || '',
|
||||||
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
|
PUBLIC_GLITCHTIP_DSN: (env) => env.PUBLIC_GLITCHTIP_DSN || '',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue