summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/auth.tsx25
-rw-r--r--lib/google-auth.ts116
-rw-r--r--lib/locales.ts63
3 files changed, 198 insertions, 6 deletions
diff --git a/lib/auth.tsx b/lib/auth.tsx
index b05c6a7..ac394f4 100644
--- a/lib/auth.tsx
+++ b/lib/auth.tsx
@@ -1,9 +1,9 @@
import {
- createContext,
- ReactNode,
- useContext,
- useEffect,
- useState,
+ createContext,
+ ReactNode,
+ useContext,
+ useEffect,
+ useState,
} from "react";
import { apiClient } from "../api/client";
@@ -17,6 +17,9 @@ type AuthContextType = AuthState & {
email: string,
password: string,
) => Promise<{ success: boolean; reason?: string }>;
+ signInWithGoogle: (
+ idToken: string,
+ ) => Promise<{ success: boolean; reason?: string }>;
signUp: (
email: string,
password: string,
@@ -60,13 +63,23 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return { success: result.success, reason: result.reason };
};
+ const signInWithGoogle = async (idToken: string) => {
+ const result = await apiClient.signInWithGoogle(idToken);
+ if (result.success) {
+ setState({ isLoading: false, isAuthenticated: true });
+ }
+ return { success: result.success, reason: result.reason };
+ };
+
const signOut = async () => {
await apiClient.signOut();
setState({ isLoading: false, isAuthenticated: false });
};
return (
- <AuthContext.Provider value={{ ...state, signIn, signUp, signOut }}>
+ <AuthContext.Provider
+ value={{ ...state, signIn, signInWithGoogle, signUp, signOut }}
+ >
{children}
</AuthContext.Provider>
);
diff --git a/lib/google-auth.ts b/lib/google-auth.ts
new file mode 100644
index 0000000..769da55
--- /dev/null
+++ b/lib/google-auth.ts
@@ -0,0 +1,116 @@
+import { ResponseType } from "expo-auth-session";
+import * as Google from "expo-auth-session/providers/google";
+import Constants from "expo-constants";
+import * as WebBrowser from "expo-web-browser";
+import { useEffect, useMemo, useState } from "react";
+import { useAuth } from "./auth";
+import { t } from "./locales";
+
+WebBrowser.maybeCompleteAuthSession();
+
+type GoogleClientIds = {
+ iosClientId?: string;
+ androidClientId?: string;
+ webClientId?: string;
+};
+
+function getGoogleClientIds(): GoogleClientIds {
+ const env = (process as any)?.env || {};
+ const extra =
+ (Constants.manifest as any)?.extra ||
+ (Constants.expoConfig as any)?.extra ||
+ {};
+
+ return {
+ iosClientId:
+ env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID || extra.GOOGLE_IOS_CLIENT_ID,
+ androidClientId:
+ env.EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID ||
+ extra.GOOGLE_ANDROID_CLIENT_ID,
+ webClientId:
+ env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID ||
+ extra.GOOGLE_WEB_CLIENT_ID ||
+ env.EXPO_PUBLIC_GOOGLE_EXPO_CLIENT_ID ||
+ extra.GOOGLE_EXPO_CLIENT_ID,
+ };
+}
+
+export function useGoogleAuth() {
+ const { signInWithGoogle } = useAuth();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const clientIds = useMemo(() => getGoogleClientIds(), []);
+
+ const [request, response, promptAsync] = Google.useAuthRequest({
+ iosClientId: clientIds.iosClientId,
+ androidClientId: clientIds.androidClientId,
+ webClientId: clientIds.webClientId,
+ responseType: ResponseType.IdToken,
+ scopes: ["openid", "profile", "email"],
+ selectAccount: true,
+ });
+
+ useEffect(() => {
+ const run = async () => {
+ if (!response) return;
+
+ if (response.type === "success") {
+ const idToken =
+ response.params?.id_token || response.authentication?.idToken;
+
+ if (!idToken) {
+ setError(t("googleMissingIdToken"));
+ setIsLoading(false);
+ return;
+ }
+
+ const result = await signInWithGoogle(idToken);
+ if (!result.success) {
+ setError(result.reason || t("googleSignInError"));
+ }
+ setIsLoading(false);
+ return;
+ }
+
+ if (response.type === "error") {
+ setError(response.error?.message || t("googleSignInError"));
+ }
+
+ // On cancel/dismissed/locked, just reset loading and keep the current screen state.
+ setIsLoading(false);
+ };
+
+ run().catch(() => {
+ setError(t("googleSignInError"));
+ setIsLoading(false);
+ });
+ }, [response, signInWithGoogle]);
+
+ const continueWithGoogle = async () => {
+ setError("");
+
+ if (!request) {
+ setError(t("googleConfigMissing"));
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ const result = await promptAsync();
+ if (result.type === "cancel" || result.type === "dismiss") {
+ setIsLoading(false);
+ }
+ } catch {
+ setError(t("googleSignInError"));
+ setIsLoading(false);
+ }
+ };
+
+ return {
+ continueWithGoogle,
+ isLoading,
+ error,
+ };
+}
diff --git a/lib/locales.ts b/lib/locales.ts
index d78621f..9ba29ba 100644
--- a/lib/locales.ts
+++ b/lib/locales.ts
@@ -36,12 +36,36 @@ const en = {
createAccount: "Create Account",
alreadyHaveAccount: "Already have an account?",
dontHaveAccount: "Don't have an account?",
+ forgotPassword: "Forgot password?",
+ resetPassword: "Reset Password",
+ resetPasswordHelp: "Request a reset link or enter a token to set a new password.",
+ sendResetLink: "Send reset link",
+ resetPasswordToken: "Reset token",
+ resetPasswordTokenPlaceholder: "Paste your reset token",
+ newPassword: "New Password",
+ newPasswordPlaceholder: "Enter your new password",
+ confirmNewPassword: "Confirm New Password",
+ switchToResetWithToken: "I have a reset token",
+ switchToRequestReset: "I need a reset link",
+ passwordResetEmailSent:
+ "If that email exists, a reset link has been sent.",
+ passwordResetSuccess: "Your password has been reset. You can sign in now.",
+ resetTokenRequired: "Reset token is required",
+ resetPasswordError: "Unable to reset password. Please try again.",
+ backToSignIn: "Back to Sign In",
invalidEmail: "Please enter a valid email",
passwordRequired: "Password is required",
passwordTooShort: "Password must be at least 8 characters",
passwordsDoNotMatch: "Passwords do not match",
signInError: "Unable to sign in. Please check your credentials.",
signUpError: "Unable to create account. Please try again.",
+ continueWithGoogle: "Continue with Google",
+ googleAuthUnifiedHint:
+ "Google will sign you in or create your Buddy account automatically.",
+ googleSignInError: "Google sign in failed. Please try again.",
+ googleMissingIdToken:
+ "Google sign in did not return an ID token. Please try again.",
+ googleConfigMissing: "Google sign in is not configured yet. Contact support.",
loadingAlerts: "Loading alerts...",
allClearTitle: "All clear!",
noAlertsToReview: "No alerts to review right now.",
@@ -74,6 +98,12 @@ const en = {
notifications: "Notifications",
dangerousMessages: "Dangerous messages",
newContactAdded: "New contact added",
+ galleryScanning: "Gallery scanning",
+ scanGalleryForInappropriateImages: "Scan gallery for inappropriate images.",
+ galleryScanningTitle: "Gallery scanning",
+ galleryScanningNone: "Do nothing",
+ galleryScanningNotify: "Notify me",
+ galleryScanningDelete: "Delete detected images",
devices: "Devices",
device: "device",
devicesPlural: "devices",
@@ -150,12 +180,39 @@ const hr: typeof en = {
createAccount: "Izradi račun",
alreadyHaveAccount: "Već imate račun?",
dontHaveAccount: "Nemate račun?",
+ forgotPassword: "Zaboravljena lozinka?",
+ resetPassword: "Resetiraj lozinku",
+ resetPasswordHelp:
+ "Zatražite poveznicu za resetiranje ili unesite token za novu lozinku.",
+ sendResetLink: "Pošalji poveznicu za resetiranje",
+ resetPasswordToken: "Token za resetiranje",
+ resetPasswordTokenPlaceholder: "Zalijepite token za resetiranje",
+ newPassword: "Nova lozinka",
+ newPasswordPlaceholder: "Unesite novu lozinku",
+ confirmNewPassword: "Potvrdite novu lozinku",
+ switchToResetWithToken: "Imam token za resetiranje",
+ switchToRequestReset: "Trebam poveznicu za resetiranje",
+ passwordResetEmailSent:
+ "Ako taj e-mail postoji, poveznica za resetiranje je poslana.",
+ passwordResetSuccess:
+ "Lozinka je uspješno resetirana. Sada se možete prijaviti.",
+ resetTokenRequired: "Token za resetiranje je obavezan",
+ resetPasswordError: "Resetiranje lozinke nije uspjelo. Pokušajte ponovno.",
+ backToSignIn: "Natrag na prijavu",
invalidEmail: "Unesite valjanu e-mail adresu",
passwordRequired: "Lozinka je obavezna",
passwordTooShort: "Lozinka mora imati najmanje 8 znakova",
passwordsDoNotMatch: "Lozinke se ne podudaraju",
signInError: "Prijava nije uspjela. Provjerite podatke.",
signUpError: "Registracija nije uspjela. Pokušajte ponovno.",
+ continueWithGoogle: "Nastavi s Google računom",
+ googleAuthUnifiedHint:
+ "Google će vas prijaviti ili automatski izraditi Buddy račun.",
+ googleSignInError: "Google prijava nije uspjela. Pokušajte ponovno.",
+ googleMissingIdToken:
+ "Google prijava nije vratila ID token. Pokušajte ponovno.",
+ googleConfigMissing:
+ "Google prijava još nije konfigurirana. Kontaktirajte podršku.",
loadingAlerts: "Učitavanje upozorenja...",
allClearTitle: "Sve je u redu!",
noAlertsToReview: "Trenutno nema upozorenja za pregled.",
@@ -188,6 +245,12 @@ const hr: typeof en = {
notifications: "Obavijesti",
dangerousMessages: "Opasne poruke",
newContactAdded: "Dodan novi kontakt",
+ galleryScanning: "Skeniranje galerije",
+ scanGalleryForInappropriateImages: "Skeniraj galeriju za neprimjerene slike.",
+ galleryScanningTitle: "Skeniranje galerije",
+ galleryScanningNone: "Ne poduzimaj ništa",
+ galleryScanningNotify: "Obavijesti me",
+ galleryScanningDelete: "Izbriši otkrivene slike",
devices: "Uređaji",
device: "uređaj",
devicesPlural: "uređaji",