diff --git a/src/components/SignInButtons/AppleAuthWrapper/index.ios.js b/src/components/SignInButtons/AppleAuthWrapper/index.ios.tsx similarity index 81% rename from src/components/SignInButtons/AppleAuthWrapper/index.ios.js rename to src/components/SignInButtons/AppleAuthWrapper/index.ios.tsx index 69882d89b1fe..12ead0267db3 100644 --- a/src/components/SignInButtons/AppleAuthWrapper/index.ios.js +++ b/src/components/SignInButtons/AppleAuthWrapper/index.ios.tsx @@ -5,19 +5,18 @@ import * as Session from '@userActions/Session'; /** * Apple Sign In wrapper for iOS * revokes the session if the credential is revoked. - * - * @returns {null} */ function AppleAuthWrapper() { useEffect(() => { if (!appleAuth.isSupported) { return; } - const listener = appleAuth.onCredentialRevoked(() => { + const removeListener = appleAuth.onCredentialRevoked(() => { Session.signOut(); }); + return () => { - listener.remove(); + removeListener(); }; }, []); diff --git a/src/components/SignInButtons/AppleAuthWrapper/index.js b/src/components/SignInButtons/AppleAuthWrapper/index.tsx similarity index 100% rename from src/components/SignInButtons/AppleAuthWrapper/index.js rename to src/components/SignInButtons/AppleAuthWrapper/index.tsx diff --git a/src/components/SignInButtons/AppleSignIn/index.android.js b/src/components/SignInButtons/AppleSignIn/index.android.tsx similarity index 92% rename from src/components/SignInButtons/AppleSignIn/index.android.js rename to src/components/SignInButtons/AppleSignIn/index.android.tsx index 9dc736789c61..cfd1c48ee8b5 100644 --- a/src/components/SignInButtons/AppleSignIn/index.android.js +++ b/src/components/SignInButtons/AppleSignIn/index.android.tsx @@ -18,9 +18,9 @@ const config = { /** * Apple Sign In method for Android that returns authToken. - * @returns {Promise} + * @returns Promise that returns a string when resolved */ -function appleSignInRequest() { +function appleSignInRequest(): Promise { appleAuthAndroid.configure(config); return appleAuthAndroid .signIn() @@ -32,7 +32,6 @@ function appleSignInRequest() { /** * Apple Sign In button for Android. - * @returns {React.Component} */ function AppleSignIn() { const handleSignIn = () => { diff --git a/src/components/SignInButtons/AppleSignIn/index.desktop.js b/src/components/SignInButtons/AppleSignIn/index.desktop.tsx similarity index 96% rename from src/components/SignInButtons/AppleSignIn/index.desktop.js rename to src/components/SignInButtons/AppleSignIn/index.desktop.tsx index cc7ae5b623a5..792c16ed0b4a 100644 --- a/src/components/SignInButtons/AppleSignIn/index.desktop.js +++ b/src/components/SignInButtons/AppleSignIn/index.desktop.tsx @@ -10,7 +10,6 @@ const appleSignInWebRouteForDesktopFlow = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL} /** * Apple Sign In button for desktop flow - * @returns {React.Component} */ function AppleSignIn() { const styles = useThemeStyles(); diff --git a/src/components/SignInButtons/AppleSignIn/index.ios.js b/src/components/SignInButtons/AppleSignIn/index.ios.tsx similarity index 91% rename from src/components/SignInButtons/AppleSignIn/index.ios.js rename to src/components/SignInButtons/AppleSignIn/index.ios.tsx index f5c6333dcf7b..3fb1179d0365 100644 --- a/src/components/SignInButtons/AppleSignIn/index.ios.js +++ b/src/components/SignInButtons/AppleSignIn/index.ios.tsx @@ -7,9 +7,9 @@ import CONST from '@src/CONST'; /** * Apple Sign In method for iOS that returns identityToken. - * @returns {Promise} + * @returns Promise that returns a string when resolved */ -function appleSignInRequest() { +function appleSignInRequest(): Promise { return appleAuth .performRequest({ requestedOperation: appleAuth.Operation.LOGIN, @@ -20,7 +20,7 @@ function appleSignInRequest() { .then((response) => appleAuth.getCredentialStateForUser(response.user).then((credentialState) => { if (credentialState !== appleAuth.State.AUTHORIZED) { - Log.alert('[Apple Sign In] Authentication failed. Original response: ', response); + Log.alert('[Apple Sign In] Authentication failed. Original response: ', {response}); throw new Error('Authentication failed'); } return response.identityToken; @@ -30,7 +30,6 @@ function appleSignInRequest() { /** * Apple Sign In button for iOS. - * @returns {React.Component} */ function AppleSignIn() { const handleSignIn = () => { diff --git a/src/components/SignInButtons/AppleSignIn/index.website.js b/src/components/SignInButtons/AppleSignIn/index.website.tsx similarity index 74% rename from src/components/SignInButtons/AppleSignIn/index.website.js rename to src/components/SignInButtons/AppleSignIn/index.website.tsx index adae0a691e13..f256330c2344 100644 --- a/src/components/SignInButtons/AppleSignIn/index.website.js +++ b/src/components/SignInButtons/AppleSignIn/index.website.tsx @@ -1,45 +1,37 @@ -import get from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useState} from 'react'; -import Config from 'react-native-config'; +import Config, {NativeConfig} from 'react-native-config'; import getUserLanguage from '@components/SignInButtons/GetUserLanguage'; -import withNavigationFocus from '@components/withNavigationFocus'; +import withNavigationFocus, {WithNavigationFocusProps} from '@components/withNavigationFocus'; import Log from '@libs/Log'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; +import {AppleIDSignInOnFailureEvent, AppleIDSignInOnSuccessEvent} from '@src/types/modules/dom'; // react-native-config doesn't trim whitespace on iOS for some reason so we // add a trim() call to lodashGet here to prevent headaches. -const lodashGet = (config, key, defaultValue) => get(config, key, defaultValue).trim(); +const getConfig = (config: NativeConfig, key: string, defaultValue: string) => (config?.[key] ?? defaultValue).trim(); -const requiredPropTypes = { - isDesktopFlow: PropTypes.bool.isRequired, +type AppleSignInDivProps = { + isDesktopFlow: boolean; }; -const singletonPropTypes = { - ...requiredPropTypes, - - // From withNavigationFocus - isFocused: PropTypes.bool.isRequired, +type SingletonAppleSignInButtonProps = AppleSignInDivProps & { + isFocused: boolean; }; -const propTypes = { - // Prop to indicate if this is the desktop flow or not. - isDesktopFlow: PropTypes.bool, -}; -const defaultProps = { - isDesktopFlow: false, +type AppleSignInProps = WithNavigationFocusProps & { + isDesktopFlow?: boolean; }; /** * Apple Sign In Configuration for Web. */ const config = { - clientId: lodashGet(Config, 'ASI_CLIENTID_OVERRIDE', CONFIG.APPLE_SIGN_IN.SERVICE_ID), + clientId: getConfig(Config, 'ASI_CLIENTID_OVERRIDE', CONFIG.APPLE_SIGN_IN.SERVICE_ID), scope: 'name email', // never used, but required for configuration - redirectURI: lodashGet(Config, 'ASI_REDIRECTURI_OVERRIDE', CONFIG.APPLE_SIGN_IN.REDIRECT_URI), + redirectURI: getConfig(Config, 'ASI_REDIRECTURI_OVERRIDE', CONFIG.APPLE_SIGN_IN.REDIRECT_URI), state: '', nonce: '', usePopup: true, @@ -49,23 +41,22 @@ const config = { * Apple Sign In success and failure listeners. */ -const successListener = (event) => { +const successListener = (event: AppleIDSignInOnSuccessEvent) => { const token = event.detail.authorization.id_token; Session.beginAppleSignIn(token); }; -const failureListener = (event) => { +const failureListener = (event: AppleIDSignInOnFailureEvent) => { if (!event.detail || event.detail.error === 'popup_closed_by_user') { return null; } - Log.warn(`Apple sign-in failed: ${event.detail}`); + Log.warn(`Apple sign-in failed: ${event.detail.error}`); }; /** * Apple Sign In button for Web. - * @returns {React.Component} */ -function AppleSignInDiv({isDesktopFlow}) { +function AppleSignInDiv({isDesktopFlow}: AppleSignInDivProps) { useEffect(() => { // `init` renders the button, so it must be called after the div is // first mounted. @@ -108,24 +99,20 @@ function AppleSignInDiv({isDesktopFlow}) { ); } -AppleSignInDiv.propTypes = requiredPropTypes; - // The Sign in with Apple script may fail to render button if there are multiple // of these divs present in the app, as it matches based on div id. So we'll // only mount the div when it should be visible. -function SingletonAppleSignInButton({isFocused, isDesktopFlow}) { +function SingletonAppleSignInButton({isFocused, isDesktopFlow}: SingletonAppleSignInButtonProps) { if (!isFocused) { return null; } return ; } -SingletonAppleSignInButton.propTypes = singletonPropTypes; - // withNavigationFocus is used to only render the button when it is visible. const SingletonAppleSignInButtonWithFocus = withNavigationFocus(SingletonAppleSignInButton); -function AppleSignIn({isDesktopFlow}) { +function AppleSignIn({isDesktopFlow = false}: AppleSignInProps) { const [scriptLoaded, setScriptLoaded] = useState(false); useEffect(() => { if (window.appleAuthScriptLoaded) { @@ -148,7 +135,5 @@ function AppleSignIn({isDesktopFlow}) { return ; } -AppleSignIn.propTypes = propTypes; -AppleSignIn.defaultProps = defaultProps; - +AppleSignIn.displayName = 'AppleSignIn'; export default withNavigationFocus(AppleSignIn); diff --git a/src/components/SignInButtons/GetUserLanguage.js b/src/components/SignInButtons/GetUserLanguage.ts similarity index 50% rename from src/components/SignInButtons/GetUserLanguage.js rename to src/components/SignInButtons/GetUserLanguage.ts index 7f45f1fa1e89..611ba415008b 100644 --- a/src/components/SignInButtons/GetUserLanguage.js +++ b/src/components/SignInButtons/GetUserLanguage.ts @@ -1,11 +1,16 @@ +import {ValueOf} from 'type-fest'; + const localeCodes = { en: 'en_US', es: 'es_ES', -}; +} as const; + +type LanguageCode = keyof typeof localeCodes; +type LocaleCode = ValueOf; -const GetUserLanguage = () => { +const GetUserLanguage = (): LocaleCode => { const userLanguage = navigator.language || navigator.userLanguage; - const languageCode = userLanguage.split('-')[0]; + const languageCode = userLanguage.split('-')[0] as LanguageCode; return localeCodes[languageCode] || 'en_US'; }; diff --git a/src/components/SignInButtons/GoogleSignIn/index.desktop.js b/src/components/SignInButtons/GoogleSignIn/index.desktop.tsx similarity index 78% rename from src/components/SignInButtons/GoogleSignIn/index.desktop.js rename to src/components/SignInButtons/GoogleSignIn/index.desktop.tsx index 9284a5332e3d..3c2abb1679f0 100644 --- a/src/components/SignInButtons/GoogleSignIn/index.desktop.js +++ b/src/components/SignInButtons/GoogleSignIn/index.desktop.tsx @@ -1,19 +1,15 @@ import React from 'react'; import {View} from 'react-native'; import IconButton from '@components/SignInButtons/IconButton'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -const propTypes = {...withLocalizePropTypes}; - const googleSignInWebRouteForDesktopFlow = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${ROUTES.GOOGLE_SIGN_IN}`; /** * Google Sign In button for desktop flow. - * @returns {React.Component} */ function GoogleSignIn() { const styles = useThemeStyles(); @@ -30,6 +26,5 @@ function GoogleSignIn() { } GoogleSignIn.displayName = 'GoogleSignIn'; -GoogleSignIn.propTypes = propTypes; -export default withLocalize(GoogleSignIn); +export default GoogleSignIn; diff --git a/src/components/SignInButtons/GoogleSignIn/index.native.js b/src/components/SignInButtons/GoogleSignIn/index.native.tsx similarity index 98% rename from src/components/SignInButtons/GoogleSignIn/index.native.js rename to src/components/SignInButtons/GoogleSignIn/index.native.tsx index c7ac763cfb73..2744d8958080 100644 --- a/src/components/SignInButtons/GoogleSignIn/index.native.js +++ b/src/components/SignInButtons/GoogleSignIn/index.native.tsx @@ -43,7 +43,6 @@ function googleSignInRequest() { /** * Google Sign In button for iOS. - * @returns {React.Component} */ function GoogleSignIn() { return ( diff --git a/src/components/SignInButtons/GoogleSignIn/index.website.js b/src/components/SignInButtons/GoogleSignIn/index.website.tsx similarity index 83% rename from src/components/SignInButtons/GoogleSignIn/index.website.js rename to src/components/SignInButtons/GoogleSignIn/index.website.tsx index 8f8a977bdb09..5d419c8744e5 100644 --- a/src/components/SignInButtons/GoogleSignIn/index.website.js +++ b/src/components/SignInButtons/GoogleSignIn/index.website.tsx @@ -1,28 +1,21 @@ -import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; import {View} from 'react-native'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; +import Response from '@src/types/modules/google'; -const propTypes = { - /** Whether we're rendering in the Desktop Flow, if so show a different button. */ - isDesktopFlow: PropTypes.bool, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - isDesktopFlow: false, +type GoogleSignInProps = { + isDesktopFlow?: boolean; }; /** Div IDs for styling the two different Google Sign-In buttons. */ const mainId = 'google-sign-in-main'; const desktopId = 'google-sign-in-desktop'; -const signIn = (response) => { +const signIn = (response: Response) => { Session.beginGoogleSignIn(response.credential); }; @@ -31,12 +24,15 @@ const signIn = (response) => { * We have to load the gis script and then determine if the page is focused before rendering the button. * @returns {React.Component} */ -function GoogleSignIn({translate, isDesktopFlow}) { + +function GoogleSignIn({isDesktopFlow = false}: GoogleSignInProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); const loadScript = useCallback(() => { const google = window.google; if (google) { google.accounts.id.initialize({ + // eslint-disable-next-line @typescript-eslint/naming-convention client_id: CONFIG.GOOGLE_SIGN_IN.WEB_CLIENT_ID, callback: signIn, }); @@ -92,7 +88,5 @@ function GoogleSignIn({translate, isDesktopFlow}) { } GoogleSignIn.displayName = 'GoogleSignIn'; -GoogleSignIn.propTypes = propTypes; -GoogleSignIn.defaultProps = defaultProps; -export default withLocalize(GoogleSignIn); +export default GoogleSignIn; diff --git a/src/components/SignInButtons/IconButton.js b/src/components/SignInButtons/IconButton.tsx similarity index 65% rename from src/components/SignInButtons/IconButton.js rename to src/components/SignInButtons/IconButton.tsx index 19a5bd9b27b8..848ca5463854 100644 --- a/src/components/SignInButtons/IconButton.js +++ b/src/components/SignInButtons/IconButton.tsx @@ -1,25 +1,13 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import {ValueOf} from 'type-fest'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; - -const propTypes = { - /** The on press method */ - onPress: PropTypes.func, - - /** Which provider you are using to sign in */ - provider: PropTypes.string.isRequired, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - onPress: () => {}, -}; +import {TranslationPaths} from '@src/languages/types'; +import IconAsset from '@src/types/utils/IconAsset'; const providerData = { [CONST.SIGN_IN_METHOD.APPLE]: { @@ -30,9 +18,21 @@ const providerData = { icon: Expensicons.GoogleLogo, accessibilityLabel: 'common.signInWithGoogle', }, +} satisfies Record< + ValueOf, + { + icon: IconAsset; + accessibilityLabel: TranslationPaths; + } +>; + +type IconButtonProps = { + onPress?: () => void; + provider: ValueOf; }; -function IconButton({onPress, translate, provider}) { +function IconButton({onPress = () => {}, provider}: IconButtonProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); return ( | null; }; @@ -309,11 +309,11 @@ function beginAppleSignIn(idToken: string) { * Shows Google sign-in process, and if an auth token is successfully obtained, * passes the token on to the Expensify API to sign in with */ -function beginGoogleSignIn(token: string) { +function beginGoogleSignIn(token: string | null) { const {optimisticData, successData, failureData} = signInAttemptState(); type BeginGoogleSignInParams = { - token: string; + token: string | null; preferredLocale: ValueOf | null; }; diff --git a/src/types/modules/appleAuth.d.ts b/src/types/modules/appleAuth.d.ts new file mode 100644 index 000000000000..1394768d613e --- /dev/null +++ b/src/types/modules/appleAuth.d.ts @@ -0,0 +1,29 @@ +type ClientConfig = { + clientId?: string; + redirectURI?: string; + scope?: string; + state?: string; + nonce?: string; + usePopup?: boolean; +}; + +type Auth = { + init: (config: ClientConfig) => void; + signIn: (signInConfig?: ClientConfig) => Promise; + renderButton: () => void; +}; + +type AppleID = { + auth: Auth; +}; + +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Window { + AppleID: AppleID; + appleAuthScriptLoaded: boolean; + } +} + +// We used the export {} line to mark this file as an external module +export {}; diff --git a/src/types/modules/dom.d.ts b/src/types/modules/dom.d.ts new file mode 100644 index 000000000000..60bd9c9ae983 --- /dev/null +++ b/src/types/modules/dom.d.ts @@ -0,0 +1,24 @@ +type AppleIDSignInOnSuccessEvent = { + detail: { + authorization: { + // eslint-disable-next-line @typescript-eslint/naming-convention + id_token: string; + }; + }; +}; + +type AppleIDSignInOnFailureEvent = { + detail: { + error: string; + }; +}; + +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface DocumentEventMap extends GlobalEventHandlersEventMap { + AppleIDSignInOnSuccess: AppleIDSignInOnSuccessEvent; + AppleIDSignInOnFailure: AppleIDSignInOnFailureEvent; + } +} + +export type {AppleIDSignInOnFailureEvent, AppleIDSignInOnSuccessEvent}; diff --git a/src/types/modules/google.d.ts b/src/types/modules/google.d.ts new file mode 100644 index 000000000000..3c29e62bf9b3 --- /dev/null +++ b/src/types/modules/google.d.ts @@ -0,0 +1,35 @@ +type Response = { + credential: string; +}; + +type Initialize = { + // eslint-disable-next-line @typescript-eslint/naming-convention + client_id: string; + callback: (response: Response) => void; +}; + +type Options = { + theme?: 'outline'; + size?: 'large'; + type?: 'standard' | 'icon'; + shape?: 'circle' | 'pill'; + width?: string; +}; + +type Google = { + accounts: { + id: { + initialize: ({client_id, callback}: Initialize) => void; + renderButton: (client_id, options: Options) => void; + }; + }; +}; + +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Window { + google: Google; + } +} + +export default Response; diff --git a/src/types/modules/navigator.d.ts b/src/types/modules/navigator.d.ts new file mode 100644 index 000000000000..aca32a543b67 --- /dev/null +++ b/src/types/modules/navigator.d.ts @@ -0,0 +1,9 @@ +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Navigator { + userLanguage: string; + } +} + +// We used the export {} line to mark this file as an external module +export {};