diff --git a/App.tsx b/App.tsx index ae836f9e6..89bf65594 100644 --- a/App.tsx +++ b/App.tsx @@ -8,7 +8,6 @@ import ThemeManager from './src/custom-theme/ThemeManager'; import BaseProps from './src/types/BaseProps'; import { ThemeType } from './src/types/Theme'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import RNBootSplash from 'react-native-bootsplash'; export interface Props extends BaseProps {} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 344a64abb..af289b527 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,72 +1,100 @@ + package="com.emobility"> - - - + + + + android:name=".MainApplication" + android:label="@string/app_name" + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher_round" + android:allowBackup="false" + android:theme="@style/BootTheme"> - - + + - - + + - - + + - - - + + + - - - - - + + + + + - + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/eMobility.xcodeproj/project.pbxproj b/ios/eMobility.xcodeproj/project.pbxproj index 7157fecda..c260e7e97 100644 --- a/ios/eMobility.xcodeproj/project.pbxproj +++ b/ios/eMobility.xcodeproj/project.pbxproj @@ -998,10 +998,12 @@ CODE_SIGN_ENTITLEMENTS = eMobility/eMobility.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = SG7N28T3A2; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = ""; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; INFOPLIST_FILE = eMobility.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/Frameworks @executable_path/Frameworks"; @@ -1045,7 +1047,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-splash-screen\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/yoga\"", ); - MARKETING_VERSION = 2.7.1; + MARKETING_VERSION = 2.7.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1073,9 +1075,11 @@ CODE_SIGN_ENTITLEMENTS = eMobility/eMobility.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = SG7N28T3A2; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = ""; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; INFOPLIST_FILE = eMobility.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/Frameworks @executable_path/Frameworks"; @@ -1119,7 +1123,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-splash-screen\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/yoga\"", ); - MARKETING_VERSION = 2.7.1; + MARKETING_VERSION = 2.7.2; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/ios/eMobility/AppDelegate.m b/ios/eMobility/AppDelegate.m index ede818541..f5254e048 100644 --- a/ios/eMobility/AppDelegate.m +++ b/ios/eMobility/AppDelegate.m @@ -143,13 +143,19 @@ - (UIInterfaceOrientationMask)application:(UIApplication *)application supported return [Orientation getOrientation]; } -- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options { return [RCTLinkingManager application:application openURL:url options:options]; } -- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler + +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity + restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { - return [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; } @end diff --git a/ios/eMobility/Info.plist b/ios/eMobility/Info.plist index 4a8e76201..2abd0d13e 100644 --- a/ios/eMobility/Info.plist +++ b/ios/eMobility/Info.plist @@ -1,114 +1,126 @@ - - LSApplicationQueriesSchemes - - comgooglemaps - citymapper - uber - lyft - transit - truckmap - waze - yandexnavi - moovit - yandextaxi - yandexmaps - kakaomap - szn-mapy - mapsme - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - $(BUNDLE_NAME) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - eMobility - CFBundleURLSchemes - - eMobility - - - - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSRequiresIPhoneOS - - NSAppTransportSecurity - NSExceptionDomains + LSApplicationQueriesSchemes + + comgooglemaps + citymapper + uber + lyft + transit + truckmap + waze + yandexnavi + moovit + yandextaxi + yandexmaps + kakaomap + szn-mapy + mapsme + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + $(BUNDLE_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + eMobility + CFBundleURLSchemes + + eMobility + + + + CFBundleURLSchemes + + https + + + + CFBundleURLSchemes + + http + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity - localhost + NSExceptionDomains - NSExceptionAllowsInsecureHTTPLoads - + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + NSLocationWhenInUseUsageDescription + Discover Charging Stations + UIAppFonts + + AntDesign.ttf + Entypo.ttf + EvilIcons.ttf + Feather.ttf + FontAwesome.ttf + FontAwesome5_Brands.ttf + FontAwesome5_Regular.ttf + FontAwesome5_Solid.ttf + Foundation.ttf + Ionicons.ttf + MaterialCommunityIcons.ttf + MaterialIcons.ttf + Octicons.ttf + Roboto_medium.ttf + Roboto.ttf + rubicon-icon-font.ttf + SimpleLineIcons.ttf + Zocial.ttf + Fontisto.ttf + + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + NSCameraUsageDescription + ${PRODUCT_NAME} needs access to the camera to scan the charging station's QR Code to register the organization or start a transaction + UIViewControllerBasedStatusBarAppearance + - NSLocationWhenInUseUsageDescription - Discover Charging Stations - UIAppFonts - - AntDesign.ttf - Entypo.ttf - EvilIcons.ttf - Feather.ttf - FontAwesome.ttf - FontAwesome5_Brands.ttf - FontAwesome5_Regular.ttf - FontAwesome5_Solid.ttf - Foundation.ttf - Ionicons.ttf - MaterialCommunityIcons.ttf - MaterialIcons.ttf - Octicons.ttf - Roboto_medium.ttf - Roboto.ttf - rubicon-icon-font.ttf - SimpleLineIcons.ttf - Zocial.ttf - Fontisto.ttf - - UIBackgroundModes - - remote-notification - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationPortraitUpsideDown - - NSCameraUsageDescription - ${PRODUCT_NAME} needs access to the camera to scan the charging station's QR Code to register the organization or start a transaction - UIViewControllerBasedStatusBarAppearance - - - + \ No newline at end of file diff --git a/ios/eMobility/eMobility.entitlements b/ios/eMobility/eMobility.entitlements index 903def2af..50ab07a9c 100644 --- a/ios/eMobility/eMobility.entitlements +++ b/ios/eMobility/eMobility.entitlements @@ -4,5 +4,20 @@ aps-environment development + com.apple.developer.associated-domains + + applinks:*.e-mobility-group.com + applinks:*.e-mobility-group.org + applinks:*.e-mobility-group.eu + applinks:*.e-mobility-group.fr + applinks:*.qa-e-mobility-group.com + applinks:*.qa-e-mobility-group.org + applinks:*.qa-e-mobility-group.eu + applinks:*.qa-e-mobility-group.fr + applinks:*.e-mobility-labs.com + applinks:*.e-mobility-labs.org + applinks:*.e-mobility-labs.eu + applinks:*.e-mobility-labs.fr + diff --git a/ios/eMobility/eMobilityRelease.entitlements b/ios/eMobility/eMobilityRelease.entitlements new file mode 100644 index 000000000..50ab07a9c --- /dev/null +++ b/ios/eMobility/eMobilityRelease.entitlements @@ -0,0 +1,23 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + applinks:*.e-mobility-group.com + applinks:*.e-mobility-group.org + applinks:*.e-mobility-group.eu + applinks:*.e-mobility-group.fr + applinks:*.qa-e-mobility-group.com + applinks:*.qa-e-mobility-group.org + applinks:*.qa-e-mobility-group.eu + applinks:*.qa-e-mobility-group.fr + applinks:*.e-mobility-labs.com + applinks:*.e-mobility-labs.org + applinks:*.e-mobility-labs.eu + applinks:*.e-mobility-labs.fr + + + diff --git a/package.json b/package.json index 9a6ace1de..f3b9e7f14 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,12 @@ "build:prepare": "if [ ! -f android/app/google-services.json ]; then cp assets/google-services-template.json android/app/google-services.json; fi && if [ ! -f ios/GoogleService-Info.plist ]; then cp assets/GoogleService-Info-template.plist ios/GoogleService-Info.plist; fi", "import-sort": "npx import-sort-cli --write '{src,__tests__,types}/**/*.ts{,x}' *.ts{,x}", "check:i18n": "cross-env TS_NODE_FILES=true ts-node-dev --project tsconfig-i18n.json --files test/I18nChecker.ts", - "react-native:install": "npm install --force && npm clean-install && cd ios && pod install" + "react-native:install": "npm install --force && npm clean-install && cd ios && pod install", + "deep-link:android:verify-email": "npx uri-scheme open \"https://slf.e-mobility-group.com/verify-email?Email=alixhumbert@gmail.com&VerificationToken=3q45j34trqfweofn2eijtne234fwegf\" --android", + "deep-link:android:reset-password": "npx uri-scheme open \"https://slfcah.e-mobility-group.com/define-password?hash=3q45j34trqfweofn2eijtne234fwegf\" --android", + "deep-link:ios:verify-email": "npx uri-scheme open \"https://slf.e-mobility-group.com/verify-email?Email=alixhumbert@gmail.com&VerificationToken=3q45j34trqfweofn2eijtne234fwegf\" --ios", + "deep-link:ios:reset-password": "npx uri-scheme open \"https://slfcah.e-mobility-group.com/define-password?hash=3q45j34trqfweofn2eijtne234fwegf\" --ios" + }, "importSort": { ".js, .jsx, .es6, .es, .mjs": { diff --git a/src/App.tsx b/src/App.tsx index bae346e83..d0d6fe200 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,7 @@ import CentralServerProvider from './provider/CentralServerProvider'; import ProviderFactory from './provider/ProviderFactory'; import Eula from './screens/auth/eula/Eula'; import Login from './screens/auth/login/Login'; -import ResetPassword from './screens/auth/reset-password/ResetPassword'; +import CreatePassword from './screens/auth/create-password/CreatePassword'; import RetrievePassword from './screens/auth/retrieve-password/RetrievePassword'; import SignUp from './screens/auth/sign-up/SignUp'; import Cars from './screens/cars/Cars'; @@ -58,6 +58,7 @@ import FontAwesome from 'react-native-vector-icons/FontAwesome'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import Settings from './screens/settings/Settings'; import RNBootSplash from 'react-native-bootsplash'; +import Message from './utils/Message'; // Init i18n I18nManager.initialize(); @@ -135,7 +136,7 @@ function createAuthNavigator(props: BaseProps) { - + ); @@ -520,6 +521,8 @@ export default class App extends React.Component { public centralServerProvider: CentralServerProvider; private location: LocationManager; private appVersion: CheckVersionResponse; + private deepLinkingUnsubscribe: () => void; + private navigationRef: any; public constructor(props: Props) { super(props); @@ -540,6 +543,7 @@ export default class App extends React.Component { public async componentDidMount() { // Load Navigation State + Message.showError('mounted') const navigationState = await SecuredStorage.getNavigationState(); // Get the central server this.centralServerProvider = await ProviderFactory.getProvider(); @@ -549,10 +553,6 @@ export default class App extends React.Component { await this.notificationManager.start(); // Assign this.centralServerProvider.setNotificationManager(this.notificationManager); - // Init Deep Linking --------------------------------------- - this.deepLinkingManager = DeepLinkingManager.getInstance(); - // Activate Deep links - this.deepLinkingManager.startListening(); // Location ------------------------------------------------ this.location = await LocationManager.getInstance(); this.location.startListening(); @@ -572,7 +572,7 @@ export default class App extends React.Component { public componentWillUnmount() { // Deactivate Deep links - this.deepLinkingManager?.stopListening(); + this.deepLinkingUnsubscribe?.(); // Stop Notifications this.notificationManager?.stop(); // Stop Location @@ -598,14 +598,8 @@ export default class App extends React.Component { return ( void RNBootSplash.hide({ fade: true })} - ref={(navigatorRef) => { - if (navigatorRef) { - this.notificationManager?.initialize(navigatorRef); - this.notificationManager.checkOnHoldNotification(); - this.deepLinkingManager?.initialize(navigatorRef, this.centralServerProvider); - } - }} + onReady={() => void this.onNavigationReady()} + ref={(ref) => this.navigationRef = ref} onStateChange={persistNavigationState} initialState={this.state.navigationState}> @@ -616,4 +610,17 @@ export default class App extends React.Component { ); } + + private async onNavigationReady(): Promise { + await RNBootSplash.hide({ fade: true }); + this.centralServerProvider = await ProviderFactory.getProvider(); + Message.showWarning('onnavigationready'); + this.notificationManager?.initialize(this.navigationRef); + void this.notificationManager.checkOnHoldNotification(); + // Init Deep Linking --------------------------------------- + this.deepLinkingManager = DeepLinkingManager.getInstance(); + this.deepLinkingManager?.initialize(this.navigationRef, this.centralServerProvider); + // Activate deep linking + this.deepLinkingUnsubscribe = this.deepLinkingManager.startListening(); + } } diff --git a/src/I18n/languages/cs.json b/src/I18n/languages/cs.json index eac6ddccc..cf1f32d08 100644 --- a/src/I18n/languages/cs.json +++ b/src/I18n/languages/cs.json @@ -113,7 +113,7 @@ "mandatoryTenant": "Organizace je povinná", "mandatoryEndpointName": "Endpoint Name is mandatory", "mandatoryEndpointURL": "Endpoint URL is mandatory", - "unknownTenant": "Neznámá organizace!", + "unknownTenant": "Organization with subdomain '{{tenantSubdomain}}' not found!", "mandatoryName": "Název je povinný", "mandatoryFirstName": "První jméno je povinné", "mandatoryEmail": "E-mail je povinný", @@ -134,8 +134,7 @@ "accountAlreadyActive": "Váš účet je již aktivní", "accountVerifiedSuccess": "Váš účet byl úspěšně aktivován!", "accountPending": "Váš účet čeká na vyřízení! Zkontrolujte svůj e-mail", - "activationTokenNotValid": "Aktivační odkaz vašeho účtu již není platný", - "activationEmailNotValid": "Tento e-mail neexistuje", + "activationEmailNotValid": "This Email does not exist in organization {{tenantName}}", "activationUnexpectedError": "Nelze aktivovat účet", "loginUnexpectedError": "Nelze se přihlásit", "resetSuccess": "Požadavek přijat! Zkontrolujte svůj e-mail", @@ -144,6 +143,7 @@ "forgotYourPassword": "Zapomněli jste své heslo?", "retrievePassword": "Načíst heslo", "resetPassword": "Resetovat heslo", + "createPassword": "Create Password", "backLogin": "Zpět na přehlášení", "required": "Požadováno", "acceptEula": "Přijímám", @@ -154,6 +154,10 @@ "resetPasswordUnexpectedError": "Heslo nelze resetovat", "resetPasswordHashNotValid": "Požadavek již není platný, požádejte znovu o nové heslo", "verifyAccountTokenNotValid": "Požadavek již není platný, požádejte o validaci nového účtu", + "invalidLinkNoToken": "Invalid link (missing token)", + "invalidLinkNoSubdomain": "Invalid link (missing subdomain)", + "invalidLinkNoEmail": "Invalid link (missing email)", + "invalidLinkNoHash": "Invalid link (missing hash)", "haveAlreadyAccount": "Máte již účet?", "name": "Příjmení", "firstName": "Křestní jméno", diff --git a/src/I18n/languages/de.json b/src/I18n/languages/de.json index e11168b63..ed541f938 100644 --- a/src/I18n/languages/de.json +++ b/src/I18n/languages/de.json @@ -113,7 +113,7 @@ "mandatoryTenant": "Die Organisation ist notwendig", "mandatoryEndpointName": "Der Endpunkt-Name ist notwendig", "mandatoryEndpointURL": "Die Endpunkt-URL ist notwendig", - "unknownTenant": "Unbekannte Organisation!", + "unknownTenant": "Organization with subdomain '{{tenantSubdomain}}' not found!", "mandatoryName": "Der Name ist notwendig", "mandatoryFirstName": "Der Vorname ist notwendig", "mandatoryEmail": "Die E-Mail ist notwendig", @@ -134,8 +134,7 @@ "accountAlreadyActive": "Ihr Account ist bereits aktiviert", "accountVerifiedSuccess": "Ihr Account wurde erfolgreich aktiviert!", "accountPending": "Ihr Konto wurde noch nicht aktiviert, überprüfen Sie Ihre E-Mails!", - "activationTokenNotValid": "Der Aktivierungslink Ihres Account ist nicht mehr aktiv", - "activationEmailNotValid": "Diese Email existiert nicht mehr", + "activationEmailNotValid": "This Email does not exist in organization {{tenantName}}", "activationUnexpectedError": "Account kann nicht aktiviert werden", "loginUnexpectedError": "Login fehlgeschlagen", "resetSuccess": "Anfrage angenommen, überprüfen Sie Ihre E-Mails!", @@ -144,6 +143,7 @@ "forgotYourPassword": "Passwort vergessen?", "retrievePassword": "Passwort wieder auffinden", "resetPassword": "Passwort zurücksetzen", + "createPassword": "Create Password", "backLogin": "Zurück zur Anmeldung", "required": "Benötigt", "acceptEula": "Ich bestätige den ", @@ -154,6 +154,10 @@ "resetPasswordHashNotValid": "Anfrage nicht mehr gültig, fordern Sie erneut ein neues Passwort an", "resetPasswordUnexpectedError": "Passwort kann nicht zurückgesetzt werden", "verifyAccountTokenNotValid": "Anfrage ist nicht länger gültig, fordern sie eine neue Accountverifizierung an", + "invalidLinkNoToken": "Invalid link (missing token)", + "invalidLinkNoSubdomain": "Invalid link (missing subdomain)", + "invalidLinkNoEmail": "Invalid link (missing email)", + "invalidLinkNoHash": "Invalid link (missing hash)", "haveAlreadyAccount": "Haben Sie bereits ein Konto?", "name": "Name", "firstName": "Vorname", diff --git a/src/I18n/languages/en.json b/src/I18n/languages/en.json index 9bfbb48c1..4fe1f8bd5 100644 --- a/src/I18n/languages/en.json +++ b/src/I18n/languages/en.json @@ -113,7 +113,7 @@ "mandatoryTenant": "The Organization is mandatory", "mandatoryEndpointName": "Endpoint Name is mandatory", "mandatoryEndpointURL": "Endpoint URL is mandatory", - "unknownTenant": "Unknown Organization!", + "unknownTenant": "Organization with subdomain '{{tenantSubdomain}}' not found!", "mandatoryName": "The name is mandatory", "mandatoryFirstName": "The First Name is mandatory", "mandatoryEmail": "The email is mandatory", @@ -134,8 +134,7 @@ "accountAlreadyActive": "Your account is already active", "accountVerifiedSuccess": "Your account has been activated with success!", "accountPending": "Your account is pending! Check your e-mail", - "activationTokenNotValid": "Your account's activation link is no longer valid", - "activationEmailNotValid": "This Email does not exist", + "activationEmailNotValid": "This Email does not exist in organization {{tenantName}}", "activationUnexpectedError": "Cannot activate account", "loginUnexpectedError": "Cannot login", "resetSuccess": "Request accepted! Check your e-mail", @@ -144,6 +143,7 @@ "forgotYourPassword": "Forgot Password?", "retrievePassword": "Retrieve Password", "resetPassword": "Reset Password", + "createPassword": "Create Password", "backLogin": "Back to Login", "required": "Required", "acceptEula": "I accept the ", @@ -154,6 +154,10 @@ "resetPasswordUnexpectedError": "Cannot reset password", "resetPasswordHashNotValid": "Request no longer valid, request a new password again", "verifyAccountTokenNotValid": "Request no longer valid, request a new account validation", + "invalidLinkNoToken": "Invalid link (missing token)", + "invalidLinkNoSubdomain": "Invalid link (missing subdomain)", + "invalidLinkNoEmail": "Invalid link (missing email)", + "invalidLinkNoHash": "Invalid link (missing hash)", "haveAlreadyAccount": "Already have an account?", "name": "Name", "firstName": "First Name", diff --git a/src/I18n/languages/es.json b/src/I18n/languages/es.json index d7db4ae79..616b8d239 100644 --- a/src/I18n/languages/es.json +++ b/src/I18n/languages/es.json @@ -113,7 +113,7 @@ "mandatoryTenant": "La organización es obligatoria", "mandatoryEndpointName": "Endpoint Name is mandatory", "mandatoryEndpointURL": "Endpoint URL is mandatory", - "unknownTenant": "¡Organización desconocida!", + "unknownTenant": "Organization with subdomain '{{tenantSubdomain}}' not found!", "mandatoryName": "El apellido es obligatorio", "mandatoryFirstName": "El nombre es obligatorio", "mandatoryEmail": "El correo electrónico es obligatorio", @@ -134,8 +134,7 @@ "accountAlreadyActive": "Su cuenta ya ha sido activada", "accountVerifiedSuccess": "¡Su cuenta ha sido activada exitosamente!", "accountPending": "¡Su cuenta está pendiente! Revise su correo electrónico", - "activationTokenNotValid": "El código de activación de su cuenta ya no es valido", - "activationEmailNotValid": "El correo electónico ya no existe", + "activationEmailNotValid": "This Email does not exist in organization {{tenantName}}", "activationUnexpectedError": "No se ha podido activar la cuenta", "loginUnexpectedError": "No se puede iniciar sesión", "resetSuccess": "¡Solicitud aceptada! Revise su correo electrónico", @@ -144,6 +143,7 @@ "forgotYourPassword": "¿Olvidó su contraseña?", "retrievePassword": "Recuperar contraseña", "resetPassword": "Restablecer contraseña", + "createPassword": "Create Password", "backLogin": "Iniciar sesión", "required": "Obligatorio", "acceptEula": "Acepto el ", @@ -154,6 +154,10 @@ "resetPasswordUnexpectedError": "No se ha podido reestablecer su contraseña", "resetPasswordHashNotValid": "La solicitud ya no es válida, solicite nuevamente una contraseña", "verifyAccountTokenNotValid": "La solicitud ya no es válida, solicite nuevamente una validación de cuenta", + "invalidLinkNoToken": "Invalid link (missing token)", + "invalidLinkNoSubdomain": "Invalid link (missing subdomain)", + "invalidLinkNoEmail": "Invalid link (missing email)", + "invalidLinkNoHash": "Invalid link (missing hash)", "haveAlreadyAccount": "¿Ya tiene una cuenta?", "name": "Apellido(s)", "firstName": "Nombre(s)", diff --git a/src/I18n/languages/fr.json b/src/I18n/languages/fr.json index 972cd50b2..9fd601b20 100644 --- a/src/I18n/languages/fr.json +++ b/src/I18n/languages/fr.json @@ -113,7 +113,7 @@ "mandatoryTenant": "L'organisation est obligatoire", "mandatoryEndpointName": "Le nom est obligatoire", "mandatoryEndpointURL": "L'URL est obligatoire", - "unknownTenant": "Organisation inconnue!", + "unknownTenant": "Aucune organisation trouvée avec le sous-domain '{{tenantSubdomain}}'!", "mandatoryName": "Le Nom est obligatoire", "mandatoryFirstName": "Le Prénom est obligatoire", "mandatoryEmail": "L'Email est obligatoire", @@ -134,8 +134,7 @@ "accountAlreadyActive": "Votre compte a déjà été activé", "accountVerifiedSuccess": "Votre compte a été activé avec succès!", "accountPending": "Votre compte n'est pas activé! Vérifiez vos e-mails", - "activationTokenNotValid": "Le lien d'activation de votre compte n'est plus valide", - "activationEmailNotValid": "Cet Email n'existe plus", + "activationEmailNotValid": "Cet email n'existe pas dans l'organisation {{tenantName}}", "activationUnexpectedError": "Impossible d'activer le compte", "loginUnexpectedError": "Connexion impossible", "resetSuccess": "Requête acceptée ! Vérifiez vos e-mails", @@ -144,6 +143,7 @@ "forgotYourPassword": "Mot de passe oublié?", "retrievePassword": "Récupérez Mot de Passe", "resetPassword": "Réinitialiser Mot de Passe", + "createPassword": "Créer Mot de Passe", "backLogin": "Se Connecter", "required": "Obligatoire", "acceptEula": "J'accepte les ", @@ -154,6 +154,10 @@ "resetPasswordHashNotValid": "La requete n'est plus valide, refaites une demande de mot de passe", "resetPasswordUnexpectedError": "Impossible de réinitialiser le mot de passe", "verifyAccountTokenNotValid": "La requete n'est plus valide, refaites une demande d'activation de compte", + "invalidLinkNoToken": "Lien non valide (token manquant)", + "invalidLinkNoSubdomain": "Invalid link (sous-domaine manquant)", + "invalidLinkNoEmail": "Lien non valide (email manquant)", + "invalidLinkNoHash": "Lien non valide (hash) manquant)", "haveAlreadyAccount": "Avez-vous déjà un compte?", "name": "Nom", "firstName": "Prénom", diff --git a/src/I18n/languages/it.json b/src/I18n/languages/it.json index 49776960a..50c1c92aa 100644 --- a/src/I18n/languages/it.json +++ b/src/I18n/languages/it.json @@ -113,7 +113,7 @@ "mandatoryTenant": "L'organizzazione è obbligatoria", "mandatoryEndpointName": "Endpoint Name is mandatory", "mandatoryEndpointURL": "Endpoint URL is mandatory", - "unknownTenant": "Organizzazione Sconosciuta!", + "unknownTenant": "Organization with subdomain '{{tenantSubdomain}}' not found!", "mandatoryName": "Il Cognome è obbligatorio", "mandatoryFirstName": "Il Nome è obbligatorio", "mandatoryEmail": "L'email è obbligatoria", @@ -134,8 +134,7 @@ "accountAlreadyActive": "Il tuo account è già attivo", "accountVerifiedSuccess": "Il tuo account è stato attivato con successo!", "accountPending": "Il tuo account è in sospeso! Controlla la tua email", - "activationTokenNotValid": "Il link di attivazione del tuo account non è più valido", - "activationEmailNotValid": "Questa email non esiste", + "activationEmailNotValid": "This Email does not exist in organization {{tenantName}}", "activationUnexpectedError": "Non posso attivare l'account", "loginUnexpectedError": "Non riesco ad eseguire il login", "resetSuccess": "Richiesta accettata! Controlla la tua email", @@ -144,6 +143,7 @@ "forgotYourPassword": "Passowrd Dimenticata?", "retrievePassword": "Recupera Password", "resetPassword": "Resetta la Password", + "createPassword": "Create Password", "backLogin": "Indietro al login", "required": "Richiesto", "acceptEula": "Accetto l'", @@ -154,6 +154,10 @@ "resetPasswordUnexpectedError": "Non posso resettare la password", "resetPasswordHashNotValid": "Richiesta non più valida, richiedi nuovamente una password", "verifyAccountTokenNotValid": "Richiesta non più valida, richiedi una nuova validazione dell'account", + "invalidLinkNoToken": "Invalid link (missing token)", + "invalidLinkNoSubdomain": "Invalid link (missing subdomain)", + "invalidLinkNoEmail": "Invalid link (missing email)", + "invalidLinkNoHash": "Invalid link (missing hash)", "haveAlreadyAccount": "Hai già un'account?", "name": "Cognome", "firstName": "Nome", diff --git a/src/I18n/languages/pt.json b/src/I18n/languages/pt.json index 18b545586..912775cab 100644 --- a/src/I18n/languages/pt.json +++ b/src/I18n/languages/pt.json @@ -113,7 +113,7 @@ "mandatoryTenant": "A organização é obrigatório", "mandatoryEndpointName": "Endpoint Name is mandatory", "mandatoryEndpointURL": "Endpoint URL is mandatory", - "unknownTenant": "Organização Desconhecida!", + "unknownTenant": "Organization with subdomain '{{tenantSubdomain}}' not found!", "mandatoryName": "O nome é obrigatório", "mandatoryFirstName": "O primeiro nome é obrigatório", "mandatoryEmail": "O email é obrigatório", @@ -134,8 +134,7 @@ "accountAlreadyActive": "A sua conta já está ativa", "accountVerifiedSuccess": "A sua conta foi ativada com sucesso!", "accountPending": "A sua conta está pendendte! Por favor Verifique o seu email", - "activationTokenNotValid": "Oseu link de ativação de conta já não está válido", - "activationEmailNotValid": "Este emial não existe", + "activationEmailNotValid": "This Email does not exist in organization {{tenantName}}", "activationUnexpectedError": "A conta não pode ser ativada", "loginUnexpectedError": "Não consigo aceder", "resetSuccess": "Pedido aceite! Verifique o seu e-mail", @@ -144,6 +143,7 @@ "forgotYourPassword": "Esqueceu-se da password?", "retrievePassword": "Reativar Password", "resetPassword": "Redefinir Password", + "createPassword": "Create Password", "backLogin": "Voltar ao Login", "required": "Obrigatório", "acceptEula": "Eu aceito ", @@ -154,6 +154,10 @@ "resetPasswordUnexpectedError": "Não posso redefinir a password", "resetPasswordHashNotValid": "O pedido excedeu a validade, peça uma nova password", "verifyAccountTokenNotValid": "O pedido excedeu a validade, peça uma nova validação de conta", + "invalidLinkNoToken": "Invalid link (missing token)", + "invalidLinkNoSubdomain": "Invalid link (missing subdomain)", + "invalidLinkNoEmail": "Invalid link (missing email)", + "invalidLinkNoHash": "Invalid link (missing hash)", "haveAlreadyAccount": "Já tem uma conta?", "name": "Nome", "firstName": "Primeiro Nome", diff --git a/src/deeplinking/DeepLinkingManager.tsx b/src/deeplinking/DeepLinkingManager.tsx index d81822f8c..1687731fb 100644 --- a/src/deeplinking/DeepLinkingManager.tsx +++ b/src/deeplinking/DeepLinkingManager.tsx @@ -1,7 +1,7 @@ import { CommonActions, NavigationContainerRef } from '@react-navigation/native'; import { StatusCodes } from 'http-status-codes'; import I18n from 'i18n-js'; -import { EmitterSubscription, Linking } from 'react-native'; +import { Linking } from 'react-native'; import DeepLinking from 'react-native-deep-linking'; import CentralServerProvider from '../provider/CentralServerProvider'; @@ -10,11 +10,14 @@ import Constants from '../utils/Constants'; import Message from '../utils/Message'; import Utils from '../utils/Utils'; +let isInitialURLRead = false; + export default class DeepLinkingManager { private static instance: DeepLinkingManager; - private navigator: NavigationContainerRef; + private navigator: NavigationContainerRef; private centralServerProvider: CentralServerProvider; - private linkingSubscription: EmitterSubscription; + private url: string; + private readonly scheme: string = 'https://'; // eslint-disable-next-line no-useless-constructor private constructor() {} @@ -26,149 +29,188 @@ export default class DeepLinkingManager { return DeepLinkingManager.instance; } - public initialize(navigator: NavigationContainerRef, centralServerProvider: CentralServerProvider) { + public async initialize(navigator: NavigationContainerRef, centralServerProvider: CentralServerProvider): Promise { // Keep this.navigator = navigator; this.centralServerProvider = centralServerProvider; // Activate Deep Linking + DeepLinking.addScheme(this.scheme); + // Allow opening the app when link has been opened by the dashboard DeepLinking.addScheme('eMobility://'); - DeepLinking.addScheme('emobility://'); // Init Routes this.addResetPasswordRoute(); this.addVerifyAccountRoute(); // Init URL - Linking.getInitialURL() - .then((url) => { - if (url) { - Linking.openURL(url); + if (!isInitialURLRead) { + try { + const initialURL = await Linking.getInitialURL(); + isInitialURLRead = true; + Message.showError(initialURL); + console.log('initialURL ' + initialURL); + if ( initialURL ) { + await this.handleUrl({ url: initialURL }); } - }) - .catch((err) => { + } catch ( err ) { console.error('An error occurred', err); - }); + } + } } public startListening() { - this.linkingSubscription = Linking.addEventListener('url', this.handleUrl); + const linkingSubscription = Linking.addEventListener('url', this.handleUrl.bind(this)); + return () => { + linkingSubscription?.remove(); + }; } - public stopListening() { - this.linkingSubscription?.remove(); + public async handleUrl({ url }: { url: string }): Promise { + // const canOpenURl = await Linking.canOpenURL(url); + // if (canOpenURl) { + if (url) { + this.centralServerProvider.setAutoLoginDisabled(true); + this.url = url; + console.log('evaluating url'); + Message.showSuccess('Evaluating URL'); + DeepLinking.evaluateUrl(url); + } + //} } - public handleUrl = ({ url }: { url: string }) => { - Linking.canOpenURL(url).then((supported) => { - if (supported) { - DeepLinking.evaluateUrl(url); - } - }); - }; + private getTenantSubdomainFromURL(): string { + const regex = new RegExp('\\/\\/(.*?)\\.'); + const res = this.url?.match(regex); + return res?.[1]; + } - private addResetPasswordRoute = () => { - // Add Route - // eslint-disable-next-line @typescript-eslint/no-misused-promises - DeepLinking.addRoute('/resetPassword/:tenant/:hash', async (response: { tenant: string; hash: string }) => { - // Check params - if (!response.tenant) { - Message.showError(I18n.t('authentication.mandatoryTenant')); + private addResetPasswordRoute(): void { + // Handle reset password request + const definePasswordCallback = async (subdomain: string, params: Record) => { + if (!subdomain) { + Message.showError(I18n.t('authentication.invalidLinkNoSubdomain')); + return; } - // Get the Tenant - const tenant = await this.centralServerProvider.getTenant(response.tenant); + const tenant = await this.centralServerProvider.getTenant(subdomain); if (!tenant) { - Message.showError(I18n.t('authentication.unknownTenant')); + Message.showError(I18n.t('authentication.unknownTenant', {tenantSubdomain: subdomain})); + return; } - if (!response.hash) { - Message.showError(I18n.t('authentication.resetPasswordHashNotValid')); + const hash = params?.hash; + if (!hash) { + Message.showError(I18n.t('authentication.invalidLinkNoHash')); + return; } // Disable this.centralServerProvider.setAutoLoginDisabled(true); // Navigate - this.navigator.dispatch( - CommonActions.navigate({ - name: 'ResetPassword', - key: `${Utils.randomNumber()}`, - params: { tenantSubDomain: response.tenant, hash: response.hash } - }) - ); + // Message.showInfo('Navigating to reset password'); + this.navigator.navigate('ResetPassword', { + key: `${Utils.randomNumber()}`, + tenantSubDomain: subdomain, + hash, + load: true + }); + }; + // Add route to handle links from the dashboard + DeepLinking.addRoute('/resetPassword/:tenant/:hash', ({tenant, ...params}: {tenant: string; hash: string}) => { + void definePasswordCallback(tenant, params as Record); }); - }; + // Add route to handle links directly from the mails + DeepLinking.addRoute('/define-password:hash', () => { + const subdomain = this.getTenantSubdomainFromURL(); + const params = Utils.getURLParameters(this.url) as {hash: string}; + void definePasswordCallback(subdomain, params); + }); + } - private addVerifyAccountRoute = () => { - // Add Route - DeepLinking.addRoute( - '/verifyAccount/:tenant/:email/:token/:resetToken', - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async (response: { tenant: string; email: string; token: string; resetToken: string }) => { - // Check params - if (!response.tenant) { - Message.showError(I18n.t('authentication.mandatoryTenant')); - } - if (!response.email) { - Message.showError(I18n.t('authentication.mandatoryEmail')); - } - // Get the Tenant - const tenant = await this.centralServerProvider.getTenant(response.tenant); - if (!tenant) { - Message.showError(I18n.t('authentication.unknownTenant')); - } - if (!response.token) { - Message.showError(I18n.t('authentication.verifyAccountTokenNotValid')); - } - // Disable - this.centralServerProvider.setAutoLoginDisabled(true); - await this.centralServerProvider.logoff(); - // Navigate to login page - this.navigator.dispatch( - CommonActions.navigate({ - name: 'Login', - key: `${Utils.randomNumber()}`, - params: { tenantSubDomain: response.tenant, email: response.email } - }) - ); - // Call the backend - try { - // Validate Account - const result = await this.centralServerProvider.verifyEmail(response.tenant, response.email, response.token); - if (result.status === Constants.REST_RESPONSE_SUCCESS) { - Message.showSuccess(I18n.t('authentication.accountVerifiedSuccess')); - // Check if user has to change his password - if (response.resetToken && response.resetToken !== 'null') { - // Change password - this.navigator.dispatch( - CommonActions.navigate({ - name: 'ResetPassword', - key: `${Utils.randomNumber()}`, - params: { tenantSubDomain: response.tenant, hash: response.resetToken } - }) - ); - } + private addVerifyAccountRoute(): void { + // Handle verify account request + const verifyAccountCallback = async (subdomain: string, params: Record) => { + const tenant = await this.centralServerProvider.getTenant(subdomain); + const email = params?.Email; + const verificationToken = params?.VerificationToken; + const resetToken = params?.ResetToken; + // Check params + if (!email) { + Message.showError(I18n.t('authentication.invalidLinkNoEmail')); + return; + } + if (!subdomain) { + Message.showError(I18n.t('authentication.invalidLinkNoSubdomain')); + return; + } + if (!tenant) { + Message.showError(I18n.t('authentication.unknownTenant', {tenantSubdomain: subdomain})); + return; + } + if (!verificationToken) { + Message.showError(I18n.t('authentication.invalidLinkNoToken')); + return; + } + // Disable + this.centralServerProvider.setAutoLoginDisabled(true); + await this.centralServerProvider.logoff(); + // Navigate to login page + this.navigator.navigate('Login', { + key: `${Utils.randomNumber()}`, + tenantSubDomain: subdomain, + email + }); + // Call the backend + try { + // Validate Account + const result = await this.centralServerProvider.verifyEmail(subdomain, email, verificationToken); + if (result.status === Constants.REST_RESPONSE_SUCCESS) { + Message.showSuccess(I18n.t('authentication.accountVerifiedSuccess')); + // Check if user has to change his password + if (resetToken && resetToken !== 'null') { + // Change password + this.navigator.dispatch( + CommonActions.navigate({ + name: 'ResetPassword', + key: `${Utils.randomNumber()}`, + params: { tenantSubDomain: subdomain, hash: resetToken, email } + }) + ); } - } catch (error) { - // Check request? - if (error.request) { - // Show error - switch (error.request.status) { - // Account already active - case HTTPError.USER_ACCOUNT_ALREADY_ACTIVE_ERROR: - Message.showError(I18n.t('authentication.accountAlreadyActive')); - break; + } + } catch (error) { + // Check request? + if (error.request) { + // Show error + switch (error.request.status) { + // Account already active + case HTTPError.USER_ACCOUNT_ALREADY_ACTIVE_ERROR: + Message.showError(I18n.t('authentication.accountAlreadyActive')); + break; // VerificationToken no longer valid - case HTTPError.INVALID_TOKEN_ERROR: - Message.showError(I18n.t('authentication.activationTokenNotValid')); - break; + case HTTPError.INVALID_TOKEN_ERROR: + Message.showError(I18n.t('authentication.verifyAccountTokenNotValid')); + break; // Email does not exist - case StatusCodes.NOT_FOUND: - Message.showError(I18n.t('authentication.activationEmailNotValid')); - break; + case StatusCodes.NOT_FOUND: + Message.showError(I18n.t('authentication.activationEmailNotValid', {tenantName: tenant?.name})); + break; // Other common Error - default: - await Utils.handleHttpUnexpectedError(this.centralServerProvider, error, 'authentication.activationUnexpectedError'); - } - } else { - Message.showError(I18n.t('authentication.activationUnexpectedError')); + default: + await Utils.handleHttpUnexpectedError(this.centralServerProvider, error, 'authentication.activationUnexpectedError'); } + } else { + Message.showError(I18n.t('authentication.activationUnexpectedError')); } } + }; + // Add route to handle links from the dashboard + DeepLinking.addRoute( + '/verifyAccount/:tenant/:email/:token/:resetToken', + ({ tenant, ...params}: {tenant: string; email: string; token: string; resetToken: string}) => { + void verifyAccountCallback(tenant, params as Record); + }); + // Add route to handle links directly from the mails + DeepLinking.addRoute('/verify-email:params', () => { + const params = Utils.getURLParameters(this.url) as {Email: string; VerificationToken: string; ResetToken: string}; + const subdomain = this.getTenantSubdomainFromURL(); + void verifyAccountCallback(subdomain, params); + } ); - }; + } } diff --git a/src/screens/auth/create-password/CreatePassword.tsx b/src/screens/auth/create-password/CreatePassword.tsx new file mode 100644 index 000000000..109c387c6 --- /dev/null +++ b/src/screens/auth/create-password/CreatePassword.tsx @@ -0,0 +1,314 @@ +import { CommonActions } from '@react-navigation/native'; +import { StatusCodes } from 'http-status-codes'; +import I18n from 'i18n-js'; +import { Button, FormControl, Icon, Stack, Spinner } from 'native-base'; +import React from 'react'; +import { + Keyboard, + KeyboardAvoidingView, + ScrollView, + TextInput, + Text, + View, +} from 'react-native'; + +import computeFormStyleSheet from '../../../FormStyles'; +import BaseProps from '../../../types/BaseProps'; +import Message from '../../../utils/Message'; +import Utils from '../../../utils/Utils'; +import BaseScreen from '../../base-screen/BaseScreen'; +import AuthHeader from '../AuthHeader'; +import computeStyleSheet from '../AuthStyles'; +import { TenantConnection } from '../../../types/Tenant'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import { scale } from 'react-native-size-matters'; + +export interface Props extends BaseProps {} + +interface State { + tenantSubDomain?: string; + tenantName?: string; + tenantLogo?: string; + hash?: string; + password?: string; + repeatPassword?: string; + errorPassword?: Record[]; + errorRepeatPassword?: Record[]; + createPasswordLoading?: boolean; + contentLoading?: boolean; + hideRepeatPassword?: boolean; + hidePassword?: boolean; + email?: string; +} + +export default class CreatePassword extends BaseScreen { + public state: State; + public props: Props; + private repeatPasswordInput: TextInput; + private formValidationDef = { + password: { + presence: { + allowEmpty: false, + message: '^' + I18n.t('authentication.mandatoryPassword') + }, + equality: { + attribute: 'ghost', + message: '^' + I18n.t('authentication.passwordRule'), + comparator(password: string, ghost: string) { + // True if EULA is checked + return /(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!#@:;,<>\/''\$%\^&\*\.\?\-_\+\=\(\)])(?=.{8,})/.test(password); + } + } + }, + repeatPassword: { + presence: { + allowEmpty: false, + message: '^' + I18n.t('authentication.mandatoryPassword') + }, + equality: { + attribute: 'password', + message: '^' + I18n.t('authentication.passwordNotMatch') + } + } + }; + + public constructor(props: Props) { + super(props); + this.state = { + tenantSubDomain: Utils.getParamFromNavigation(this.props.route, 'tenantSubDomain', '') as string, + hash: Utils.getParamFromNavigation(this.props.route, 'hash', null) as string, + tenantName: '', + tenantLogo: null, + password: '', + repeatPassword: '', + createPasswordLoading: false, + hidePassword: true, + hideRepeatPassword: true, + contentLoading: true + }; + } + + // Enforce goBack to Login page as deeplinking is broken with react-navigation + public onBack(): boolean { + this.props.navigation.navigate('Login'); + return true; + } + + public setState = ( + state: State | ((prevState: Readonly, props: Readonly) => State | Pick) | Pick, + callback?: () => void + ) => { + super.setState(state, callback); + }; + + public async getTenantLogo(tenant: TenantConnection): Promise { + try { + if (tenant) { + const tenantLogo = await this.centralServerProvider.getTenantLogoBySubdomain(tenant); + return tenantLogo; + } + } catch (error) { + switch ( error?.request?.status ) { + case StatusCodes.NOT_FOUND: + return null; + default: + await Utils.handleHttpUnexpectedError( + this.centralServerProvider, + error, + null, + null, + null, + async (redirectedTenant: TenantConnection) => this.getTenantLogo(redirectedTenant) + ); + break; + } + } + return null; + } + + private async load(): Promise { + this.setState({contentLoading: true}, async () => { + const tenantSubDomain = Utils.getParamFromNavigation(this.props.route, 'tenantSubDomain', this.state.tenantSubDomain) as string; + const hash = Utils.getParamFromNavigation(this.props.route, 'hash', null) as string; + const tenant = await this.centralServerProvider.getTenant(tenantSubDomain); + const tenantLogo = await this.getTenantLogo(tenant); + this.setState({ tenantLogo, tenantSubDomain, tenantName: tenant?.name ?? '', hash, contentLoading: false}); + }); + } + + public async componentDidMount(): Promise { + // Call parent + await super.componentDidMount(); + // Init + await this.load(); + // Disable Auto Login + this.centralServerProvider.setAutoLoginDisabled(true); + } + + public async componentDidFocus(): Promise { + super.componentDidFocus(); + await this.load(); + } + + public async componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any) { + const load = Utils.getParamFromNavigation(this.props.route, 'load', false, true) as string; + if (load) { + this.load(); + } + } + + public resetPassword = async () => { + // Check field + const formIsValid = Utils.validateInput(this, this.formValidationDef); + if (formIsValid) { + const { tenantSubDomain, password, hash } = this.state; + try { + // Loading + this.setState({ createPasswordLoading: true }); + // Register + await this.centralServerProvider.resetPassword(tenantSubDomain, hash, password); + // Clear user's credentials + await this.centralServerProvider.clearUserPassword(tenantSubDomain); + // Reset + this.setState({ createPasswordLoading: false }); + // Show + Message.showSuccess(I18n.t('authentication.resetPasswordSuccess')); + // Navigate + this.props.navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [ + { + name: 'Login', + params: { + tenantSubDomain: this.state.tenantSubDomain + } + } + ] + }) + ); + } catch (error) { + // Reset + this.setState({ createPasswordLoading: false }); + // Check request? + if (error.request) { + // Show error + switch (error.request.status) { + // Invalid Hash + case StatusCodes.NOT_FOUND: + Message.showError(I18n.t('authentication.resetPasswordHashNotValid')); + break; + default: + // Other common Error + await Utils.handleHttpUnexpectedError(this.centralServerProvider, error, 'authentication.resetPasswordUnexpectedError', null, null, async () => this.resetPassword()); + } + } else { + Message.showError(I18n.t('authentication.resetPasswordUnexpectedError')); + } + } + } + }; + + public render() { + const style = computeStyleSheet(); + const formStyle = computeFormStyleSheet(); + const commonColor = Utils.getCurrentCommonColor(); + const { tenantName, createPasswordLoading, hidePassword, hideRepeatPassword, tenantLogo, contentLoading } = this.state; + // Get logo + return ( + + {contentLoading ? ( + + ) : ( + <> + + + + + + + this.repeatPasswordInput.focus()} + returnKeyType={'next'} + placeholder={I18n.t('authentication.password')} + placeholderTextColor={commonColor.placeholderTextColor} + style={formStyle.inputField} + autoCapitalize="none" + blurOnSubmit={false} + autoCorrect={false} + keyboardType={'default'} + onChangeText={(text) => this.setState({ password: text })} + secureTextEntry={hidePassword} + /> + this.setState({ hidePassword: !hidePassword })} + style={formStyle.inputIcon} + /> + + {this.state.errorPassword && + this.state.errorPassword.map((errorMessage, index) => ( + + {errorMessage} + + ))} + + + (this.repeatPasswordInput = ref)} + selectionColor={commonColor.textColor} + onSubmitEditing={() => Keyboard.dismiss()} + returnKeyType={'next'} + placeholder={I18n.t('authentication.repeatPassword')} + placeholderTextColor={commonColor.placeholderTextColor} + style={formStyle.inputField} + autoCapitalize="none" + blurOnSubmit={false} + autoCorrect={false} + keyboardType={'default'} + onChangeText={(text) => this.setState({ repeatPassword: text })} + secureTextEntry={hideRepeatPassword} + /> + this.setState({ hideRepeatPassword: !hideRepeatPassword })} + style={formStyle.inputIcon} + /> + + {this.state.errorRepeatPassword && + this.state.errorRepeatPassword.map((errorMessage, index) => ( + + {errorMessage} + + ))} + {createPasswordLoading ? ( + + ) : ( + + )} + + + + + + + + )} + + ); + } +} diff --git a/src/screens/auth/reset-password/ResetPassword.tsx b/src/screens/auth/reset-password/ResetPassword.tsx deleted file mode 100644 index b2401315f..000000000 --- a/src/screens/auth/reset-password/ResetPassword.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import { CommonActions } from '@react-navigation/native'; -import { StatusCodes } from 'http-status-codes'; -import I18n from 'i18n-js'; -import { Button, FormControl, Icon, Stack, Spinner } from 'native-base'; -import React from 'react'; -import { Keyboard, KeyboardAvoidingView, ScrollView, TextInput, Text, View } from 'react-native'; - -import computeFormStyleSheet from '../../../FormStyles'; -import BaseProps from '../../../types/BaseProps'; -import Message from '../../../utils/Message'; -import Utils from '../../../utils/Utils'; -import BaseScreen from '../../base-screen/BaseScreen'; -import AuthHeader from '../AuthHeader'; -import computeStyleSheet from '../AuthStyles'; -import { TenantConnection } from '../../../types/Tenant'; -import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; -import { scale } from 'react-native-size-matters'; - -export interface Props extends BaseProps {} - -interface State { - tenantSubDomain?: string; - tenantName?: string; - tenantLogo?: string; - hash?: string; - password?: string; - repeatPassword?: string; - errorPassword?: Record[]; - errorRepeatPassword?: Record[]; - loading?: boolean; - hideRepeatPassword?: boolean; - hidePassword?: boolean; -} - -export default class ResetPassword extends BaseScreen { - public state: State; - public props: Props; - private repeatPasswordInput: TextInput; - private formValidationDef = { - password: { - presence: { - allowEmpty: false, - message: '^' + I18n.t('authentication.mandatoryPassword') - }, - equality: { - attribute: 'ghost', - message: '^' + I18n.t('authentication.passwordRule'), - comparator(password: string, ghost: string) { - // True if EULA is checked - return /(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!#@:;,<>\/''\$%\^&\*\.\?\-_\+\=\(\)])(?=.{8,})/.test(password); - } - } - }, - repeatPassword: { - presence: { - allowEmpty: false, - message: '^' + I18n.t('authentication.mandatoryPassword') - }, - equality: { - attribute: 'password', - message: '^' + I18n.t('authentication.passwordNotMatch') - } - } - }; - - public constructor(props: Props) { - super(props); - this.state = { - tenantSubDomain: Utils.getParamFromNavigation(this.props.route, 'tenantSubDomain', '') as string, - hash: Utils.getParamFromNavigation(this.props.route, 'hash', null) as string, - tenantName: '', - tenantLogo: null, - password: '', - repeatPassword: '', - loading: false, - hidePassword: true, - hideRepeatPassword: true - }; - } - - public setState = ( - state: State | ((prevState: Readonly, props: Readonly) => State | Pick) | Pick, - callback?: () => void - ) => { - super.setState(state, callback); - }; - - public async setTenantLogo(tenant: TenantConnection): Promise { - try { - if (tenant) { - const tenantLogo = await this.centralServerProvider.getTenantLogoBySubdomain(tenant); - this.setState({tenantLogo}); - } - } catch (error) { - switch ( error?.request?.status ) { - case StatusCodes.NOT_FOUND: - return null; - default: - await Utils.handleHttpUnexpectedError( - this.centralServerProvider, - error, - null, - null, - null, - async (redirectedTenant: TenantConnection) => this.setTenantLogo(redirectedTenant) - ); - break; - } - } - return null; - } - - public async componentDidMount(): Promise { - // Call parent - await super.componentDidMount(); - // Init - const tenant = await this.centralServerProvider.getTenant(this.state.tenantSubDomain); - await this.setTenantLogo(tenant); - this.setState({ - tenantName: tenant ? tenant.name : '' - }); - // Disable Auto Login - this.centralServerProvider.setAutoLoginDisabled(true); - } - - public async componentDidFocus(): Promise { - super.componentDidFocus(); - const tenantSubdomain = Utils.getParamFromNavigation(this.props.route, 'tenantSubDomain', this.state.tenantSubDomain) as string; - const tenant = await this.centralServerProvider.getTenant(tenantSubdomain); - await this.setTenantLogo(tenant); - } - - public resetPassword = async () => { - // Check field - const formIsValid = Utils.validateInput(this, this.formValidationDef); - if (formIsValid) { - const { tenantSubDomain, password, hash } = this.state; - try { - // Loading - this.setState({ loading: true }); - // Register - await this.centralServerProvider.resetPassword(tenantSubDomain, hash, password); - // Clear user's credentials - await this.centralServerProvider.clearUserPassword(tenantSubDomain); - // Reset - this.setState({ loading: false }); - // Show - Message.showSuccess(I18n.t('authentication.resetPasswordSuccess')); - // Navigate - this.props.navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [ - { - name: 'Login', - params: { - tenantSubDomain: this.state.tenantSubDomain - } - } - ] - }) - ); - } catch (error) { - // Reset - this.setState({ loading: false }); - // Check request? - if (error.request) { - // Show error - switch (error.request.status) { - // Invalid Hash - case StatusCodes.NOT_FOUND: - Message.showError(I18n.t('authentication.resetPasswordHashNotValid')); - break; - default: - // Other common Error - await Utils.handleHttpUnexpectedError(this.centralServerProvider, error, 'authentication.resetPasswordUnexpectedError', null, null, async () => this.resetPassword()); - } - } else { - Message.showError(I18n.t('authentication.resetPasswordUnexpectedError')); - } - } - } - }; - - public render() { - const style = computeStyleSheet(); - const formStyle = computeFormStyleSheet(); - const commonColor = Utils.getCurrentCommonColor(); - const { tenantName, loading, hidePassword, hideRepeatPassword, tenantLogo } = this.state; - // Get logo - return ( - - - - - - - - this.repeatPasswordInput.focus()} - returnKeyType={'next'} - placeholder={I18n.t('authentication.password')} - placeholderTextColor={commonColor.placeholderTextColor} - style={formStyle.inputField} - autoCapitalize="none" - blurOnSubmit={false} - autoCorrect={false} - keyboardType={'default'} - onChangeText={(text) => this.setState({ password: text })} - secureTextEntry={hidePassword} - /> - this.setState({ hidePassword: !hidePassword })} - style={formStyle.inputIcon} - /> - - {this.state.errorPassword && - this.state.errorPassword.map((errorMessage, index) => ( - - {errorMessage} - - ))} - - - (this.repeatPasswordInput = ref)} - selectionColor={commonColor.textColor} - onSubmitEditing={() => Keyboard.dismiss()} - returnKeyType={'next'} - placeholder={I18n.t('authentication.repeatPassword')} - placeholderTextColor={commonColor.placeholderTextColor} - style={formStyle.inputField} - autoCapitalize="none" - blurOnSubmit={false} - autoCorrect={false} - keyboardType={'default'} - onChangeText={(text) => this.setState({ repeatPassword: text })} - secureTextEntry={hideRepeatPassword} - /> - this.setState({ hideRepeatPassword: !hideRepeatPassword })} - style={formStyle.inputIcon} - /> - - {this.state.errorRepeatPassword && - this.state.errorRepeatPassword.map((errorMessage, index) => ( - - {errorMessage} - - ))} - {loading ? ( - - ) : ( - - )} - - - - - - - - ); - } -} diff --git a/src/screens/auth/retrieve-password/RetrievePassword.tsx b/src/screens/auth/retrieve-password/RetrievePassword.tsx index 36f99fcdd..188c3a349 100644 --- a/src/screens/auth/retrieve-password/RetrievePassword.tsx +++ b/src/screens/auth/retrieve-password/RetrievePassword.tsx @@ -228,7 +228,7 @@ export default class RetrievePassword extends BaseScreen { {captchaSiteKey && captchaBaseUrl && !captcha && ( ; }; + public static getURLParameters(url: string): Record { + const res = {} as Record; + if ( url ) { + const params = url.split('?')?.[1]?.split('&'); + params?.forEach(param => { + const paramParts = param.split('='); + if ( paramParts.length === 2 ) { + res[paramParts[0]] = paramParts[1]; + } + }); + } + return res; + } + public static async checkForUpdate(): Promise { try { return await checkVersion();