Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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:",
Expand Down
Binary file added assets/social/apple-icon-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/social/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/social/facebook-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/social/google-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 9 additions & 19 deletions scripts/data/swagger-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@
}
},
"401": {
"description": "Unauthorized - Invalid credentials",
"description": "Unauthorized",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorUnauthorizedEntity" }
Expand Down Expand Up @@ -1287,7 +1287,7 @@
"ErrorValidationEntity": {
"type": "object",
"properties": {
"status": {
"statusCode": {
"type": "number",
"description": "HTTP status code indicating the error",
"example": 422
Expand All @@ -1299,7 +1299,7 @@
"additionalProperties": { "type": "string" }
}
},
"required": ["status", "errors"]
"required": ["statusCode", "errors"]
},
"UpdateArticleDto": {
"type": "object",
Expand Down Expand Up @@ -1434,7 +1434,7 @@
},
"required": ["id", "name"]
},
"LastConsentProperties": {
"LastConsentEntity": {
"type": "object",
"properties": {
"termsAccepted": {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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."
}
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/api/query/auth/auth.msw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})

Expand Down
4 changes: 2 additions & 2 deletions src/api/types/authGoogleLoginDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion src/api/types/errorValidationEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion src/api/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
2 changes: 1 addition & 1 deletion src/api/types/refreshEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions src/api/types/userEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> => {
setIsDisabled(!(await GoogleSignin?.hasPlayServices?.()))
}, [])

useEffect(() => {
verifyPlayServices()
}, [verifyPlayServices])

const verifyToken = useCallback(async (): Promise<void> => {
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<void> => {
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 <SocialButton disabled={isDisabled} onPress={signIn} type="google" />
}
})
}

export { NativeGoogleButton }
9 changes: 9 additions & 0 deletions src/components/molecules/SocialButtons/GoogleButton/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <NativeGoogleButton />
}
53 changes: 53 additions & 0 deletions src/components/molecules/SocialButtons/SocialButton.tsx
Original file line number Diff line number Diff line change
@@ -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<SocialButtonProps> = ({ type = 'google', ...rest }) => {
const { colorScheme } = useColorScheme()

const { source, text } = socialButtonVariants[type]

return (
<Button
alignItems="center"
justifyContent="center"
leftElement={<Image source={source[colorScheme] || source['light']} width={24} height={24} />}
variant="SecondaryGray"
maxW={360}
w="full"
{...rest}
>
{text()}
</Button>
)
}
2 changes: 2 additions & 0 deletions src/components/molecules/SocialButtons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './GoogleButton'
export * from './SocialButton'
1 change: 1 addition & 0 deletions src/components/molecules/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './Field'
export * from './MenuItem'
export * from './SocialButtons'
export * from './TextArea'
1 change: 1 addition & 0 deletions src/components/wrappers/FormWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const FormWrapper: FC<PropsWithChildren<FormWrapperProps>> = ({
return (
<SafeAreaView {...{ edges }} style={styles.safeAreaContainer}>
<KeyboardAwareScrollView
showsVerticalScrollIndicator={false}
{...keyboardAwareProps}
contentContainerStyle={[
styles.contentContainerStyle,
Expand Down
1 change: 0 additions & 1 deletion src/constants/asyncStorageKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export const ASYNC_STORAGE_KEYS = {
NEXT_DEEP_LINK: '@navigation/next_deeplink',
PUSH_TOKEN: '@notification/push-token',
USER_LANGUAGE: '@language/user-language',
USER_REFRESH_TOKEN: 'user_token-refresh_token', // This value is used in `expo-secure-store` package and it can't include '@' and '/'
USER_TOKEN: 'user_token-token', // This value is used in `expo-secure-store` package and it can't include '@' and '/'
WAS_PUSH_TOKEN_SEND: '@notification/was-push-token-send',
} as const
3 changes: 2 additions & 1 deletion src/constants/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Constants from 'expo-constants'

export const ENV = {
API_URL: Constants?.expoConfig?.extra?.API_URL,
ENVIRONMENT_NAME: Constants?.expoConfig?.extra?.ENVIRONMENT_NAME,
EAS_PROJECT_ID: Constants.expoConfig?.extra?.eas?.projectId,
ENVIRONMENT_NAME: Constants?.expoConfig?.extra?.ENVIRONMENT_NAME,
WEB_CLIENT_ID: Constants.expoConfig?.extra?.WEB_CLIENT_ID,
}
5 changes: 5 additions & 0 deletions src/constants/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export const lightBinarLogo = require('@baca/assets/logo/logo-light.png')

export const darkLogoSygnet = require('@baca/assets/logo/logo-sygnet-dark.png')
export const lightLogoSygnet = require('@baca/assets/logo/logo-sygnet-light.png')

export const appleIcon = require('@baca/assets/social/apple-icon.png')
export const appleIconDark = require('@baca/assets/social/apple-icon-dark.png')
export const facebookIcon = require('@baca/assets/social/facebook-icon.png')
export const googleIcon = require('@baca/assets/social/google-icon.png')
5 changes: 4 additions & 1 deletion src/design-system/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { StyledProps } from '../types'
export type ButtonProps = StyledProps &
PressableProps & {
disabled?: boolean
leftElement?: JSX.Element
leftIconName?: IconNames
loaderElement?: JSX.Element
loading?: boolean
Expand Down Expand Up @@ -70,6 +71,7 @@ const RawButton = memo(
{
children,
disabled,
leftElement,
leftIconName,
loading,
rightIconName,
Expand Down Expand Up @@ -295,7 +297,8 @@ const RawButton = memo(
/>
) : (
(props: PressableStateCallbackType) => (
<Row gap={buttonSizeVariant.iconGap}>
<Row alignItems="center" gap={buttonSizeVariant.iconGap}>
{leftElement && leftElement}
{leftIconName && iconElement(props, leftIconName)}
{childrenElement(props)}
{rightIconName && iconElement(props, rightIconName)}
Expand Down
Loading