diff --git a/.vscode/settings.json b/.vscode/settings.json index e2c76588..613645d9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,13 @@ { - "cSpell.words": ["Binar", "diawi", "icomoon", "mobileprovision", "notificated", "postsecondary", "Touchables"], + "cSpell.words": [ + "Binar", + "diawi", + "icomoon", + "mobileprovision", + "notificated", + "postsecondary", + "Touchables" + ], "todohighlight.keywords": [ { "text": "CONFIG:", diff --git a/assets/social/apple-icon-dark.png b/assets/social/apple-icon-dark.png new file mode 100644 index 00000000..213c5404 Binary files /dev/null and b/assets/social/apple-icon-dark.png differ diff --git a/assets/social/apple-icon.png b/assets/social/apple-icon.png new file mode 100644 index 00000000..34a7babe Binary files /dev/null and b/assets/social/apple-icon.png differ diff --git a/assets/social/facebook-icon.png b/assets/social/facebook-icon.png new file mode 100644 index 00000000..cc110986 Binary files /dev/null and b/assets/social/facebook-icon.png differ diff --git a/assets/social/google-icon.png b/assets/social/google-icon.png new file mode 100644 index 00000000..a524ee8f Binary files /dev/null and b/assets/social/google-icon.png differ diff --git a/scripts/data/swagger-spec.json b/scripts/data/swagger-spec.json index 9f9f29e9..983cae3d 100644 --- a/scripts/data/swagger-spec.json +++ b/scripts/data/swagger-spec.json @@ -416,7 +416,7 @@ } }, "401": { - "description": "Unauthorized - Invalid credentials", + "description": "Unauthorized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorUnauthorizedEntity" } @@ -1287,7 +1287,7 @@ "ErrorValidationEntity": { "type": "object", "properties": { - "status": { + "statusCode": { "type": "number", "description": "HTTP status code indicating the error", "example": 422 @@ -1299,7 +1299,7 @@ "additionalProperties": { "type": "string" } } }, - "required": ["status", "errors"] + "required": ["statusCode", "errors"] }, "UpdateArticleDto": { "type": "object", @@ -1434,7 +1434,7 @@ }, "required": ["id", "name"] }, - "LastConsentProperties": { + "LastConsentEntity": { "type": "object", "properties": { "termsAccepted": { @@ -1508,17 +1508,7 @@ "example": { "id": 1, "name": "ACTIVE" }, "allOf": [{ "$ref": "#/components/schemas/Status" }] }, - "consent": { - "example": { - "createdAt": "2024-03-07T23:29:27.697Z", - "privacyPolicyAccepted": true, - "privacyPolicyVersion": "1.0", - "termsAccepted": true, - "termsVersion": "1.0", - "updatedAt": "2024-03-07T23:29:27.697Z" - }, - "allOf": [{ "$ref": "#/components/schemas/LastConsentProperties" }] - } + "consent": { "$ref": "#/components/schemas/LastConsentEntity" } }, "required": [ "id", @@ -1779,7 +1769,7 @@ "description": "The refresh token for refreshing the access token." }, "tokenExpires": { - "type": "string", + "type": "number", "example": 1708531622031, "description": "The expiry date of the access token." } @@ -1837,13 +1827,13 @@ "AuthGoogleLoginDto": { "type": "object", "properties": { - "idToken": { + "accessToken": { "type": "string", "example": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOGI2ZTFlYTI1Y2I2M2Q0ZTI5YWI1Y2M2ZDZmODBlZjRmNDY2NjciLCJ0eXAiOiJKV1QifQ.eyJhenAiOiIxMjM0NTY3ODkwMTIzNDU2Nzg5MC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImF1ZCI6IjEyMzQ1Njc4OTAxMjM0NTY3ODkwLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiZXhwIjoxNjUxNTc2MDAwLCJpYXQiOjE2NTE1NzI0MDAsImlzcyI6ImFjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMjM0NTY3ODkwMTIzNDU2Nzg5MCIsImVtYWlsIjoianVzdHVzZXJAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJKdXN0IFVzZXIiLCJwaWN0dXJlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9qdXN0dXNlci9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiSnVzdCIsImZhbWlseV9uYW1lIjoiVXNlciJ9.QWxsYWtoemF0aGtqZGxza2FqaGRsa2FqZGxza2FqZGhza2FqaGRrc2FqaGtqZHNhbGtqZHNhbGtqZGhsYWtqZHNhbGtqaGRsYWtqaGRza2FqaGRrc2FqaGRrc2FqaGRrc2Fq", - "description": "Google ID token obtained after user authentication using Google OAuth. Use this token to authenticate the request to the application." + "description": "Google Access token obtained after user authentication using Google OAuth. Use this token to authenticate the request to the application." } }, - "required": ["idToken"] + "required": ["accessToken"] }, "AuthFacebookLoginDto": { "type": "object", diff --git a/src/api/query/auth/auth.msw.ts b/src/api/query/auth/auth.msw.ts index 398ac953..28ce03d3 100644 --- a/src/api/query/auth/auth.msw.ts +++ b/src/api/query/auth/auth.msw.ts @@ -130,7 +130,7 @@ export const getAuthControllerRefreshResponseMock = ( ): RefreshEntity => ({ accessToken: faker.word.sample(), refreshToken: faker.word.sample(), - tokenExpires: faker.word.sample(), + tokenExpires: faker.number.int({ min: undefined, max: undefined }), ...overrideResponse, }) diff --git a/src/api/types/authGoogleLoginDto.ts b/src/api/types/authGoogleLoginDto.ts index f0b31967..67b9f6da 100644 --- a/src/api/types/authGoogleLoginDto.ts +++ b/src/api/types/authGoogleLoginDto.ts @@ -8,6 +8,6 @@ */ export interface AuthGoogleLoginDto { - /** Google ID token obtained after user authentication using Google OAuth. Use this token to authenticate the request to the application. */ - idToken: string + /** Google Access token obtained after user authentication using Google OAuth. Use this token to authenticate the request to the application. */ + accessToken: string } diff --git a/src/api/types/errorValidationEntity.ts b/src/api/types/errorValidationEntity.ts index 38e77c14..5abc5437 100644 --- a/src/api/types/errorValidationEntity.ts +++ b/src/api/types/errorValidationEntity.ts @@ -12,5 +12,5 @@ export interface ErrorValidationEntity { /** Object containing field-specific validation errors */ errors: ErrorValidationEntityErrors /** HTTP status code indicating the error */ - status: number + statusCode: number } diff --git a/src/api/types/index.ts b/src/api/types/index.ts index cf69b96d..0915bd06 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -38,7 +38,7 @@ export * from './healthCheckInfoDto' export * from './healthCheckStatusDto' export * from './healthEntity' export * from './healthEntityError' -export * from './lastConsentProperties' +export * from './lastConsentEntity' export * from './refreshEntity' export * from './role' export * from './roleDto' diff --git a/src/api/types/lastConsentProperties.ts b/src/api/types/lastConsentEntity.ts similarity index 95% rename from src/api/types/lastConsentProperties.ts rename to src/api/types/lastConsentEntity.ts index 25675c10..a26caaf8 100644 --- a/src/api/types/lastConsentProperties.ts +++ b/src/api/types/lastConsentEntity.ts @@ -7,7 +7,7 @@ * OpenAPI spec version: 1.0 */ -export interface LastConsentProperties { +export interface LastConsentEntity { /** The date and time when the consents were last created or the user agreed to the terms for the first time. */ createdAt: string /** Whether the privacy policy was accepted. */ diff --git a/src/api/types/refreshEntity.ts b/src/api/types/refreshEntity.ts index a18e4a16..bddcd5dc 100644 --- a/src/api/types/refreshEntity.ts +++ b/src/api/types/refreshEntity.ts @@ -13,5 +13,5 @@ export interface RefreshEntity { /** The refresh token for refreshing the access token. */ refreshToken: string /** The expiry date of the access token. */ - tokenExpires: string + tokenExpires: number } diff --git a/src/api/types/userEntity.ts b/src/api/types/userEntity.ts index 3c3b90cf..218e4f80 100644 --- a/src/api/types/userEntity.ts +++ b/src/api/types/userEntity.ts @@ -6,12 +6,12 @@ * API documentation for the starter-kit project in NestJS by BinarApps. The API allows management of users, sessions and offers various functions for logged in users. Contains examples of authentication, authorization, and CRUD for selected resources. * OpenAPI spec version: 1.0 */ -import type { LastConsentProperties } from './lastConsentProperties' +import type { LastConsentEntity } from './lastConsentEntity' import type { Role } from './role' import type { Status } from './status' export interface UserEntity { - consent?: LastConsentProperties + consent?: LastConsentEntity createdAt: string deletedAt: string email: string diff --git a/src/components/molecules/SocialButtons/GoogleButton/NativeGoogleButton.tsx b/src/components/molecules/SocialButtons/GoogleButton/NativeGoogleButton.tsx new file mode 100644 index 00000000..cf582110 --- /dev/null +++ b/src/components/molecules/SocialButtons/GoogleButton/NativeGoogleButton.tsx @@ -0,0 +1,99 @@ +import { useAuthGoogleControllerLogin } from '@baca/api/query/auth-social/auth-social' +import { ENV, isExpoGo, isWeb } from '@baca/constants' +import { useCallback, useEffect, useState } from '@baca/hooks' +import { assignPushToken, setToken } from '@baca/services' +import { isSignedInAtom, store } from '@baca/store' +import { showErrorToast } from '@baca/utils' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' + +import { SocialButton } from '../SocialButton' + +let NativeGoogleButton: FC = () => null + +if (!isExpoGo && !isWeb) { + // Conditionally import makes it work with expo go + import('@react-native-google-signin/google-signin').then(({ GoogleSignin, statusCodes }) => { + type GoogleSignInError = Error & { code: keyof typeof statusCodes } + + NativeGoogleButton = () => { + const [isDisabled, setIsDisabled] = useState(false) + const { mutate: signInByGoogle } = useAuthGoogleControllerLogin() + const { t } = useTranslation() + + useEffect(() => { + // No extra configuration is needed, + // but for the more customization check: + // https://github.com/react-native-google-signin/google-signin#configureoptions + GoogleSignin?.configure?.({ + webClientId: ENV.WEB_CLIENT_ID, + }) + }, []) + + const verifyPlayServices = useCallback(async (): Promise => { + setIsDisabled(!(await GoogleSignin?.hasPlayServices?.())) + }, []) + + useEffect(() => { + verifyPlayServices() + }, [verifyPlayServices]) + + const verifyToken = useCallback(async (): Promise => { + const tokenResponse = await GoogleSignin?.getTokens?.() + + const { accessToken } = tokenResponse || {} + + signInByGoogle( + { + data: { + accessToken, + }, + }, + { + onSuccess: async (response) => { + const { user, ...token } = response + if (token) { + await setToken(token) + } + store.set(isSignedInAtom, true) + + // Send push token to backend + await assignPushToken() + }, + } + ) + }, [signInByGoogle]) + + const signIn = useCallback(async (): Promise => { + try { + await GoogleSignin?.signIn?.() + await verifyToken() + } catch (error) { + // TODO: This could be extracted to external function with an additional handling of the error codes + const typedError = error as GoogleSignInError + + if (typedError?.code) { + switch (typedError.code) { + case statusCodes?.SIGN_IN_CANCELLED: + case statusCodes?.IN_PROGRESS: + break + case statusCodes?.PLAY_SERVICES_NOT_AVAILABLE: + showErrorToast({ description: t('errors.play_services_not_available') }) + break + default: + showErrorToast({ description: t('errors.something_went_wrong') }) + break + } + return + } + + showErrorToast({ description: t('errors.something_went_wrong') }) + } + }, [t, verifyToken]) + + return + } + }) +} + +export { NativeGoogleButton } diff --git a/src/components/molecules/SocialButtons/GoogleButton/index.tsx b/src/components/molecules/SocialButtons/GoogleButton/index.tsx new file mode 100644 index 00000000..184d596c --- /dev/null +++ b/src/components/molecules/SocialButtons/GoogleButton/index.tsx @@ -0,0 +1,9 @@ +import { isExpoGo, isWeb } from '@baca/constants' + +import { NativeGoogleButton } from './NativeGoogleButton' + +export const GoogleButton = () => { + //TODO: Add google button for web + if (isExpoGo || isWeb) return null + return +} diff --git a/src/components/molecules/SocialButtons/SocialButton.tsx b/src/components/molecules/SocialButtons/SocialButton.tsx new file mode 100644 index 00000000..a8e96848 --- /dev/null +++ b/src/components/molecules/SocialButtons/SocialButton.tsx @@ -0,0 +1,53 @@ +import { appleIconDark, appleIcon, facebookIcon, googleIcon } from '@baca/constants' +import { useColorScheme } from '@baca/contexts' +import { Button, ButtonProps } from '@baca/design-system' +import i18n from '@baca/i18n' +import { FC } from 'react' +import { Image, ImageSourcePropType } from 'react-native' + +type SocialMediaType = 'apple' | 'facebook' | 'google' + +const socialButtonVariants: { + [key in SocialMediaType]: { + source: { light: ImageSourcePropType; dark?: ImageSourcePropType } + text: () => string + } +} = { + apple: { + source: { light: appleIcon, dark: appleIconDark }, + text: () => i18n.t('sign_in_screen.sign_in_by_apple'), + }, + facebook: { + source: { light: facebookIcon }, + text: () => i18n.t('sign_in_screen.sign_in_by_facebook'), + }, + google: { + source: { light: googleIcon }, + text: () => i18n.t('sign_in_screen.sign_in_by_google'), + }, +} + +type SocialButtonProps = { + onPress: () => void + type: SocialMediaType +} & ButtonProps + +export const SocialButton: FC = ({ type = 'google', ...rest }) => { + const { colorScheme } = useColorScheme() + + const { source, text } = socialButtonVariants[type] + + return ( + + ) +} diff --git a/src/components/molecules/SocialButtons/index.ts b/src/components/molecules/SocialButtons/index.ts new file mode 100644 index 00000000..e81f3f99 --- /dev/null +++ b/src/components/molecules/SocialButtons/index.ts @@ -0,0 +1,2 @@ +export * from './GoogleButton' +export * from './SocialButton' diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index 4f4e7d1d..e0da150f 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -1,3 +1,4 @@ export * from './Field' export * from './MenuItem' +export * from './SocialButtons' export * from './TextArea' diff --git a/src/components/wrappers/FormWrapper.tsx b/src/components/wrappers/FormWrapper.tsx index 1375cc6f..ee4a42a2 100644 --- a/src/components/wrappers/FormWrapper.tsx +++ b/src/components/wrappers/FormWrapper.tsx @@ -21,6 +21,7 @@ export const FormWrapper: FC> = ({ return ( ) : ( (props: PressableStateCallbackType) => ( - + + {leftElement && leftElement} {leftIconName && iconElement(props, leftIconName)} {childrenElement(props)} {rightIconName && iconElement(props, rightIconName)} diff --git a/src/design-system/components/Button/__snapshots__/Button.test.tsx.snap b/src/design-system/components/Button/__snapshots__/Button.test.tsx.snap index 541ac05a..8acaea4b 100644 --- a/src/design-system/components/Button/__snapshots__/Button.test.tsx.snap +++ b/src/design-system/components/Button/__snapshots__/Button.test.tsx.snap @@ -49,11 +49,13 @@ exports[`Button renders correctly 1`] = ` testID="baseButton" > { {t('components_screen.test_notification')} - - - {t('components_screen.typography.label')} - - - {t('components_screen.button_variants.header')} - + + {t('components_screen.button_variants.header')}: - - + + - - + + - - - - - + + + + - + + + + {t('sign_in_screen.do_not_have_an_account')} diff --git a/src/screens/auth/SignUpScreen.tsx b/src/screens/auth/SignUpScreen.tsx index c009e066..4706bc52 100644 --- a/src/screens/auth/SignUpScreen.tsx +++ b/src/screens/auth/SignUpScreen.tsx @@ -1,4 +1,4 @@ -import { CompanyLogo, ControlledField, FormWrapper } from '@baca/components' +import { CompanyLogo, ControlledField, FormWrapper, GoogleButton } from '@baca/components' import { Box, Button, Center, Display, Row, Spacer, Text } from '@baca/design-system' import { useSignUpForm, useTranslation } from '@baca/hooks' import { router } from 'expo-router' @@ -118,6 +118,10 @@ export const SignUpScreen = () => { {t('sign_up_screen.get_started')} + + + + {t('sign_up_screen.already_have_an_account')}