diff --git a/android/app/build.gradle b/android/app/build.gradle index ba8609118d..9c991dfe8c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -239,6 +239,7 @@ configurations.all { dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + implementation project(':lottie-react-native') //noinspection GradleDynamicVersio implementation "com.facebook.react:react-native:+" // From node_modules diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index a851f50a63..671d70eff1 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -13,6 +13,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import com.airbnb.android.react.lottie.LottiePackage; import com.mattermost.helpers.RealPathUtil; import com.mattermost.share.ShareModule; import com.wix.reactnativenotifications.RNNotificationsPackage; @@ -61,6 +62,7 @@ public class MainApplication extends NavigationApplication implements INotificat // Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage()); packages.add(new RNNotificationsPackage(MainApplication.this)); + packages.add(new LottiePackage()); packages.add( diff --git a/android/settings.gradle b/android/settings.gradle index f01cb7e193..dd9a86a5f5 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'Mattermost' +include ':lottie-react-native' +project(':lottie-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/lottie-react-native/src/android') include ':reactnativenotifications' project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app') apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) diff --git a/app/actions/remote/session.ts b/app/actions/remote/session.ts index e7e73aadd9..4e6afe3eec 100644 --- a/app/actions/remote/session.ts +++ b/app/actions/remote/session.ts @@ -170,10 +170,8 @@ export const sendPasswordResetEmail = async (serverUrl: string, email: string) = let response; try { response = await client.sendPasswordResetEmail(email); - } catch (e) { - return { - error: e, - }; + } catch (error) { + return {error}; } return { data: response.data, diff --git a/app/components/channel_list/index.tsx b/app/components/channel_list/index.tsx index 77fe21c151..94a5ec5cac 100644 --- a/app/components/channel_list/index.tsx +++ b/app/components/channel_list/index.tsx @@ -15,7 +15,7 @@ import ChannelListHeader from './header'; import LoadingError from './loading_error'; import SearchField from './search'; -// import Loading from './loading'; +// import Loading from '@components/loading'; const channels: TempoChannel[] = [ {id: '1', name: 'Just a channel'}, diff --git a/app/components/channel_list/loading/index.tsx b/app/components/channel_list/loading/index.tsx deleted file mode 100644 index 67f7a68ab2..0000000000 --- a/app/components/channel_list/loading/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import LottieView from 'lottie-react-native'; -import React from 'react'; -import {StyleSheet, View} from 'react-native'; - -const Loading = () => ( - - - -); - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - padding: 20, - maxHeight: 40, - }, - lottie: { - height: 32, - width: 32, - }, -}); - -export default Loading; diff --git a/app/components/channel_list/loading/spinner.json b/app/components/channel_list/loading/spinner.json deleted file mode 100644 index 8fa3774f24..0000000000 --- a/app/components/channel_list/loading/spinner.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":40,"w":32,"h":32,"nm":"Spinner - white","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":40,"s":[360]}],"ix":10},"p":{"a":0,"k":[16,16,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[27,27],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gs","o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":3,"ix":10},"g":{"p":3,"k":{"a":0,"k":[0.001,1,1,1,0.5,1,1,1,1,1,1,1,0,1,0.5,0.5,1,0],"ix":8}},"s":{"a":0,"k":[2.423,11.896],"ix":4},"e":{"a":0,"k":[1.431,-12.387],"ix":5},"t":1,"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":13},"bm":0,"nm":"Gradient Stroke 1","mn":"ADBE Vector Graphic - G-Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0.05,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.95,-15.938],[-15.963,16],[-0.025,16],[-0.013,-15.938]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[27,27],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0.05,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1800,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/components/channel_list/loading_error/__snapshots__/index.test.tsx.snap b/app/components/channel_list/loading_error/__snapshots__/index.test.tsx.snap index a4e54968eb..c6df4de1db 100644 --- a/app/components/channel_list/loading_error/__snapshots__/index.test.tsx.snap +++ b/app/components/channel_list/loading_error/__snapshots__/index.test.tsx.snap @@ -107,7 +107,6 @@ exports[`Loading Error should match snapshot 1`] = ` "borderRadius": 4, "flex": 0, "justifyContent": "center", - "textAlignVertical": "center", }, Object { "height": 48, @@ -134,8 +133,8 @@ exports[`Loading Error should match snapshot 1`] = ` }, Object { "fontSize": 16, - "lineHeight": 18, - "marginTop": 2, + "lineHeight": 16, + "marginTop": 1, }, Object { "color": "#1c58d9", diff --git a/app/components/floating_text_input_label/index.tsx b/app/components/floating_text_input_label/index.tsx index fc3d5abfb3..140ec7a59b 100644 --- a/app/components/floating_text_input_label/index.tsx +++ b/app/components/floating_text_input_label/index.tsx @@ -87,6 +87,7 @@ const getLabelPositions = (style: TextStyle, labelStyle: TextStyle, smallLabelSt }; export type FloatingTextInputRef = { + blur: () => void; focus: () => void; isFocused: () => boolean; } @@ -135,6 +136,7 @@ const FloatingTextInput = forwardRef ({ + blur: () => inputRef.current?.blur(), focus: () => inputRef.current?.focus(), isFocused: () => inputRef.current?.isFocused() || false, }), [inputRef]); @@ -250,7 +252,7 @@ const FloatingTextInput = forwardRef - {error && ( + {Boolean(error) && ( {showErrorIcon && errorIcon && @@ -45,11 +48,8 @@ exports[`Loading should match snapshot 1`] = ` style={ Object { "aspectRatio": 1, - "bottom": 0, - "left": 0, - "position": "absolute", - "right": 0, - "top": 0, + "height": 32, + "width": 32, } } /> diff --git a/app/components/channel_list/loading/index.test.tsx b/app/components/loading/index.test.tsx similarity index 100% rename from app/components/channel_list/loading/index.test.tsx rename to app/components/loading/index.test.tsx diff --git a/app/components/loading/index.tsx b/app/components/loading/index.tsx index 4baed450d0..9caeb30a10 100644 --- a/app/components/loading/index.tsx +++ b/app/components/loading/index.tsx @@ -1,23 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import LottieView from 'lottie-react-native'; import React from 'react'; -import {ActivityIndicator, StyleSheet, View, ViewStyle} from 'react-native'; +import {StyleSheet, View, ViewStyle} from 'react-native'; type LoadingProps = { - color?: string; - size?: 'small' | 'large'; + containerStyle?: ViewStyle; style?: ViewStyle; } -const Loading = ({size = 'large', color = 'grey', style}: LoadingProps) => { +const Loading = ({containerStyle, style}: LoadingProps) => { return ( - - + ); @@ -27,11 +27,12 @@ const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', - alignItems: 'center', + padding: 20, + maxHeight: 40, }, - - loading: { - marginLeft: 3, + lottie: { + height: 32, + width: 32, }, }); diff --git a/app/components/loading/spinner.json b/app/components/loading/spinner.json new file mode 100644 index 0000000000..7a9d55e9e3 --- /dev/null +++ b/app/components/loading/spinner.json @@ -0,0 +1,501 @@ +{ + "v": "5.5.7", + "meta": { + "g": "LottieFiles AE 0.1.20", + "a": "", + "k": "", + "d": "", + "tc": "" + }, + "fr": 60, + "ip": 0, + "op": 40, + "w": 32, + "h": 32, + "nm": "Spinner - white", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Shape Layer 2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 0, + "s": [ + 0 + ] + }, + { + "t": 40, + "s": [ + 360 + ] + } + ], + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 16, + 16, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 27, + 27 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "gs", + "o": { + "a": 0, + "k": 100, + "ix": 9 + }, + "w": { + "a": 0, + "k": 3, + "ix": 10 + }, + "g": { + "p": 3, + "k": { + "a": 0, + "k": [ + 0.001, + 1, + 1, + 1, + 0.5, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0.5, + 0.5, + 1, + 0 + ], + "ix": 8 + } + }, + "s": { + "a": 0, + "k": [ + 2.423, + 11.896 + ], + "ix": 4 + }, + "e": { + "a": 0, + "k": [ + 1.431, + -12.387 + ], + "ix": 5 + }, + "t": 1, + "lc": 1, + "lj": 1, + "ml": 4, + "ml2": { + "a": 0, + "k": 4, + "ix": 13 + }, + "bm": 0, + "nm": "Gradient Stroke 1", + "mn": "ADBE Vector Graphic - G-Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0.05, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 1800, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Shape Layer 1", + "parent": 1, + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "hasMask": true, + "masksProperties": [ + { + "inv": false, + "mode": "a", + "pt": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -15.95, + -15.938 + ], + [ + -15.963, + 16 + ], + [ + -0.025, + 16 + ], + [ + -0.013, + -15.938 + ] + ], + "c": true + }, + "ix": 1 + }, + "o": { + "a": 0, + "k": 100, + "ix": 3 + }, + "x": { + "a": 0, + "k": 0, + "ix": 4 + }, + "nm": "Mask 1" + } + ], + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { + "a": 0, + "k": [ + 27, + 27 + ], + "ix": 2 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 3 + }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [ + 1, + 1, + 1, + 1 + ], + "ix": 3 + }, + "o": { + "a": 0, + "k": 100, + "ix": 4 + }, + "w": { + "a": 0, + "k": 3, + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0.05, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Ellipse 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 1800, + "st": 0, + "bm": 0 + } + ], + "markers": [] +} diff --git a/app/constants/screens.ts b/app/constants/screens.ts index 1fb0f0201b..4dffda7675 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -13,7 +13,6 @@ export const HOME = 'Home'; export const INTEGRATION_SELECTOR = 'IntegrationSelector'; export const IN_APP_NOTIFICATION = 'InAppNotification'; export const LOGIN = 'Login'; -export const LOGIN_OPTIONS = 'LoginOptions'; export const MAIN_SIDEBAR = 'MainSidebar'; export const MFA = 'MFA'; export const PERMALINK = 'Permalink'; @@ -36,7 +35,6 @@ export default { INTEGRATION_SELECTOR, IN_APP_NOTIFICATION, LOGIN, - LOGIN_OPTIONS, MAIN_SIDEBAR, MFA, PERMALINK, diff --git a/app/constants/sso.ts b/app/constants/sso.ts index d743bff642..a507c8d0dd 100644 --- a/app/constants/sso.ts +++ b/app/constants/sso.ts @@ -7,11 +7,11 @@ export const REDIRECT_URL_SCHEME = 'mmauth://'; export const REDIRECT_URL_SCHEME_DEV = 'mmauthbeta://'; const constants = keyMirror({ + SAML: null, GITLAB: null, GOOGLE: null, OFFICE365: null, OPENID: null, - SAML: null, }); export default { diff --git a/app/helpers/api/channel.ts b/app/helpers/api/channel.ts index 0c9298addf..5a29b63ffa 100644 --- a/app/helpers/api/channel.ts +++ b/app/helpers/api/channel.ts @@ -14,7 +14,7 @@ export function privateChannelJoinPrompt(displayName: string, intl: IntlShape): }), intl.formatMessage({ id: 'permalink.show_dialog_warn.description', - defaultMessage: 'You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?', + defaultMessage: 'You are about to join {channel} without explicitly being added by the Channel Admin. Are you sure you wish to join this private channel?', }, { channel: displayName, }), diff --git a/app/screens/server/background.tsx b/app/screens/background.tsx similarity index 100% rename from app/screens/server/background.tsx rename to app/screens/background.tsx diff --git a/app/screens/forgot_password/__snapshots__/forgot_password.test.tsx.snap b/app/screens/forgot_password/__snapshots__/forgot_password.test.tsx.snap deleted file mode 100644 index 686967a2ea..0000000000 --- a/app/screens/forgot_password/__snapshots__/forgot_password.test.tsx.snap +++ /dev/null @@ -1,152 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ForgotPassword should match snapshot 1`] = ` - - - - - - - - - To reset your password, enter the email address you used to sign up - - - - - Reset my password - - - - - -`; diff --git a/app/screens/forgot_password/forgot_password.test.tsx b/app/screens/forgot_password/forgot_password.test.tsx deleted file mode 100644 index c1510c36ae..0000000000 --- a/app/screens/forgot_password/forgot_password.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {act, waitFor} from '@testing-library/react-native'; -import React from 'react'; - -import * as SessionAPICalls from '@actions/remote/session'; -import {Preferences} from '@constants'; -import {renderWithIntl, fireEvent} from '@test/intl-test-helper'; - -import ForgotPassword from './index'; - -describe('ForgotPassword', () => { - const baseProps = { - componentId: 'ForgotPassword', - serverUrl: 'https://community.mattermost.com', - theme: Preferences.THEMES.denim, - }; - - test('should match snapshot', () => { - const {toJSON} = renderWithIntl(); - expect(toJSON()).toMatchSnapshot(); - }); - - test('Error on failure of email regex', async () => { - const {getByTestId} = renderWithIntl(); - const emailTextInput = getByTestId('forgot.password.email'); - const resetButton = getByTestId('forgot.password.button'); - - fireEvent.changeText(emailTextInput, 'bar'); - - act(() => { - fireEvent.press(resetButton); - }); - - expect(getByTestId('forgot.password.error.text')).toBeDefined(); - }); - - test('Should show password link sent texts', async () => { - const spyOnResetAPICall = jest.spyOn(SessionAPICalls, 'sendPasswordResetEmail'); - const {getByTestId} = renderWithIntl(); - const emailTextInput = getByTestId('forgot.password.email'); - const resetButton = getByTestId('forgot.password.button'); - - fireEvent.changeText(emailTextInput, 'test@test.com'); - - await waitFor(() => { - fireEvent.press(resetButton); - }); - - expect(spyOnResetAPICall).toHaveBeenCalled(); - }); -}); diff --git a/app/screens/forgot_password/inbox.svg b/app/screens/forgot_password/inbox.svg new file mode 100644 index 0000000000..1dc6f447f8 --- /dev/null +++ b/app/screens/forgot_password/inbox.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/screens/forgot_password/index.tsx b/app/screens/forgot_password/index.tsx index 67b11fb429..dbe6442bd2 100644 --- a/app/screens/forgot_password/index.tsx +++ b/app/screens/forgot_password/index.tsx @@ -1,36 +1,128 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; -import {Image, Text, TextInput, TouchableWithoutFeedback, View} from 'react-native'; +import {Keyboard, Platform, Text, useWindowDimensions, View} from 'react-native'; import Button from 'react-native-button'; +import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; +import {Navigation} from 'react-native-navigation'; +import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import {SafeAreaView} from 'react-native-safe-area-context'; import {sendPasswordResetEmail} from '@actions/remote/session'; -import ErrorText from '@components/error_text'; +import FloatingTextInput from '@components/floating_text_input_label'; import FormattedText from '@components/formatted_text'; +import {Screens} from '@constants'; +import {useIsTablet} from '@hooks/device'; +import Background from '@screens/background'; +import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; import {isEmail} from '@utils/helpers'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +// @ts-expect-error svg extension +import Inbox from './inbox.svg'; type Props = { serverUrl: string; theme: Theme; } +const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView); + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + centered: { + width: '100%', + maxWidth: 600, + }, + container: { + flex: 1, + justifyContent: 'center', + marginTop: Platform.select({android: 56}), + }, + error: { + marginTop: 64, + }, + flex: { + flex: 1, + }, + form: { + marginTop: 20, + }, + header: { + color: theme.mentionColor, + marginBottom: 12, + ...typography('Heading', 1000, 'SemiBold'), + }, + innerContainer: { + alignItems: 'center', + height: '100%', + justifyContent: 'center', + paddingHorizontal: 24, + }, + returnButton: { + marginTop: 32, + }, + subheader: { + color: changeOpacity(theme.centerChannelColor, 0.6), + marginBottom: 12, + ...typography('Body', 200, 'Regular'), + }, + successContainer: { + alignItems: 'center', + paddingHorizontal: 24, + justifyContent: 'center', + flex: 1, + }, + successText: { + color: changeOpacity(theme.centerChannelColor, 0.64), + ...typography('Body', 200, 'SemiBold'), + textAlign: 'center', + }, + successTitle: { + color: theme.mentionColor, + marginBottom: 12, + ...typography('Heading', 1000), + }, +})); + const ForgotPassword = ({serverUrl, theme}: Props) => { + const dimensions = useWindowDimensions(); + const translateX = useSharedValue(dimensions.width); + const isTablet = useIsTablet(); const [email, setEmail] = useState(''); const [error, setError] = useState(''); const [isPasswordLinkSent, setIsPasswordLinkSent] = useState(false); const {formatMessage} = useIntl(); - const emailIdRef = useRef(null); + const keyboardAwareRef = useRef(); const styles = getStyleSheet(theme); - const changeEmail = (emailAddress: string) => { + const changeEmail = useCallback((emailAddress: string) => { setEmail(emailAddress); - }; + setError(''); + }, []); - const submitResetPassword = async () => { + const onFocus = useCallback(() => { + if (Platform.OS === 'ios') { + let offsetY = 150; + if (isTablet) { + const {width, height} = dimensions; + const isLandscape = width > height; + offsetY = (isLandscape ? 230 : 150); + } + requestAnimationFrame(() => { + keyboardAwareRef.current?.scrollToPosition(0, offsetY); + }); + } + }, [dimensions]); + + const onReturn = useCallback(() => { + Navigation.popTo(Screens.LOGIN); + }, []); + + const submitResetPassword = useCallback(async () => { + Keyboard.dismiss(); if (!isEmail(email)) { setError( formatMessage({ @@ -41,184 +133,156 @@ const ForgotPassword = ({serverUrl, theme}: Props) => { return; } - const {data, error: apiError = undefined} = await sendPasswordResetEmail(serverUrl, email); + const {data} = await sendPasswordResetEmail(serverUrl, email); if (data) { setIsPasswordLinkSent(true); + return; } - setError(apiError as string); - }; - - const onBlur = useCallback(() => { - emailIdRef.current?.blur(); - }, []); - - const getDisplayErrorView = () => { - return ( - - ); - }; + setError(formatMessage({ + id: 'password_send.generic_error', + defaultMessage: 'We were unable to send you a reset password link. Please contact your System Admin for assistance.', + })); + }, [email]); const getCenterContent = () => { if (isPasswordLinkSent) { return ( + + - + {email} - + ); } return ( - - - - - + + + + + + + ); }; + const transform = useAnimatedStyle(() => { + const duration = Platform.OS === 'android' ? 250 : 350; + return { + transform: [{translateX: withTiming(translateX.value, {duration})}], + }; + }, []); + + useEffect(() => { + const listener = { + componentDidAppear: () => { + translateX.value = 0; + }, + componentDidDisappear: () => { + translateX.value = -dimensions.width; + }, + }; + const unsubscribe = Navigation.events().registerComponentListener(listener, Screens.FORGOT_PASSWORD); + + return () => unsubscribe.remove(); + }, [dimensions]); + return ( - - - - - {getDisplayErrorView()} - {getCenterContent()} - - - + + + + {getCenterContent()} + + ); }; -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - container: { - flex: 1, - }, - innerContainer: { - alignItems: 'center', - flexDirection: 'column', - justifyContent: 'center', - paddingHorizontal: 15, - paddingVertical: 50, - }, - forgotPasswordBtn: { - borderColor: 'transparent', - marginTop: 15, - }, - resetSuccessContainer: { - marginTop: 15, - padding: 10, - backgroundColor: '#dff0d8', - borderColor: '#d6e9c6', - }, - emailId: { - fontFamily: 'OpenSans-Semibold', - }, - successTxtColor: { - color: '#3c763d', - }, - defaultTopPadding: { - paddingTop: 15, - }, - subheader: { - textAlign: 'center', - fontSize: 16, - fontWeight: '300', - color: changeOpacity(theme.centerChannelColor, 0.6), - marginBottom: 15, - lineHeight: 22, - }, - inputBox: { - fontSize: 16, - height: 45, - borderColor: 'gainsboro', - borderWidth: 1, - marginTop: 5, - marginBottom: 5, - paddingLeft: 10, - alignSelf: 'stretch', - borderRadius: 3, - color: theme.centerChannelColor, - }, - signupButton: { - borderRadius: 3, - borderColor: theme.buttonBg, - borderWidth: 1, - alignItems: 'center', - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - signupButtonText: { - textAlign: 'center', - color: theme.buttonBg, - fontSize: 17, - }, - innerContainerImage: { - height: 72, - resizeMode: 'contain', - }, -})); - export default ForgotPassword; diff --git a/app/screens/index.tsx b/app/screens/index.tsx index 213ec6d90e..4702670b03 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -137,9 +137,6 @@ Navigation.setLazyComponentRegistrator((screenName) => { case Screens.LOGIN: screen = withIntl(require('@screens/login').default); break; - case Screens.LOGIN_OPTIONS: - screen = withIntl(require('@screens/login_options').default); - break; // case 'LongPost': // screen = require('@screens/long_post').default; // break; diff --git a/app/screens/login/__snapshots__/login.test.tsx.snap b/app/screens/login/__snapshots__/login.test.tsx.snap deleted file mode 100644 index 3aa66f944b..0000000000 --- a/app/screens/login/__snapshots__/login.test.tsx.snap +++ /dev/null @@ -1,213 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Login Login screen should match snapshot 1`] = ` - - - - - - - - - Sign in - - - - - I forgot my password - - - - - -`; diff --git a/app/screens/login/form.tsx b/app/screens/login/form.tsx new file mode 100644 index 0000000000..921bfc95b5 --- /dev/null +++ b/app/screens/login/form.tsx @@ -0,0 +1,396 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useManagedConfig} from '@mattermost/react-native-emm'; +import React, {MutableRefObject, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {Keyboard, Platform, TextInput, useWindowDimensions, View} from 'react-native'; +import Button from 'react-native-button'; +import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; + +import {login} from '@actions/remote/session'; +import ClientError from '@client/rest/error'; +import FloatingTextInput from '@components/floating_text_input_label'; +import FormattedText from '@components/formatted_text'; +import Loading from '@components/loading'; +import {FORGOT_PASSWORD, MFA} from '@constants/screens'; +import {useIsTablet} from '@hooks/device'; +import {t} from '@i18n'; +import {goToScreen, loginAnimationOptions, resetToHome} from '@screens/navigation'; +import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; +import {preventDoubleTap} from '@utils/tap'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +import type {LaunchProps} from '@typings/launch'; + +interface LoginProps extends LaunchProps { + config: Partial; + keyboardAwareRef: MutableRefObject; + license: Partial; + numberSSOs: number; + serverDisplayName: string; + theme: Theme; +} + +export const MFA_EXPECTED_ERRORS = ['mfa.validate_token.authenticate.app_error', 'ent.mfa.validate_token.authenticate.app_error']; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + container: { + marginBottom: 24, + }, + inputBoxEmail: { + marginTop: 16, + marginBottom: 5, + color: theme.centerChannelColor, + }, + inputBoxPassword: { + marginTop: 24, + marginBottom: 11, + color: theme.centerChannelColor, + }, + forgotPasswordBtn: { + borderColor: 'transparent', + }, + forgotPasswordError: { + marginTop: 10, + }, + forgotPasswordTxt: { + paddingVertical: 10, + color: theme.buttonBg, + fontSize: 14, + fontFamily: 'OpenSans-SemiBold', + }, + loadingContainerStyle: { + marginRight: 10, + padding: 0, + top: -2, + flex: undefined, + }, + loginButton: { + marginTop: 25, + }, + loading: { + height: 20, + width: 20, + }, +})); + +const LoginForm = ({config, extra, keyboardAwareRef, numberSSOs, serverDisplayName, launchError, launchType, license, serverUrl, theme}: LoginProps) => { + const styles = getStyleSheet(theme); + const isTablet = useIsTablet(); + const dimensions = useWindowDimensions(); + const loginRef = useRef(null); + const passwordRef = useRef(null); + const intl = useIntl(); + const managedConfig = useManagedConfig(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + const [loginId, setLoginId] = useState(''); + const [password, setPassword] = useState(''); + const [buttonDisabled, setButtonDisabled] = useState(true); + const emailEnabled = config.EnableSignInWithEmail === 'true'; + const usernameEnabled = config.EnableSignInWithUsername === 'true'; + const ldapEnabled = license.IsLicensed === 'true' && config.EnableLdap === 'true' && license.LDAP === 'true'; + + const focus = () => { + if (Platform.OS === 'ios') { + let ssoOffset = 0; + switch (numberSSOs) { + case 0: + ssoOffset = 0; + break; + case 1: + case 2: + ssoOffset = 48; + break; + default: + ssoOffset = 3 * 48; + break; + } + let offsetY = 150 - ssoOffset; + if (isTablet) { + const {width, height} = dimensions; + const isLandscape = width > height; + offsetY = (isLandscape ? 230 : 150) - ssoOffset; + } + requestAnimationFrame(() => { + keyboardAwareRef.current?.scrollToPosition(0, offsetY); + }); + } + }; + + const preSignIn = preventDoubleTap(async () => { + setIsLoading(true); + + Keyboard.dismiss(); + signIn(); + }); + + const signIn = async () => { + const result: LoginActionResponse = await login(serverUrl!, {serverDisplayName, loginId: loginId.toLowerCase(), password, config, license}); + if (checkLoginResponse(result)) { + if (!result.hasTeams && !result.error) { + // eslint-disable-next-line no-console + console.log('TODO: GO TO NO TEAMS'); + return; + } + goToHome(result.time || 0, result.error as never); + } + }; + + const goToHome = (time: number, loginError?: never) => { + const hasError = launchError || Boolean(loginError); + resetToHome({extra, launchError: hasError, launchType, serverUrl, time}); + }; + + const checkLoginResponse = (data: LoginActionResponse) => { + let errorId = ''; + const clientError = data.error as ClientErrorProps; + if (clientError && clientError.server_error_id) { + errorId = clientError.server_error_id; + } + + if (data.failed && MFA_EXPECTED_ERRORS.includes(errorId)) { + goToMfa(); + setIsLoading(false); + return false; + } + + if (data?.error && data.failed) { + setIsLoading(false); + setError(getLoginErrorMessage(data.error)); + return false; + } + + setIsLoading(false); + + return true; + }; + + const goToMfa = () => { + goToScreen(MFA, '', {goToHome, loginId, password, config, serverDisplayName, license, serverUrl, theme}, loginAnimationOptions()); + }; + + const getLoginErrorMessage = (loginError: string | ClientErrorProps | Error) => { + if (typeof loginError === 'string') { + return loginError; + } + + if (loginError instanceof ClientError) { + const errorId = loginError.server_error_id; + if (!errorId) { + return loginError.message; + } + + if (errorId === 'api.user.login.invalid_credentials_email_username') { + return intl.formatMessage({ + id: 'login.invalid_credentials', + defaultMessage: 'The email and password combination is incorrect', + }); + } + } + + return loginError.message; + }; + + const createLoginPlaceholder = () => { + const {formatMessage} = intl; + const loginPlaceholders = []; + + if (emailEnabled) { + loginPlaceholders.push(formatMessage({id: 'login.email', defaultMessage: 'Email'})); + } + + if (usernameEnabled) { + loginPlaceholders.push(formatMessage({id: 'login.username', defaultMessage: 'Username'})); + } + + if (ldapEnabled) { + if (config.LdapLoginFieldName) { + loginPlaceholders.push(config.LdapLoginFieldName); + } else { + loginPlaceholders.push(formatMessage({id: 'login.ldapUsername', defaultMessage: 'AD/LDAP Username'})); + } + } + + if (loginPlaceholders.length >= 2) { + return loginPlaceholders.slice(0, loginPlaceholders.length - 1).join(', ') + + ` ${formatMessage({id: 'login.or', defaultMessage: 'or'})} ` + + loginPlaceholders[loginPlaceholders.length - 1]; + } + + if (loginPlaceholders.length === 1) { + return loginPlaceholders[0]; + } + + return ''; + }; + + const focusPassword = useCallback(() => { + passwordRef?.current?.focus(); + }, []); + + const onBlur = useCallback(() => { + if (Platform.OS === 'ios') { + const reset = !passwordRef.current?.isFocused() && !loginRef.current?.isFocused(); + if (reset) { + keyboardAwareRef.current?.scrollToPosition(0, 0); + } + } + }, []); + + const onFocus = useCallback(() => { + focus(); + }, [dimensions]); + + const onLogin = useCallback(() => { + Keyboard.dismiss(); + preSignIn(); + }, [loginId, password]); + + const onLoginChange = useCallback((text) => { + setLoginId(text); + if (error) { + setError(undefined); + } + }, [error]); + + const onPasswordChange = useCallback((text) => { + setPassword(text); + if (error) { + setError(undefined); + } + }, [error]); + + const onPressForgotPassword = useCallback(() => { + const passProps = { + theme, + serverUrl, + }; + + goToScreen(FORGOT_PASSWORD, '', passProps, loginAnimationOptions()); + }, [theme]); + + // useEffect to set userName for EMM + useEffect(() => { + const setEmmUsernameIfAvailable = async () => { + if (managedConfig?.username && loginRef.current) { + loginRef.current.setNativeProps({text: managedConfig.username}); + setLoginId(managedConfig.username); + } + }; + + setEmmUsernameIfAvailable(); + }, []); + + useEffect(() => { + if (loginId && password) { + setButtonDisabled(false); + return; + } + setButtonDisabled(true); + }, [loginId, password]); + + const renderProceedButton = useMemo(() => { + const buttonType = buttonDisabled ? 'disabled' : 'default'; + const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType); + const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType); + + let buttonID = t('login.signIn'); + let buttonText = 'Log In'; + let buttonIcon; + + if (isLoading) { + buttonID = t('login.signingIn'); + buttonText = 'Logging In'; + buttonIcon = ( + + ); + } + + return ( + + ); + }, [buttonDisabled, isLoading, theme]); + + return ( + + + + + {(emailEnabled || usernameEnabled) && ( + + )} + {renderProceedButton} + + ); +}; + +export default LoginForm; diff --git a/app/screens/login/index.tsx b/app/screens/login/index.tsx index cccbc6b0a3..8e27af9c49 100644 --- a/app/screens/login/index.tsx +++ b/app/screens/login/index.tsx @@ -1,472 +1,219 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {useManagedConfig} from '@mattermost/react-native-emm'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {useIntl} from 'react-intl'; -import { - ActivityIndicator, - Image, - InteractionManager, - Keyboard, - SafeAreaView, - StyleProp, - Text, - TextInput, - TextStyle, - TouchableWithoutFeedback, - View, - ViewStyle, -} from 'react-native'; -import Button from 'react-native-button'; -import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; -import {NavigationFunctionComponent} from 'react-native-navigation'; -import {login} from '@actions/remote/session'; -import ErrorText from '@components/error_text'; +import React, {useEffect, useMemo, useRef} from 'react'; +import {Platform, useWindowDimensions, View} from 'react-native'; +import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; +import {Navigation} from 'react-native-navigation'; +import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import {SafeAreaView} from 'react-native-safe-area-context'; + import FormattedText from '@components/formatted_text'; -import {FORGOT_PASSWORD, MFA} from '@constants/screens'; -import {t} from '@i18n'; -import {goToScreen, resetToHome} from '@screens/navigation'; +import {Screens} from '@constants'; +import {useIsTablet} from '@hooks/device'; +import Background from '@screens/background'; +import {goToScreen, loginAnimationOptions} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +import Form from './form'; +import LoginOptionsSeparator from './login_options_separator'; +import SsoOptions from './sso_options'; import type {LaunchProps} from '@typings/launch'; -interface LoginProps extends LaunchProps { - componentId: string; +export interface LoginOptionsProps extends LaunchProps { config: ClientConfig; - serverDisplayName: string; + hasLoginForm: boolean; license: ClientLicense; + serverDisplayName: string; + serverUrl: string; + ssoOptions: Record; theme: Theme; } -export const MFA_EXPECTED_ERRORS = ['mfa.validate_token.authenticate.app_error', 'ent.mfa.validate_token.authenticate.app_error']; +const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({ + centered: { + width: '100%', + maxWidth: 600, + }, + container: { + flex: 1, + ...Platform.select({ + android: { + marginTop: 56, + }, + }), + }, + flex: { + flex: 1, + }, + header: { + color: theme.mentionColor, + marginBottom: 12, + ...typography('Heading', 1000, 'SemiBold'), + }, + innerContainer: { + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 24, + }, + subheader: { + color: changeOpacity(theme.centerChannelColor, 0.6), + marginBottom: 12, + ...typography('Body', 200, 'Regular'), + }, +})); -const Login: NavigationFunctionComponent = ({config, serverDisplayName, extra, launchError, launchType, license, serverUrl, theme}: LoginProps) => { - const styles = getStyleSheet(theme); +const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView); - const loginRef = useRef(null); - const passwordRef = useRef(null); - const scrollRef = useRef(null); - - const intl = useIntl(); - const managedConfig = useManagedConfig(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState | string | undefined | null>(); - - const [loginId, setLoginId] = useState(''); - const [password, setPassword] = useState(''); - - // useEffect to set userName for EMM - useEffect(() => { - const setEmmUsernameIfAvailable = async () => { - if (managedConfig?.username && loginRef.current) { - loginRef.current.setNativeProps({text: managedConfig.username}); - setLoginId(managedConfig.username); - } - }; - - setEmmUsernameIfAvailable(); - }, []); - - const preSignIn = preventDoubleTap(async () => { - setIsLoading(true); - setError(null); - - Keyboard.dismiss(); - InteractionManager.runAfterInteractions(async () => { - if (!loginId) { - t('login.noEmail'); - t('login.noEmailLdapUsername'); - t('login.noEmailUsername'); - t('login.noEmailUsernameLdapUsername'); - t('login.noLdapUsername'); - t('login.noUsername'); - t('login.noUsernameLdapUsername'); - - // it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements - let msgId = 'login.no'; - if (config.EnableSignInWithEmail === 'true') { - msgId += 'Email'; - } - if (config.EnableSignInWithUsername === 'true') { - msgId += 'Username'; - } - if (license.IsLicensed === 'true' && config.EnableLdap === 'true') { - msgId += 'LdapUsername'; - } - - const ldapUsername = intl.formatMessage({ - id: 'login.ldapUsernameLower', - defaultMessage: 'AD/LDAP username', - }); - - setIsLoading(false); - setError(intl.formatMessage( - { - id: msgId, - defaultMessage: '', - }, - { - ldapUsername: config.LdapLoginFieldName || ldapUsername, - }, - )); - return; - } - - if (!password) { - setIsLoading(false); - setError(intl.formatMessage({ - id: t('login.noPassword'), - defaultMessage: 'Please enter your password', - })); - - return; - } - - signIn(); - }); - }); - - const signIn = async () => { - const result: LoginActionResponse = await login(serverUrl!, {serverDisplayName, loginId: loginId.toLowerCase(), password, config, license}); - if (checkLoginResponse(result)) { - if (!result.hasTeams && !result.error) { - // eslint-disable-next-line no-console - console.log('GO TO NO TEAMS'); - return; - } - goToHome(result.time || 0, result.error as never); - } - }; - - const goToHome = (time: number, loginError?: never) => { - const hasError = launchError || Boolean(loginError); - resetToHome({extra, launchError: hasError, launchType, serverUrl, time}); - }; - - const checkLoginResponse = (data: LoginActionResponse) => { - let errorId = ''; - const clientError = data.error as ClientErrorProps; - if (clientError && clientError.server_error_id) { - errorId = clientError.server_error_id; - } - - if (data.failed && MFA_EXPECTED_ERRORS.includes(errorId)) { - goToMfa(); - setIsLoading(false); - return false; - } - - if (data?.error && data.failed) { - setIsLoading(false); - setError(getLoginErrorMessage(data.error)); - return false; - } - - setIsLoading(false); - - return true; - }; - - const goToMfa = () => { - const screen = MFA; - const title = intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'}); - goToScreen(screen, title, {goToHome, loginId, password, config, serverDisplayName, license, serverUrl, theme}); - }; - - const getLoginErrorMessage = (loginError: string | ClientErrorProps | Error) => { - if (typeof loginError === 'string') { - return loginError; - } - - return getServerErrorForLogin(loginError as ClientErrorProps); - }; - - const getServerErrorForLogin = (serverError?: ClientErrorProps) => { - if (!serverError) { - return null; - } - - const errorId = serverError.server_error_id; - - if (!errorId) { - return serverError.message; - } - - if (errorId === 'store.sql_user.get_for_login.app_error' || errorId === 'ent.ldap.do_login.user_not_registered.app_error') { - return { - intl: { - id: t('login.userNotFound'), - defaultMessage: 'We couldn\'t find an account matching your login credentials.', - }, - }; - } - - if (errorId === 'api.user.check_user_password.invalid.app_error' || errorId === 'ent.ldap.do_login.invalid_password.app_error') { - return { - intl: { - id: t('login.invalidPassword'), - defaultMessage: 'Your password is incorrect.', - }, - }; - } - - return serverError.message; - }; - - const onPressForgotPassword = () => { - const screen = FORGOT_PASSWORD; - const title = intl.formatMessage({id: 'password_form.title', defaultMessage: 'Password Reset'}); - const passProps = { - theme, - serverUrl, - }; - - goToScreen(screen, title, passProps); - }; - - const createLoginPlaceholder = () => { - const {formatMessage} = intl; - const loginPlaceholders = []; - - if (config.EnableSignInWithEmail === 'true') { - loginPlaceholders.push(formatMessage({id: 'login.email', defaultMessage: 'Email'})); - } - - if (config.EnableSignInWithUsername === 'true') { - loginPlaceholders.push(formatMessage({id: 'login.username', defaultMessage: 'Username'})); - } - - if (license.IsLicensed === 'true' && license.LDAP === 'true' && config.EnableLdap === 'true') { - if (config.LdapLoginFieldName) { - loginPlaceholders.push(config.LdapLoginFieldName); - } else { - loginPlaceholders.push(formatMessage({id: 'login.ldapUsername', defaultMessage: 'AD/LDAP Username'})); - } - } - - if (loginPlaceholders.length >= 2) { - return loginPlaceholders.slice(0, loginPlaceholders.length - 1).join(', ') + - ` ${formatMessage({id: 'login.or', defaultMessage: 'or'})} ` + - loginPlaceholders[loginPlaceholders.length - 1]; - } - - if (loginPlaceholders.length === 1) { - return loginPlaceholders[0]; - } - - return ''; - }; - - const onBlur = () => { - loginRef?.current?.blur(); - passwordRef?.current?.blur(); - Keyboard.dismiss(); - }; - - const onLoginChange = useCallback((text) => { - setLoginId(text); - }, []); - - const onPasswordChange = useCallback((text) => { - setPassword(text); - }, []); - - const onPasswordFocus = useCallback(() => { - passwordRef?.current?.focus(); - }, []); - - // **** **** **** RENDER METHOD **** **** **** - - const renderProceedButton = () => { - if (isLoading) { +const LoginOptions = ({config, extra, hasLoginForm, launchType, launchError, license, serverDisplayName, serverUrl, ssoOptions, theme}: LoginOptionsProps) => { + const styles = getStyles(theme); + const keyboardAwareRef = useRef(); + const dimensions = useWindowDimensions(); + const isTablet = useIsTablet(); + const translateX = useSharedValue(dimensions.width); + const numberSSOs = useMemo(() => { + return Object.values(ssoOptions).filter((v) => v).length; + }, [ssoOptions]); + const description = useMemo(() => { + if (hasLoginForm) { return ( - + ); + } else if (numberSSOs) { + return ( + ); } - const additionalStyle: StyleProp = { - ...(config.EmailLoginButtonColor && { - backgroundColor: config.EmailLoginButtonColor, - }), - ...(config.EmailLoginButtonBorderColor && { - borderColor: config.EmailLoginButtonBorderColor, - }), - }; - - const additionalTextStyle: StyleProp = { - ...(config.EmailLoginButtonTextColor && { - color: config.EmailLoginButtonTextColor, - }), - }; - return ( - + ); - }; + }, [hasLoginForm, numberSSOs, theme]); + + const goToSso = preventDoubleTap((ssoType: string) => { + goToScreen(Screens.SSO, '', {config, extra, launchError, launchType, license, theme, ssoType, serverDisplayName, serverUrl}, loginAnimationOptions()); + }); + + const optionsSeparator = hasLoginForm && Boolean(numberSSOs) && ( + + ); + + const transform = useAnimatedStyle(() => { + const duration = Platform.OS === 'android' ? 250 : 350; + return { + transform: [{translateX: withTiming(translateX.value, {duration})}], + }; + }, []); + + useEffect(() => { + const listener = { + componentDidAppear: () => { + translateX.value = 0; + }, + componentDidDisappear: () => { + translateX.value = -dimensions.width; + }, + }; + const unsubscribe = Navigation.events().registerComponentListener(listener, Screens.LOGIN); + + return () => unsubscribe.remove(); + }, [dimensions]); + + let additionalContainerStyle; + if (numberSSOs < 3 || !hasLoginForm || (isTablet && dimensions.height > dimensions.width)) { + additionalContainerStyle = styles.flex; + } + + let title; + if (hasLoginForm || numberSSOs > 0) { + title = ( + + ); + } else { + title = ( + + ); + } return ( - - + + + - - {config?.SiteName && ( - {config?.SiteName} - + {title} + {description} + {hasLoginForm && +
- )} - {error && ( - 0 && + - )} - - - {renderProceedButton()} - {(config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true') && ( - - )} + } + - - + + ); }; -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - container: { - flex: 1, - }, - innerContainer: { - alignItems: 'center', - flexDirection: 'column', - justifyContent: 'center', - paddingHorizontal: 15, - paddingVertical: 50, - }, - forgotPasswordBtn: { - borderColor: 'transparent', - marginTop: 15, - }, - forgotPasswordTxt: { - color: theme.linkColor, - }, - inputBox: { - fontSize: 16, - height: 45, - borderColor: 'gainsboro', - borderWidth: 1, - marginTop: 5, - marginBottom: 5, - paddingLeft: 10, - alignSelf: 'stretch', - borderRadius: 3, - color: theme.centerChannelColor, - }, - subheader: { - textAlign: 'center', - fontSize: 16, - fontWeight: '300', - color: changeOpacity(theme.centerChannelColor, 0.6), - marginBottom: 15, - lineHeight: 22, - }, - signupButton: { - borderRadius: 3, - borderColor: theme.buttonBg, - borderWidth: 1, - alignItems: 'center', - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - signupButtonText: { - textAlign: 'center', - color: theme.buttonBg, - fontSize: 17, - }, - header: { - color: theme.centerChannelColor, - textAlign: 'center', - marginTop: 15, - marginBottom: 15, - fontSize: 32, - fontFamily: 'OpenSans-Semibold', - }, -})); - -export default Login; +export default LoginOptions; diff --git a/app/screens/login/login.test.tsx b/app/screens/login/login.test.tsx deleted file mode 100644 index 16681767b6..0000000000 --- a/app/screens/login/login.test.tsx +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {Preferences, Screens} from '@constants'; -import * as NavigationActions from '@screens/navigation'; -import {waitFor, renderWithIntl, fireEvent} from '@test/intl-test-helper'; - -import Login from './index'; - -jest.mock('@actions/remote/session', () => { - return { - login: () => { - return { - data: undefined, - failed: true, - error: { - server_error_id: 'mfa.validate_token.authenticate.app_error', - }, - }; - }, - }; -}); - -describe('Login', () => { - const baseProps = { - componentId: Screens.LOGIN, - config: { - EnableSignInWithEmail: 'true', - EnableSignInWithUsername: 'true', - }, - license: { - IsLicensed: 'false', - }, - theme: Preferences.THEMES.denim, - serverUrl: 'https://locahost:8065', - }; - - test('Login screen should match snapshot', () => { - const {toJSON} = renderWithIntl( - , - ); - - expect(toJSON()).toMatchSnapshot(); - }); - - test('should show "I forgot my password" with only email login enabled', () => { - const props = { - ...baseProps, - config: { - ...baseProps.config, - EnableSignInWithUsername: 'false', - }, - }; - - const {getByTestId} = renderWithIntl(, {locale: 'es'}); - - expect(getByTestId('login.forgot')).toBeDefined(); - }); - - test('should show "I forgot my password" with only username login enabled', () => { - const props = { - ...baseProps, - config: { - ...baseProps.config, - EnableSignInWithEmail: 'false', - }, - }; - - const {getByTestId} = renderWithIntl(, {locale: 'fr'}); - - expect(getByTestId('login.forgot')).toBeDefined(); - }); - - test('should not show "I forgot my password" without email or username login enabled', () => { - const props = { - ...baseProps, - config: { - ...baseProps.config, - EnableSignInWithEmail: 'false', - EnableSignInWithUsername: 'false', - }, - }; - - const {getByTestId} = renderWithIntl(); - let forgot; - - try { - forgot = getByTestId('login.forgot'); - } catch { - // do nothing - } - - expect(forgot).toBeUndefined(); - }); - - test('should go to MFA screen when login response returns MFA error', async () => { - const goToScreen = jest.spyOn(NavigationActions, 'goToScreen'); - - const {getByTestId} = renderWithIntl(); - const loginInput = getByTestId('login.username.input'); - const passwordInput = getByTestId('login.password.input'); - const loginButton = getByTestId('login.signin.button'); - const loginId = 'user'; - const password = 'password'; - - fireEvent.changeText(loginInput, loginId); - fireEvent.changeText(passwordInput, password); - - await waitFor(() => fireEvent.press(loginButton), {timeout: 300}); - - expect(goToScreen). - toHaveBeenCalledWith( - 'MFA', - 'Multi-factor Authentication', - { - goToHome: expect.anything(), - loginId, - password, - config: {EnableSignInWithEmail: 'true', EnableSignInWithUsername: 'true'}, - license: {IsLicensed: 'false'}, - serverUrl: baseProps.serverUrl, - theme: baseProps.theme, - }, - ); - }); - - test('should go to ForgotPassword screen when forgotPassword is called', () => { - const goToScreen = jest.spyOn(NavigationActions, 'goToScreen'); - - const {getByTestId} = renderWithIntl(); - const forgot = getByTestId('login.forgot'); - - fireEvent.press(forgot); - - expect(goToScreen). - toHaveBeenCalledWith( - 'ForgotPassword', - 'Password Reset', - { - serverUrl: baseProps.serverUrl, - theme: baseProps.theme, - }, - ); - }); -}); diff --git a/app/screens/login/login_options_separator.tsx b/app/screens/login/login_options_separator.tsx new file mode 100644 index 0000000000..e5843b7493 --- /dev/null +++ b/app/screens/login/login_options_separator.tsx @@ -0,0 +1,53 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {View} from 'react-native'; + +import FormattedText from '@components/formatted_text'; +import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; + +type LoginOptionsSeparatorProps = { + theme: Theme; +} + +const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => ({ + container: { + flexDirection: 'row', + alignItems: 'center', + color: changeOpacity(theme.centerChannelColor, 0.64), + }, + line: { + flex: 1, + height: 0.4, + backgroundColor: changeOpacity(theme.centerChannelColor, 0.16), + }, + text: { + marginRight: 6, + marginLeft: 6, + textAlign: 'center', + color: changeOpacity(theme.centerChannelColor, 0.64), + fontFamily: 'OpenSans', + fontSize: 12, + top: -2, + }, +})); + +const LoginOptionsSeparator = ({theme}: LoginOptionsSeparatorProps) => { + const styles = getStyleFromTheme(theme); + + return ( + + + + + + ); +}; + +export default LoginOptionsSeparator; diff --git a/app/screens/login/sso_options.tsx b/app/screens/login/sso_options.tsx new file mode 100644 index 0000000000..12cab08715 --- /dev/null +++ b/app/screens/login/sso_options.tsx @@ -0,0 +1,172 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Image, ImageSourcePropType, View} from 'react-native'; +import Button from 'react-native-button'; + +import CompassIcon from '@components/compass_icon'; +import FormattedText from '@components/formatted_text'; +import {Sso} from '@constants'; +import {t} from '@i18n'; +import {buttonBackgroundStyle} from '@utils/buttonStyles'; +import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; + +type SsoInfo = { + defaultMessage: string; + id: string; + imageSrc?: ImageSourcePropType; + compassIcon?: string; +}; + +type Props = { + goToSso: (ssoType: string) => void; + ssoOnly: boolean; + ssoOptions: Record; + theme: Theme; +} + +const SsoOptions = ({goToSso, ssoOnly, ssoOptions, theme}: Props) => { + const styles = getStyleSheet(theme); + const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary'); + + const getSsoButtonOptions = ((ssoType: string): SsoInfo => { + const sso: SsoInfo = {} as SsoInfo; + switch (ssoType) { + case Sso.SAML: + sso.defaultMessage = 'SAML'; + sso.compassIcon = 'lock'; + sso.id = t('mobile.login_options.saml'); + break; + case Sso.GITLAB: + sso.defaultMessage = 'GitLab'; + sso.imageSrc = require('@assets/images/Icon_Gitlab.png'); + sso.id = t('mobile.login_options.gitlab'); + break; + case Sso.GOOGLE: + sso.defaultMessage = 'Google'; + sso.imageSrc = require('@assets/images/Icon_Google.png'); + sso.id = t('mobile.login_options.google'); + break; + case Sso.OFFICE365: + sso.defaultMessage = 'Office 365'; + sso.imageSrc = require('@assets/images/Icon_Office.png'); + sso.id = t('mobile.login_options.office365'); + break; + case Sso.OPENID: + sso.defaultMessage = 'Open ID'; + sso.id = t('mobile.login_options.openid'); + break; + + default: + } + return sso; + }); + + const enabledSSOs = Object.keys(ssoOptions).filter( + (ssoType: string) => ssoOptions[ssoType], + ); + + let styleViewContainer; + let styleButtonContainer; + if (enabledSSOs.length === 2 && !ssoOnly) { + styleViewContainer = styles.containerAsRow; + styleButtonContainer = styles.buttonContainer; + } + + const componentArray = []; + for (const ssoType of enabledSSOs) { + const {compassIcon, defaultMessage, id, imageSrc} = getSsoButtonOptions(ssoType); + const handlePress = () => { + goToSso(ssoType); + }; + + componentArray.push( + , + ); + } + + return ( + + {componentArray} + + ); +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + container: { + marginVertical: 24, + }, + containerAsRow: { + flexDirection: 'row', + alignItems: 'center', + }, + buttonContainer: { + width: '48%', + marginRight: 8, + }, + button: { + marginVertical: 4, + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: changeOpacity(theme.centerChannelColor, 0.16), + }, + buttonTextContainer: { + color: theme.centerChannelColor, + flexDirection: 'row', + marginLeft: 9, + }, + buttonText: { + color: theme.centerChannelColor, + fontFamily: 'OpenSans-SemiBold', + fontSize: 16, + lineHeight: 18, + top: 2, + }, + logoStyle: { + height: 18, + marginRight: 5, + width: 18, + }, +})); + +export default SsoOptions; diff --git a/app/screens/login_options/types.d.ts b/app/screens/login/types.d.ts similarity index 87% rename from app/screens/login_options/types.d.ts rename to app/screens/login/types.d.ts index 0e7fb5e809..66d254ba36 100644 --- a/app/screens/login_options/types.d.ts +++ b/app/screens/login/types.d.ts @@ -2,11 +2,12 @@ // See LICENSE.txt for license information. type LoginOptionWithConfigProps = { + ssoType?: string; config: ClientConfig; onPress: (type: string|GestureResponderEvent) => void | (() => void); theme: Theme; } type LoginOptionWithConfigAndLicenseProps = LoginOptionWithConfigProps & { - license: ClientLicense; + license?: ClientLicense; }; diff --git a/app/screens/login_options/email.tsx b/app/screens/login_options/email.tsx deleted file mode 100644 index f3f2fa4385..0000000000 --- a/app/screens/login_options/email.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {StyleProp, ViewStyle} from 'react-native'; -import Button from 'react-native-button'; - -import LocalConfig from '@assets/config.json'; -import FormattedText from '@components/formatted_text'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - -const EmailOption = ({config, onPress, theme}: LoginOptionWithConfigProps) => { - const styles = getStyleSheet(theme); - const forceHideFromLocal = LocalConfig.HideEmailLoginExperimental; - - if (!forceHideFromLocal && (config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true')) { - const backgroundColor = config.EmailLoginButtonColor || '#2389d7'; - const additionalStyle: StyleProp = { - backgroundColor, - }; - - if (config.EmailLoginButtonBorderColor) { - additionalStyle.borderColor = config.EmailLoginButtonBorderColor; - } - - const textColor = config.EmailLoginButtonTextColor || 'white'; - - return ( - - ); - } - - return null; -}; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ - button: { - borderRadius: 3, - borderColor: theme.buttonBg, - alignItems: 'center', - borderWidth: 1, - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - buttonText: { - textAlign: 'center', - color: theme.buttonBg, - fontSize: 17, - }, -})); - -export default EmailOption; diff --git a/app/screens/login_options/gitlab.tsx b/app/screens/login_options/gitlab.tsx deleted file mode 100644 index 8e36a77937..0000000000 --- a/app/screens/login_options/gitlab.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {Image, Text} from 'react-native'; -import Button from 'react-native-button'; - -import LocalConfig from '@assets/config.json'; -import {Sso} from '@constants'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - -const GitLabOption = ({config, onPress, theme}: LoginOptionWithConfigProps) => { - const styles = getStyleSheet(theme); - const forceHideFromLocal = LocalConfig.HideGitLabLoginExperimental; - - const handlePress = () => { - onPress(Sso.GITLAB); - }; - - if (!forceHideFromLocal && config.EnableSignUpWithGitLab === 'true') { - const additionalButtonStyle = { - backgroundColor: '#548', - borderColor: 'transparent', - borderWidth: 0, - }; - - const logoStyle = { - height: 18, - marginRight: 5, - width: 18, - }; - - const textColor = 'white'; - return ( - - ); - } - - return null; -}; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ - button: { - borderRadius: 3, - borderColor: theme.buttonBg, - alignItems: 'center', - borderWidth: 1, - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - buttonText: { - textAlign: 'center', - color: theme.buttonBg, - fontSize: 17, - }, -})); - -export default GitLabOption; diff --git a/app/screens/login_options/google.tsx b/app/screens/login_options/google.tsx deleted file mode 100644 index 8568417c4c..0000000000 --- a/app/screens/login_options/google.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {Image} from 'react-native'; -import Button from 'react-native-button'; - -import FormattedText from '@components/formatted_text'; -import {Sso} from '@constants'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - -const GoogleOption = ({config, onPress, theme}: LoginOptionWithConfigProps) => { - const styles = getStyleSheet(theme); - - const handlePress = () => { - onPress(Sso.GOOGLE); - }; - - if (config.EnableSignUpWithGoogle === 'true') { - const additionalButtonStyle = { - backgroundColor: '#c23321', - borderColor: 'transparent', - borderWidth: 0, - }; - - const logoStyle = { - height: 18, - marginRight: 5, - width: 18, - }; - - const textColor = 'white'; - return ( - - ); - } - - return null; -}; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ - button: { - borderRadius: 3, - borderColor: theme.buttonBg, - alignItems: 'center', - borderWidth: 1, - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - buttonText: { - textAlign: 'center', - color: theme.buttonBg, - fontSize: 17, - }, -})); - -export default GoogleOption; diff --git a/app/screens/login_options/index.tsx b/app/screens/login_options/index.tsx deleted file mode 100644 index 69cbbfd515..0000000000 --- a/app/screens/login_options/index.tsx +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {useIntl} from 'react-intl'; -import {Image, ScrollView, Text} from 'react-native'; -import {NavigationFunctionComponent} from 'react-native-navigation'; -import {SafeAreaView} from 'react-native-safe-area-context'; - -import FormattedText from '@components/formatted_text'; -import {LOGIN, SSO} from '@constants/screens'; -import {goToScreen} from '@screens/navigation'; -import {preventDoubleTap} from '@utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; - -import EmailOption from './email'; -import GitLabOption from './gitlab'; -import GoogleOption from './google'; -import LdapOption from './ldap'; -import Office365Option from './office365'; -import OpenIdOption from './open_id'; -import SamlOption from './saml'; - -import type {LaunchProps} from '@typings/launch'; - -interface LoginOptionsProps extends LaunchProps { - componentId: string; - serverUrl: string; - serverDisplayName: string; - config: ClientConfig; - license: ClientLicense; - theme: Theme; -} - -const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({ - container: { - flex: 1, - }, - header: { - color: theme.centerChannelColor, - textAlign: 'center', - marginTop: 15, - marginBottom: 15, - fontSize: 32, - fontFamily: 'OpenSans-Semibold', - }, - subheader: { - textAlign: 'center', - fontSize: 16, - fontWeight: '300', - color: changeOpacity(theme.centerChannelColor, 0.6), - marginBottom: 15, - lineHeight: 22, - }, - innerContainer: { - alignItems: 'center', - flexDirection: 'column', - justifyContent: 'center', - paddingHorizontal: 15, - flex: 1, - }, -})); - -const LoginOptions: NavigationFunctionComponent = ({config, extra, launchType, launchError, license, serverDisplayName, serverUrl, theme}: LoginOptionsProps) => { - const intl = useIntl(); - const styles = getStyles(theme); - - const displayLogin = preventDoubleTap(() => { - const screen = LOGIN; - const title = intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}); - - goToScreen(screen, title, {config, extra, launchError, launchType, license, serverDisplayName, serverUrl, theme}); - }); - - const displaySSO = preventDoubleTap((ssoType: string) => { - const screen = SSO; - const title = intl.formatMessage({id: 'mobile.routes.sso', defaultMessage: 'Single Sign-On'}); - goToScreen(screen, title, {config, extra, launchError, launchType, license, theme, ssoType, serverDisplayName, serverUrl}); - }); - - return ( - - - - - {config.SiteName} - - - - - - - - - - - - - ); -}; - -export default LoginOptions; diff --git a/app/screens/login_options/ldap.tsx b/app/screens/login_options/ldap.tsx deleted file mode 100644 index 0c2f0a8107..0000000000 --- a/app/screens/login_options/ldap.tsx +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {Text} from 'react-native'; -import Button from 'react-native-button'; - -import LocalConfig from '@assets/config.json'; -import FormattedText from '@components/formatted_text'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - -const LdapOption = ({config, license, onPress, theme}: LoginOptionWithConfigAndLicenseProps) => { - const styles = getStyleSheet(theme); - const forceHideFromLocal = LocalConfig.HideLDAPLoginExperimental; - - if (!forceHideFromLocal && license.IsLicensed === 'true' && config.EnableLdap === 'true') { - const backgroundColor = config.LdapLoginButtonColor || '#2389d7'; - const additionalButtonStyle = { - backgroundColor, - borderColor: 'transparent', - borderWidth: 1, - }; - - if (config.LdapLoginButtonBorderColor) { - additionalButtonStyle.borderColor = config.LdapLoginButtonBorderColor; - } - - const textColor = config.LdapLoginButtonTextColor || 'white'; - - let buttonText; - if (config.LdapLoginFieldName) { - buttonText = ( - - {config.LdapLoginFieldName} - - ); - } else { - buttonText = ( - - ); - } - - return ( - - ); - } - - return null; -}; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ - button: { - borderRadius: 3, - borderColor: theme.buttonBg, - alignItems: 'center', - borderWidth: 1, - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - buttonText: { - textAlign: 'center', - color: theme.buttonBg, - fontSize: 17, - }, -})); - -export default LdapOption; diff --git a/app/screens/login_options/office365.tsx b/app/screens/login_options/office365.tsx deleted file mode 100644 index 259ecd10ec..0000000000 --- a/app/screens/login_options/office365.tsx +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import Button from 'react-native-button'; - -import LocalConfig from '@assets/config.json'; -import FormattedText from '@components/formatted_text'; -import {Sso} from '@constants'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - -const Office365Option = ({config, license, onPress, theme}: LoginOptionWithConfigAndLicenseProps) => { - const styles = getStyleSheet(theme); - const forceHideFromLocal = LocalConfig.HideO365LoginExperimental; - const o365Enabled = config.EnableSignUpWithOffice365 === 'true' && license.IsLicensed === 'true' && - license.Office365OAuth === 'true'; - - const handlePress = () => { - onPress(Sso.OFFICE365); - }; - - if (!forceHideFromLocal && o365Enabled) { - const additionalButtonStyle = { - backgroundColor: '#2389d7', - borderColor: 'transparent', - borderWidth: 0, - }; - - const textColor = 'white'; - - return ( - - ); - } - - return null; -}; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ - button: { - borderRadius: 3, - borderColor: theme.buttonBg, - alignItems: 'center', - borderWidth: 1, - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - buttonText: { - textAlign: 'center', - color: theme.buttonBg, - fontSize: 17, - }, -})); - -export default Office365Option; diff --git a/app/screens/login_options/open_id.tsx b/app/screens/login_options/open_id.tsx deleted file mode 100644 index b25110844e..0000000000 --- a/app/screens/login_options/open_id.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import Button from 'react-native-button'; - -import FormattedText from '@components/formatted_text'; -import {Sso} from '@constants'; -import {isMinimumServerVersion} from '@utils/helpers'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - -const OpenIdOption = ({config, license, onPress, theme}: LoginOptionWithConfigAndLicenseProps) => { - const styles = getStyleSheet(theme); - const openIdEnabled = config.EnableSignUpWithOpenId === 'true' && license.IsLicensed === 'true' && isMinimumServerVersion(config.Version, 5, 33, 0); - - const handlePress = () => { - onPress(Sso.OPENID); - }; - - if (openIdEnabled) { - const additionalButtonStyle = { - backgroundColor: config.OpenIdButtonColor || '#145DBF', - borderColor: 'transparent', - borderWidth: 0, - }; - - const textColor = 'white'; - - return ( - - ); - } - - return null; -}; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ - button: { - borderRadius: 3, - borderColor: theme.buttonBg, - alignItems: 'center', - borderWidth: 1, - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - buttonText: { - textAlign: 'center', - color: theme.buttonBg, - fontSize: 17, - }, -})); - -export default OpenIdOption; diff --git a/app/screens/login_options/saml.tsx b/app/screens/login_options/saml.tsx deleted file mode 100644 index c799e97533..0000000000 --- a/app/screens/login_options/saml.tsx +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {Text} from 'react-native'; -import Button from 'react-native-button'; - -import LocalConfig from '@assets/config.json'; -import {Sso} from '@constants'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - -const SamlOption = ({config, license, onPress, theme}: LoginOptionWithConfigAndLicenseProps) => { - const styles = getStyleSheet(theme); - const forceHideFromLocal = LocalConfig.HideSAMLLoginExperimental; - const enabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true'; - - const handlePress = () => { - onPress(Sso.SAML); - }; - - if (!forceHideFromLocal && enabled) { - const backgroundColor = config.SamlLoginButtonColor || '#34a28b'; - - const additionalStyle = { - backgroundColor, - borderColor: 'transparent', - borderWidth: 0, - }; - - if (config.SamlLoginButtonBorderColor) { - additionalStyle.borderColor = config.SamlLoginButtonBorderColor; - } - - const textColor = config.SamlLoginButtonTextColor || 'white'; - - return ( - - ); - } - - return null; -}; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ - button: { - borderRadius: 3, - borderColor: theme.buttonBg, - alignItems: 'center', - borderWidth: 1, - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - buttonText: { - textAlign: 'center', - color: theme.buttonBg, - fontSize: 17, - }, -})); - -export default SamlOption; diff --git a/app/screens/mfa/__snapshots__/mfa.test.tsx.snap b/app/screens/mfa/__snapshots__/mfa.test.tsx.snap deleted file mode 100644 index 8c173a54a3..0000000000 --- a/app/screens/mfa/__snapshots__/mfa.test.tsx.snap +++ /dev/null @@ -1,172 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`*** MFA Screen *** MFA screen should match snapshot 1`] = ` - - - - - - - To complete the sign in process, please enter a token from your smartphone's authenticator - - - - - - - - - Proceed - - - - - -`; diff --git a/app/screens/mfa/index.tsx b/app/screens/mfa/index.tsx index 060e243d0f..045a57be43 100644 --- a/app/screens/mfa/index.tsx +++ b/app/screens/mfa/index.tsx @@ -3,27 +3,29 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; -import { - ActivityIndicator, - EventSubscription, - Image, - Keyboard, - KeyboardAvoidingView, - Platform, - TextInput, - TouchableWithoutFeedback, - View, -} from 'react-native'; +import {Keyboard, Platform, useWindowDimensions, View} from 'react-native'; import Button from 'react-native-button'; +import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; +import {Navigation} from 'react-native-navigation'; +import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import {SafeAreaView} from 'react-native-safe-area-context'; import {login} from '@actions/remote/session'; import ClientError from '@client/rest/error'; -import ErrorText from '@components/error_text'; +import FloatingTextInput from '@components/floating_text_input_label'; import FormattedText from '@components/formatted_text'; +import Loading from '@components/loading'; +import {Screens} from '@constants'; +import {useIsTablet} from '@hooks/device'; import {t} from '@i18n'; +import Background from '@screens/background'; +import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +// @ts-expect-error svg extension +import Shield from './mfa.svg'; type MFAProps = { config: Partial; @@ -36,38 +38,95 @@ type MFAProps = { theme: Theme; } +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + centered: { + width: '100%', + maxWidth: 600, + }, + container: { + flex: 1, + justifyContent: 'center', + marginTop: Platform.select({android: 56}), + }, + error: { + marginTop: 64, + }, + flex: { + flex: 1, + }, + form: { + marginTop: 20, + }, + header: { + color: theme.mentionColor, + marginBottom: 12, + ...typography('Heading', 1000, 'SemiBold'), + }, + innerContainer: { + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 24, + height: '100%', + }, + loading: { + height: 20, + width: 20, + }, + loadingContainerStyle: { + marginRight: 10, + padding: 0, + top: -2, + flex: undefined, + }, + proceedButton: { + marginTop: 32, + }, + shield: { + alignItems: 'center', + marginBottom: 56.22, + }, + subheader: { + color: changeOpacity(theme.centerChannelColor, 0.6), + marginBottom: 12, + ...typography('Body', 200, 'Regular'), + }, +})); + +const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView); + const MFA = ({config, goToHome, license, loginId, password, serverDisplayName, serverUrl, theme}: MFAProps) => { + const dimensions = useWindowDimensions(); + const translateX = useSharedValue(dimensions.width); + const isTablet = useIsTablet(); + const keyboardAwareRef = useRef(); const intl = useIntl(); const [token, setToken] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const {formatMessage} = useIntl(); - const textInputRef = useRef(null); const styles = getStyleSheet(theme); - const onBlur = useCallback(() => { - textInputRef?.current?.blur(); - }, []); - - useEffect(() => { - let listener: undefined | EventSubscription; - if (Platform.OS === 'android') { - listener = Keyboard.addListener('keyboardDidHide', onBlur); - } - return () => { - if (listener) { - listener.remove(); + const onFocus = useCallback(() => { + if (Platform.OS === 'ios') { + let offsetY = 150; + if (isTablet) { + const {width, height} = dimensions; + const isLandscape = width > height; + offsetY = (isLandscape ? 270 : 150); } - }; - }, []); + requestAnimationFrame(() => { + keyboardAwareRef.current?.scrollToPosition(0, offsetY); + }); + } + }, [dimensions]); - const handleInput = (userToken: string) => { + const handleInput = useCallback((userToken: string) => { setToken(userToken); setError(''); - }; + }, []); - const submit = preventDoubleTap(async () => { + const submit = useCallback(preventDoubleTap(async () => { Keyboard.dismiss(); if (!token) { setError( @@ -100,141 +159,110 @@ const MFA = ({config, goToHome, license, loginId, password, serverDisplayName, s return; } goToHome(result.time || 0, result.error as never); - }); + }), [token]); - const getProceedView = () => { - if (isLoading) { - return ( - - ); - } - return ( - - ); - }; + const transform = useAnimatedStyle(() => { + const duration = Platform.OS === 'android' ? 250 : 350; + return { + transform: [{translateX: withTiming(translateX.value, {duration})}], + }; + }, []); + + useEffect(() => { + const listener = { + componentDidAppear: () => { + translateX.value = 0; + }, + componentDidDisappear: () => { + translateX.value = -dimensions.width; + }, + }; + const unsubscribe = Navigation.events().registerComponentListener(listener, Screens.MFA); + + return () => unsubscribe.remove(); + }, [dimensions]); return ( - - + + - - - - - + + + + - - - {getProceedView()} + + + + - - - + + + ); }; -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - flex: { - flex: 1, - }, - signupButton: { - borderRadius: 3, - borderColor: theme.buttonBg, - borderWidth: 1, - alignItems: 'center', - alignSelf: 'stretch', - marginTop: 10, - padding: 15, - }, - inputBox: { - fontSize: 16, - height: 45, - borderColor: 'gainsboro', - borderWidth: 1, - marginTop: 5, - marginBottom: 5, - paddingLeft: 10, - alignSelf: 'stretch', - borderRadius: 3, - color: theme.centerChannelColor, - }, - header: { - color: theme.centerChannelColor, - textAlign: 'center', - marginTop: 15, - marginBottom: 15, - fontSize: 32, - fontFamily: 'OpenSans-Semibold', - }, - label: { - color: changeOpacity(theme.centerChannelColor, 0.6), - fontSize: 20, - }, - container: { - flex: 1, - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - }, - signupContainer: { - paddingRight: 15, - paddingLeft: 15, - }, - signupButtonText: { - color: theme.buttonBg, - textAlign: 'center', - fontSize: 17, - }, - containerImage: { - height: 72, - resizeMode: 'contain', - }, -})); - export default MFA; diff --git a/app/screens/mfa/mfa.svg b/app/screens/mfa/mfa.svg new file mode 100644 index 0000000000..0f0c6ca9e7 --- /dev/null +++ b/app/screens/mfa/mfa.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/screens/mfa/mfa.test.tsx b/app/screens/mfa/mfa.test.tsx deleted file mode 100644 index 26503aa408..0000000000 --- a/app/screens/mfa/mfa.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {fireEvent, waitFor} from '@testing-library/react-native'; -import React from 'react'; - -import {Preferences} from '@constants'; -import {renderWithIntl} from '@test/intl-test-helper'; - -import Mfa from './index'; - -jest.mock('@actions/remote/session', () => { - return { - login: jest.fn().mockResolvedValue({error: undefined, hasTeams: true}), - }; -}); - -describe('*** MFA Screen ***', () => { - const baseProps = { - config: {}, - goToHome: jest.fn(), - loginId: 'loginId', - password: 'passwd', - license: {}, - serverUrl: 'https://locahost:8065', - serverDisplayName: 'Test Server', - theme: Preferences.THEMES.denim, - }; - - test('MFA screen should match snapshot', () => { - const {toJSON} = renderWithIntl(); - expect(toJSON()).toMatchSnapshot(); - }); - - test('should call login method on submit', async () => { - const props = { - ...baseProps, - goToHome: jest.fn(), - }; - - const spyOnGoToHome = jest.spyOn(props, 'goToHome'); - const {getByTestId} = renderWithIntl(); - const submitBtn = getByTestId('login_mfa.submit'); - const inputText = getByTestId('login_mfa.input'); - fireEvent.changeText(inputText, 'token123'); - - await waitFor(() => { - fireEvent.press(submitBtn); - }); - - expect(spyOnGoToHome).toHaveBeenCalled(); - }); -}); diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index 9411ab9aaa..a9a453ead5 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -18,7 +18,57 @@ import type {LaunchProps} from '@typings/launch'; const {MattermostManaged} = NativeModules; const isRunningInSplitView = MattermostManaged.isRunningInSplitView; -const appearanceControlledScreens = [Screens.SERVER, Screens.LOGIN, Screens.LOGIN_OPTIONS, Screens.FORGOT_PASSWORD, Screens.MFA]; +const appearanceControlledScreens = [Screens.SERVER, Screens.LOGIN, Screens.FORGOT_PASSWORD, Screens.MFA]; + +const alpha = { + from: 0, + to: 1, + duration: 150, +}; + +export const loginAnimationOptions = () => { + const theme = getThemeFromState(); + return { + topBar: { + visible: true, + drawBehind: true, + translucid: true, + noBorder: true, + elevation: 0, + background: { + color: 'transparent', + }, + backButton: { + color: changeOpacity(theme.centerChannelColor, 0.56), + }, + scrollEdgeAppearance: { + active: true, + noBorder: true, + translucid: true, + }, + }, + animations: { + topBar: { + alpha, + }, + push: { + waitForRender: true, + content: { + alpha, + }, + }, + pop: { + content: { + alpha: { + from: 1, + to: 0, + duration: 100, + }, + }, + }, + }, + }; +}; Navigation.setDefaultOptions({ layout: { @@ -35,7 +85,7 @@ Appearance.addChangeListener(() => { for (const screen of screens) { if (appearanceControlledScreens.includes(screen)) { Navigation.updateProps(screen, {theme}); - setNavigatorStyles(screen, theme); + setNavigatorStyles(screen, theme, loginAnimationOptions(), theme.centerChannelBg); } } } @@ -95,7 +145,7 @@ export function resetToHome(passProps = {}) { export function resetToSelectServer(passProps: LaunchProps) { const theme = getThemeFromState(); - const isDark = tinyColor(theme.sidebarBg).isDark(); + const isDark = tinyColor(theme.centerChannelBg).isDark(); StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content'); EphemeralStore.clearNavigationComponents(); diff --git a/app/screens/server/form.tsx b/app/screens/server/form.tsx index bbb1aa267a..d3d49a1aa1 100644 --- a/app/screens/server/form.tsx +++ b/app/screens/server/form.tsx @@ -3,12 +3,13 @@ import React, {MutableRefObject, useCallback, useEffect, useRef} from 'react'; import {useIntl} from 'react-intl'; -import {ActivityIndicator, Platform, useWindowDimensions, View} from 'react-native'; +import {Keyboard, Platform, useWindowDimensions, View} from 'react-native'; import Button from 'react-native-button'; import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; import FloatingTextInput, {FloatingTextInputRef} from '@components/floating_text_input_label'; import FormattedText from '@components/formatted_text'; +import Loading from '@components/loading'; import {useIsTablet} from '@hooks/device'; import {t} from '@i18n'; import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; @@ -62,6 +63,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ connectingIndicator: { marginRight: 10, }, + loadingContainerStyle: { + marginRight: 10, + padding: 0, + top: -2, + flex: undefined, + }, + loading: { + height: 20, + width: 20, + }, })); const ServerForm = ({ @@ -99,11 +110,19 @@ const ServerForm = ({ }; const onBlur = useCallback(() => { - if (Platform.OS === 'ios' && isTablet && !urlRef.current?.isFocused() && !displayNameRef.current?.isFocused()) { - keyboardAwareRef.current?.scrollToPosition(0, 0); + if (Platform.OS === 'ios') { + const reset = !displayNameRef.current?.isFocused() && !urlRef.current?.isFocused(); + if (reset) { + keyboardAwareRef.current?.scrollToPosition(0, 0); + } } }, []); + const onConnect = useCallback(() => { + Keyboard.dismiss(); + handleConnect(); + }, [buttonDisabled, connecting]); + const onFocus = useCallback(() => { focus(); }, [dimensions]); @@ -122,8 +141,9 @@ const ServerForm = ({ } }, [dimensions, isTablet]); - let styleButtonText = buttonTextStyle(theme, 'lg', 'primary', 'default'); - let styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary'); + const buttonType = buttonDisabled ? 'disabled' : 'default'; + const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType); + const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType); let buttonID = t('mobile.components.select_server_view.connect'); let buttonText = 'Connect'; @@ -133,16 +153,11 @@ const ServerForm = ({ buttonID = t('mobile.components.select_server_view.connecting'); buttonText = 'Connecting'; buttonIcon = ( - ); - } else if (buttonDisabled) { - styleButtonText = buttonTextStyle(theme, 'lg', 'primary', 'disabled'); - styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', 'disabled'); } return ( @@ -150,6 +165,7 @@ const ServerForm = ({ {buttonIcon} diff --git a/app/screens/server/header.tsx b/app/screens/server/header.tsx index 7add7fee04..5951b55152 100644 --- a/app/screens/server/header.tsx +++ b/app/screens/server/header.tsx @@ -37,7 +37,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ }, description: { color: changeOpacity(theme.centerChannelColor, 0.64), - ...typography('Body', 100, 'Regular'), + ...typography('Body', 200, 'Regular'), }, })); diff --git a/app/screens/server/index.tsx b/app/screens/server/index.tsx index 37fa8b6fcf..5514d29e02 100644 --- a/app/screens/server/index.tsx +++ b/app/screens/server/index.tsx @@ -4,9 +4,10 @@ import {useManagedConfig, ManagedConfig} from '@mattermost/react-native-emm'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; -import {Alert, Platform, View} from 'react-native'; +import {Alert, Platform, useWindowDimensions, View} from 'react-native'; import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; import {Navigation} from 'react-native-navigation'; +import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import {SafeAreaView} from 'react-native-safe-area-context'; import {doPing} from '@actions/remote/general'; @@ -14,20 +15,18 @@ import {fetchConfigAndLicense} from '@actions/remote/systems'; import LocalConfig from '@assets/config.json'; import ClientError from '@client/rest/error'; import AppVersion from '@components/app_version'; -import {Screens} from '@constants'; +import {Screens, Sso} from '@constants'; import DatabaseManager from '@database/manager'; import {t} from '@i18n'; import NetworkManager from '@init/network_manager'; import {queryServerByDisplayName, queryServerByIdentifier} from '@queries/app/servers'; -import {goToScreen} from '@screens/navigation'; +import Background from '@screens/background'; +import {goToScreen, loginAnimationOptions} from '@screens/navigation'; import {DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch'; import {getErrorMessage} from '@utils/client_error'; -import {isMinimumServerVersion} from '@utils/helpers'; -import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {getServerUrlAfterRedirect, isValidUrl, sanitizeUrl} from '@utils/url'; -import Background from './background'; import ServerForm from './form'; import ServerHeader from './header'; @@ -43,9 +42,13 @@ const defaultServerUrlMessage = { defaultMessage: 'Please enter a valid server URL', }; +const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView); + const Server = ({componentId, extra, launchType, launchError, theme}: ServerProps) => { const intl = useIntl(); const managedConfig = useManagedConfig(); + const dimensions = useWindowDimensions(); + const translateX = useSharedValue(0); const keyboardAwareRef = useRef(); const [connecting, setConnecting] = useState(false); const [displayName, setDisplayName] = useState(''); @@ -103,62 +106,66 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp useEffect(() => { const listener = { componentDidAppear: () => { + translateX.value = 0; if (url) { NetworkManager.invalidateClient(url); } }, + componentDidDisappear: () => { + translateX.value = -dimensions.width; + }, }; const unsubscribe = Navigation.events().registerComponentListener(listener, componentId); return () => unsubscribe.remove(); - }, [componentId, url]); + }, [componentId, url, dimensions]); const displayLogin = (serverUrl: string, config: ClientConfig, license: ClientLicense) => { - const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true'; + const isLicensed = license.IsLicensed === 'true'; + const samlEnabled = config.EnableSaml === 'true' && isLicensed && license.SAML === 'true'; const gitlabEnabled = config.EnableSignUpWithGitLab === 'true'; - const googleEnabled = config.EnableSignUpWithGoogle === 'true' && license.IsLicensed === 'true'; - const o365Enabled = config.EnableSignUpWithOffice365 === 'true' && license.IsLicensed === 'true' && license.Office365OAuth === 'true'; - const openIdEnabled = config.EnableSignUpWithOpenId === 'true' && license.IsLicensed === 'true' && isMinimumServerVersion(config.Version, 5, 33, 0); - - let screen = Screens.LOGIN; - let title = formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}); - if (samlEnabled || gitlabEnabled || googleEnabled || o365Enabled || openIdEnabled) { - screen = Screens.LOGIN_OPTIONS; - title = formatMessage({id: 'mobile.routes.loginOptions', defaultMessage: 'Login Chooser'}); - } - - const {allowOtherServers} = managedConfig; - let visible = !LocalConfig.AutoSelectServerUrl; - - if (allowOtherServers === 'false') { - visible = false; - } + const googleEnabled = config.EnableSignUpWithGoogle === 'true' && isLicensed; + const o365Enabled = config.EnableSignUpWithOffice365 === 'true' && isLicensed && license.Office365OAuth === 'true'; + const openIdEnabled = config.EnableSignUpWithOpenId === 'true' && isLicensed; + const ldapEnabled = isLicensed && config.EnableLdap === 'true' && license.LDAP === 'true'; + const hasLoginForm = config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true' || ldapEnabled; + const ssoOptions: Record = { + [Sso.SAML]: samlEnabled, + [Sso.GITLAB]: gitlabEnabled, + [Sso.GOOGLE]: googleEnabled, + [Sso.OFFICE365]: o365Enabled, + [Sso.OPENID]: openIdEnabled, + }; + const enabledSSOs = Object.keys(ssoOptions).filter((key) => ssoOptions[key]); + const numberSSOs = enabledSSOs.length; const passProps = { config, - displayName, extra, + hasLoginForm, launchError, launchType, license, - theme, + serverDisplayName: displayName, serverUrl, + ssoOptions, + theme, }; - const defaultOptions = { - popGesture: visible, - topBar: { - visible, - height: visible ? null : 0, - }, - }; + const redirectSSO = !hasLoginForm && numberSSOs === 1; + const screen = redirectSSO ? Screens.SSO : Screens.LOGIN; + if (redirectSSO) { + // @ts-expect-error ssoType not in definition + passProps.ssoType = enabledSSOs[0]; + } - goToScreen(screen, title, passProps, defaultOptions); + goToScreen(screen, '', passProps, loginAnimationOptions()); setConnecting(false); + setButtonDisabled(false); setUrl(serverUrl); }; - const handleConnect = preventDoubleTap(async (manualUrl?: string) => { + const handleConnect = async (manualUrl?: string) => { if (buttonDisabled && !manualUrl) { return; } @@ -190,7 +197,7 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp } pingServer(serverUrl); - }); + }; const handleDisplayNameTextChanged = useCallback((text: string) => { setDisplayName(text); @@ -218,7 +225,6 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp let canceled = false; setConnecting(true); cancelPing = () => { - // We should not need this once we have the cancelable network-client library canceled = true; setConnecting(false); cancelPing = undefined; @@ -237,9 +243,9 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp pingServer(nurl, false); } else { setUrlError(getErrorMessage(result.error as ClientError, intl)); + setButtonDisabled(true); setConnecting(false); } - setButtonDisabled(true); return; } @@ -252,25 +258,33 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp } const server = await queryServerByIdentifier(DatabaseManager.appDatabase!.database, data.config!.DiagnosticId); + setConnecting(false); + if (server && server.lastActiveAt > 0) { setButtonDisabled(true); setUrlError(formatMessage({ id: 'mobile.server_identifier.exists', defaultMessage: 'You are already connected to this server.', })); - setConnecting(false); return; } displayLogin(serverUrl, data.config!, data.license!); }; + const transform = useAnimatedStyle(() => { + const duration = Platform.OS === 'android' ? 250 : 350; + return { + transform: [{translateX: withTiming(translateX.value, {duration})}], + }; + }, []); + return ( - - + ); }; @@ -317,9 +331,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ flex: 1, }, scrollContainer: { - justifyContent: 'center', alignItems: 'center', - flex: 1, + height: '100%', + justifyContent: 'center', }, })); diff --git a/app/screens/sso/index.tsx b/app/screens/sso/index.tsx index ef2cbc267d..3e302d7969 100644 --- a/app/screens/sso/index.tsx +++ b/app/screens/sso/index.tsx @@ -2,13 +2,17 @@ // See LICENSE.txt for license information. import {useManagedConfig} from '@mattermost/react-native-emm'; -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; +import {Platform, StyleSheet, useWindowDimensions, View} from 'react-native'; +import {Navigation} from 'react-native-navigation'; +import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import {SafeAreaView} from 'react-native-safe-area-context'; import {ssoLogin} from '@actions/remote/session'; import ClientError from '@client/rest/error'; -import {Sso} from '@constants'; +import {Screens, Sso} from '@constants'; +import Background from '@screens/background'; import {resetToHome} from '@screens/navigation'; -import {isMinimumServerVersion} from '@utils/helpers'; import SSOWithRedirectURL from './sso_with_redirect_url'; import SSOWithWebView from './sso_with_webview'; @@ -20,11 +24,22 @@ interface SSOProps extends LaunchProps { license: Partial; ssoType: string; serverDisplayName: string; - theme: Partial; + theme: Theme; } +const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView); + +const styles = StyleSheet.create({ + flex: { + flex: 1, + }, +}); + const SSO = ({config, extra, launchError, launchType, serverDisplayName, serverUrl, ssoType, theme}: SSOProps) => { const managedConfig = useManagedConfig(); + const inAppSessionAuth = managedConfig?.inAppSessionAuth === 'true'; + const dimensions = useWindowDimensions(); + const translateX = useSharedValue(inAppSessionAuth ? 0 : dimensions.width); const [loginError, setLoginError] = useState(''); let completeUrlPath = ''; @@ -92,7 +107,26 @@ const SSO = ({config, extra, launchError, launchType, serverDisplayName, serverU resetToHome({extra, launchError: hasError, launchType, serverUrl, time}); }; - const isSSOWithRedirectURLAvailable = isMinimumServerVersion(config.Version!, 5, 33, 0); + const transform = useAnimatedStyle(() => { + const duration = Platform.OS === 'android' ? 250 : 350; + return { + transform: [{translateX: withTiming(translateX.value, {duration})}], + }; + }, []); + + useEffect(() => { + const listener = { + componentDidAppear: () => { + translateX.value = 0; + }, + componentDidDisappear: () => { + translateX.value = -dimensions.width; + }, + }; + const unsubscribe = Navigation.events().registerComponentListener(listener, Screens.SSO); + + return () => unsubscribe.remove(); + }, [dimensions]); const props = { doSSOLogin, @@ -102,8 +136,9 @@ const SSO = ({config, extra, launchError, launchType, serverDisplayName, serverU theme, }; - if (!isSSOWithRedirectURLAvailable || managedConfig?.inAppSessionAuth === 'true') { - return ( + let ssoComponent; + if (inAppSessionAuth) { + ssoComponent = ( ); + } else { + ssoComponent = ( + + ); } + return ( - + + + + {ssoComponent} + + ); }; diff --git a/app/screens/sso/sso.test.tsx b/app/screens/sso/sso.test.tsx index c1872d697c..45d37d3c26 100644 --- a/app/screens/sso/sso.test.tsx +++ b/app/screens/sso/sso.test.tsx @@ -33,12 +33,6 @@ describe('SSO', () => { launchType: LaunchType.Normal, }; - test('implement with webview when version is less than 5.32 version', async () => { - const props = {...baseProps, config: {Version: '5.32.0'}}; - const {getByTestId} = renderWithIntl(); - expect(getByTestId('sso.webview')).toBeTruthy(); - }); - test('implement with OS browser & redirect url from version 5.33', async () => { const props = {...baseProps, config: {Version: '5.36.0'}}; const {getByTestId} = renderWithIntl(); diff --git a/app/screens/sso/sso_with_redirect_url.tsx b/app/screens/sso/sso_with_redirect_url.tsx index 173baee8c5..a52f5085b8 100644 --- a/app/screens/sso/sso_with_redirect_url.tsx +++ b/app/screens/sso/sso_with_redirect_url.tsx @@ -1,17 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. + import React, {useEffect, useState} from 'react'; import {useIntl} from 'react-intl'; -import {Linking, Platform, Text, TouchableOpacity, View} from 'react-native'; +import {Linking, Platform, Text, View} from 'react-native'; +import Button from 'react-native-button'; import DeviceInfo from 'react-native-device-info'; -import {SafeAreaView} from 'react-native-safe-area-context'; import urlParse from 'url-parse'; import FormattedText from '@components/formatted_text'; -import Loading from '@components/loading'; import {Sso} from '@constants'; import NetworkManager from '@init/network_manager'; +import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; import {tryOpenURL} from '@utils/url'; interface SSOWithRedirectURLProps { @@ -20,9 +22,40 @@ interface SSOWithRedirectURLProps { loginUrl: string; serverUrl: string; setLoginError: (value: string) => void; - theme: Partial; + theme: Theme; } +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + button: { + marginTop: 25, + }, + container: { + flex: 1, + paddingHorizontal: 24, + }, + errorText: { + color: changeOpacity(theme.centerChannelColor, 0.72), + textAlign: 'center', + ...typography('Body', 200, 'Regular'), + }, + infoContainer: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + }, + infoText: { + color: changeOpacity(theme.centerChannelColor, 0.72), + ...typography('Body', 100, 'Regular'), + }, + infoTitle: { + color: theme.mentionColor, + marginBottom: 4, + ...typography('Heading', 700), + }, + }; +}); + const SSOWithRedirectURL = ({doSSOLogin, loginError, loginUrl, serverUrl, setLoginError, theme}: SSOWithRedirectURLProps) => { const [error, setError] = useState(''); const style = getStyleSheet(theme); @@ -52,7 +85,7 @@ const SSOWithRedirectURL = ({doSSOLogin, loginError, loginUrl, serverUrl, setLog if (e && Platform.OS === 'android' && e?.message?.match(/no activity found to handle intent/i)) { message = intl.formatMessage({ id: 'mobile.oauth.failed_to_open_link_no_browser', - defaultMessage: 'The link failed to open. Please verify if a browser is installed in the current space.', + defaultMessage: 'The link failed to open. Please verify that a browser is installed on the device.', }); } else { message = intl.formatMessage({ @@ -88,88 +121,62 @@ const SSOWithRedirectURL = ({doSSOLogin, loginError, loginUrl, serverUrl, setLog }; const listener = Linking.addEventListener('url', onURLChange); - init(false); + + const timeout = setTimeout(() => { + init(false); + }, 1000); return () => { listener.remove(); + clearTimeout(timeout); }; }, []); return ( - {loginError || error ? ( - - - - {`${loginError || error}.`} - - - init()}> + + + + {`${loginError || error}.`} + + ) : ( + - )} - + ); }; -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { - return { - container: { - flex: 1, - }, - errorContainer: { - alignItems: 'center', - flex: 1, - marginTop: 40, - }, - errorTextContainer: { - marginBottom: 12, - }, - errorText: { - color: changeOpacity(theme.centerChannelColor, 0.6), - fontSize: 16, - lineHeight: 23, - textAlign: 'center', - }, - infoContainer: { - alignItems: 'center', - flex: 1, - justifyContent: 'center', - marginTop: 40, - }, - infoText: { - color: changeOpacity(theme.centerChannelColor, 0.6), - fontSize: 16, - lineHeight: 23, - marginBottom: 6, - }, - button: { - backgroundColor: theme.buttonBg, - color: theme.buttonColor, - fontSize: 16, - paddingHorizontal: 9, - paddingVertical: 9, - marginTop: 3, - }, - }; -}); - export default SSOWithRedirectURL; diff --git a/app/screens/sso/sso_with_webview.tsx b/app/screens/sso/sso_with_webview.tsx index 28674668f4..84c24d1a53 100644 --- a/app/screens/sso/sso_with_webview.tsx +++ b/app/screens/sso/sso_with_webview.tsx @@ -5,7 +5,6 @@ import CookieManager, {Cookie, Cookies} from '@react-native-cookies/cookies'; import React, {useEffect} from 'react'; import {useIntl} from 'react-intl'; import {Alert, Platform, Text, View} from 'react-native'; -import {SafeAreaView} from 'react-native-safe-area-context'; import {WebView} from 'react-native-webview'; import { WebViewErrorEvent, @@ -20,6 +19,16 @@ import {Sso} from '@constants'; import {popTopScreen} from '@screens/navigation'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +interface SSOWithWebViewProps { + completeUrlPath: string; + doSSOLogin: (bearerToken: string, csrfToken: string) => void; + loginError: string; + loginUrl: string; + serverUrl: string; + ssoType: string; + theme: Partial; +} + const HEADERS = { 'X-Mobile-App': 'mattermost', }; @@ -56,15 +65,25 @@ const oneLoginFormScalingJS = ` })(); `; -interface SSOWithWebViewProps { - completeUrlPath: string; - doSSOLogin: (bearerToken: string, csrfToken: string) => void; - loginError: string; - loginUrl: string; - serverUrl: string; - ssoType: string; - theme: Partial; -} +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + container: { + flex: 1, + marginTop: Platform.select({android: 56}), + }, + errorContainer: { + alignItems: 'center', + flex: 1, + marginTop: 40, + }, + errorText: { + color: changeOpacity(theme.centerChannelColor, 0.4), + fontSize: 16, + lineHeight: 23, + paddingHorizontal: 30, + }, + }; +}); const SSOWithWebView = ({completeUrlPath, doSSOLogin, loginError, loginUrl, serverUrl, ssoType, theme}: SSOWithWebViewProps) => { const style = getStyleSheet(theme); @@ -137,8 +156,8 @@ const SSOWithWebView = ({completeUrlPath, doSSOLogin, loginError, loginUrl, serv '', [{ text: intl.formatMessage({ - id: 'mobile.oauth.something_wrong.okButon', - defaultMessage: 'Ok', + id: 'mobile.oauth.something_wrong.okButton', + defaultMessage: 'OK', }), onPress: () => { popTopScreen(); @@ -227,7 +246,7 @@ const SSOWithWebView = ({completeUrlPath, doSSOLogin, loginError, loginUrl, serv }; return ( - @@ -236,27 +255,8 @@ const SSOWithWebView = ({completeUrlPath, doSSOLogin, loginError, loginUrl, serv {error || loginError} ) : renderWebView()} - + ); }; -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { - return { - container: { - flex: 1, - }, - errorContainer: { - alignItems: 'center', - flex: 1, - marginTop: 40, - }, - errorText: { - color: changeOpacity(theme.centerChannelColor, 0.4), - fontSize: 16, - lineHeight: 23, - paddingHorizontal: 30, - }, - }; -}); - export default SSOWithWebView; diff --git a/app/utils/buttonStyles.ts b/app/utils/buttonStyles.ts index dd760a683b..8fdc4ee801 100644 --- a/app/utils/buttonStyles.ts +++ b/app/utils/buttonStyles.ts @@ -44,7 +44,6 @@ export const buttonBackgroundStyle = ( flex: 0, alignItems: 'center', justifyContent: 'center', - textAlignVertical: 'center', borderRadius: 4, }, fullWidth: { @@ -422,8 +421,8 @@ export const buttonTextStyle = ( }, lg: { fontSize: 16, - lineHeight: 18, - marginTop: 2, + lineHeight: 16, + marginTop: 1, }, }); diff --git a/app/utils/theme/index.ts b/app/utils/theme/index.ts index b44b5ec6c9..4b34e2edd9 100644 --- a/app/utils/theme/index.ts +++ b/app/utils/theme/index.ts @@ -92,7 +92,7 @@ export function concatStyles(...styles: T[]) { return ([] as T[]).concat(...styles); } -export function setNavigatorStyles(componentId: string, theme: Theme) { +export function setNavigatorStyles(componentId: string, theme: Theme, additionalOptions: Options = {}, statusBarColor?: string) { const options: Options = { topBar: { title: { @@ -117,10 +117,15 @@ export function setNavigatorStyles(componentId: string, theme: Theme) { color: theme.sidebarHeaderTextColor, }; } - const isDark = tinyColor(theme.sidebarBg).isDark(); + const isDark = tinyColor(statusBarColor || theme.sidebarBg).isDark(); StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content'); - mergeNavigationOptions(componentId, options); + const mergeOptions = { + ...options, + ...additionalOptions, + }; + + mergeNavigationOptions(componentId, mergeOptions); } export function setNavigationStackStyles(theme: Theme) { diff --git a/assets/base/config.json b/assets/base/config.json index b5babdffc7..2212700d52 100644 --- a/assets/base/config.json +++ b/assets/base/config.json @@ -13,12 +13,6 @@ "MobileNoticeURL": "https://about.mattermost.com/mobile-notice-txt/", "RudderApiKey": "", - "HideEmailLoginExperimental": false, - "HideGitLabLoginExperimental": false, - "HideLDAPLoginExperimental": false, - "HideSAMLLoginExperimental": false, - "HideO365LoginExperimental": false, - "SentryEnabled": false, "SentryDsnIos": "", "SentryDsnAndroid": "", diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index e5d4a92b0a..a1c2ddd343 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -119,27 +119,18 @@ "last_users_message.others": "{numOthers} others ", "last_users_message.removed_from_channel.type": "were **removed from the channel**.", "last_users_message.removed_from_team.type": "were **removed from the team**.", - "login_mfa.enterToken": "To complete the sign in process, please enter a token from your smartphone's authenticator", - "login_mfa.token": "MFA Token", + "login_mfa.enterToken": "To complete the sign in process, please enter the code from your mobile device's authenticator app.", + "login_mfa.token": "Enter MFA Token", "login_mfa.tokenReq": "Please enter an MFA token", "login.email": "Email", - "login.forgot": "I forgot my password", - "login.invalidPassword": "Your password is incorrect.", + "login.forgot": "Forgot your password?", + "login.invalid_credentials": "The email and password combination is incorrect", "login.ldapUsername": "AD/LDAP Username", - "login.ldapUsernameLower": "AD/LDAP username", - "login.noEmail": "Please enter your email", - "login.noEmailLdapUsername": "Please enter your email or {ldapUsername}", - "login.noEmailUsername": "Please enter your email or username", - "login.noEmailUsernameLdapUsername": "Please enter your email, username or {ldapUsername}", - "login.noLdapUsername": "Please enter your {ldapUsername}", - "login.noPassword": "Please enter your password", - "login.noUsername": "Please enter your username", - "login.noUsernameLdapUsername": "Please enter your username or {ldapUsername}", "login.or": "or", "login.password": "Password", - "login.signIn": "Sign in", + "login.signIn": "Log In", + "login.signingIn": "Logging In", "login.username": "Username", - "login.userNotFound": "We couldn't find an account matching your login credentials.", "mobile.about.appVersion": "App Version: {version} (Build {number})", "mobile.about.copyright": "Copyright 2015-{currentYear} Mattermost, Inc. All rights reserved", "mobile.about.database": "Database: {type}", @@ -177,7 +168,18 @@ "mobile.join_channel.error": "We couldn't join the channel {displayName}. Please check your connection and try again.", "mobile.link.error.text": "Unable to open the link.", "mobile.link.error.title": "Error", - "mobile.login_options.choose_title": "Choose your login method", + "mobile.login_options.cant_heading": "Can't Log In", + "mobile.login_options.enter_credentials": "Enter your login details below.", + "mobile.login_options.gitlab": "GitLab", + "mobile.login_options.google": "Google", + "mobile.login_options.heading": "Log In to Your Account", + "mobile.login_options.none": "You can't log in to your account yet. At least one login option must be configured. Contact your System Admin for assistance.", + "mobile.login_options.office365": "Office 365", + "mobile.login_options.openid": "Open ID", + "mobile.login_options.saml": "SAML", + "mobile.login_options.select_option": "Select a login option below.", + "mobile.login_options.separator_text": "or log in with", + "mobile.login_options.sso_continue": "Continue with", "mobile.managed.blocked_by": "Blocked by {vendor}", "mobile.managed.exit": "Exit", "mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.", @@ -196,10 +198,12 @@ "mobile.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.", "mobile.oauth.failed_to_login": "Your login attempt failed. Please try again.", "mobile.oauth.failed_to_open_link": "The link failed to open. Please try again.", - "mobile.oauth.failed_to_open_link_no_browser": "The link failed to open. Please verify if a browser is installed in the current space.", + "mobile.oauth.failed_to_open_link_no_browser": "The link failed to open. Please verify that a browser is installed on the device.", "mobile.oauth.something_wrong": "Something went wrong", - "mobile.oauth.something_wrong.okButon": "Ok", - "mobile.oauth.switch_to_browser": "Please use your browser to complete the login", + "mobile.oauth.something_wrong.okButton": "OK", + "mobile.oauth.switch_to_browser": "You are being redirected to your login provider", + "mobile.oauth.switch_to_browser.error_title": "Sign in error", + "mobile.oauth.switch_to_browser.title": "Redirecting...", "mobile.oauth.try_again": "Try again", "mobile.post_info.add_reaction": "Add Reaction", "mobile.post_pre_header.flagged": "Saved", @@ -221,10 +225,6 @@ "mobile.routes.code": "{language} Code", "mobile.routes.code.noLanguage": "Code", "mobile.routes.custom_status": "Set a Status", - "mobile.routes.login": "Login", - "mobile.routes.loginOptions": "Login Chooser", - "mobile.routes.mfa": "Multi-factor Authentication", - "mobile.routes.sso": "Single Sign-On", "mobile.routes.table": "Table", "mobile.routes.user_profile": "Profile", "mobile.server_identifier.exists": "You are already connected to this server.", @@ -270,12 +270,13 @@ "notification.message_not_found": "Message not found", "notification.not_channel_member": "This message belongs to a channel where you are not a member.", "notification.not_team_member": "This message belongs to a team where you are not a member.", - "password_form.title": "Password Reset", - "password_send.checkInbox": "Please check your inbox.", "password_send.description": "To reset your password, enter the email address you used to sign up", "password_send.error": "Please enter a valid email address.", + "password_send.generic_error": "We were unable to send you a reset password link. Please contact your System Admin for assistance.", "password_send.link": "If the account exists, a password reset email will be sent to:", - "password_send.reset": "Reset my password", + "password_send.link.title": "Reset Link Sent", + "password_send.reset": "Reset Your Password", + "password_send.return": "Return to Log In", "permalink.show_dialog_warn.cancel": "Cancel", "permalink.show_dialog_warn.description": "You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?", "permalink.show_dialog_warn.join": "Join", @@ -297,15 +298,11 @@ "posts_view.newMsg": "New Messages", "screen.search.placeholder": "Search messages & files", "search_bar.search": "Search", - "signup.email": "Email and Password", - "signup.google": "Google Apps", - "signup.office365": "Office 365", "status_dropdown.set_away": "Away", "status_dropdown.set_dnd": "Do Not Disturb", "status_dropdown.set_offline": "Offline", "status_dropdown.set_online": "Online", "status_dropdown.set_ooo": "Out Of Office", "team_list.no_other_teams.description": "To join another team, ask a Team Admin for an invitation, or create your own team.", - "team_list.no_other_teams.title": "No additional teams to join", - "web.root.signup_info": "All team communication in one place, searchable and accessible anywhere" + "team_list.no_other_teams.title": "No additional teams to join" } diff --git a/assets/base/images/Icon_Gitlab.png b/assets/base/images/Icon_Gitlab.png new file mode 100644 index 0000000000..93d20de1bd Binary files /dev/null and b/assets/base/images/Icon_Gitlab.png differ diff --git a/assets/base/images/Icon_Gitlab@2x.png b/assets/base/images/Icon_Gitlab@2x.png new file mode 100644 index 0000000000..4054f696de Binary files /dev/null and b/assets/base/images/Icon_Gitlab@2x.png differ diff --git a/assets/base/images/Icon_Gitlab@3x.png b/assets/base/images/Icon_Gitlab@3x.png new file mode 100644 index 0000000000..48c1015f7b Binary files /dev/null and b/assets/base/images/Icon_Gitlab@3x.png differ diff --git a/assets/base/images/Icon_Google.png b/assets/base/images/Icon_Google.png new file mode 100644 index 0000000000..1138bfa135 Binary files /dev/null and b/assets/base/images/Icon_Google.png differ diff --git a/assets/base/images/Icon_Google@2x.png b/assets/base/images/Icon_Google@2x.png new file mode 100644 index 0000000000..da77d5c772 Binary files /dev/null and b/assets/base/images/Icon_Google@2x.png differ diff --git a/assets/base/images/Icon_Google@3x.png b/assets/base/images/Icon_Google@3x.png new file mode 100644 index 0000000000..b3349cfff0 Binary files /dev/null and b/assets/base/images/Icon_Google@3x.png differ diff --git a/assets/base/images/Icon_Office.png b/assets/base/images/Icon_Office.png new file mode 100644 index 0000000000..f5b90ab556 Binary files /dev/null and b/assets/base/images/Icon_Office.png differ diff --git a/assets/base/images/Icon_Office@2x.png b/assets/base/images/Icon_Office@2x.png new file mode 100644 index 0000000000..28f8e42378 Binary files /dev/null and b/assets/base/images/Icon_Office@2x.png differ diff --git a/assets/base/images/Icon_Office@3x.png b/assets/base/images/Icon_Office@3x.png new file mode 100644 index 0000000000..18db79a374 Binary files /dev/null and b/assets/base/images/Icon_Office@3x.png differ diff --git a/assets/base/images/gitlab.png b/assets/base/images/gitlab.png deleted file mode 100644 index fe7638b363..0000000000 Binary files a/assets/base/images/gitlab.png and /dev/null differ diff --git a/assets/base/images/google.png b/assets/base/images/google.png deleted file mode 100644 index 9271884323..0000000000 Binary files a/assets/base/images/google.png and /dev/null differ diff --git a/ios/Podfile b/ios/Podfile index 3197c3ccb6..b502d61161 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -25,6 +25,8 @@ target 'Mattermost' do # TODO: Remove this once upstream PR https://github.com/daltoniam/Starscream/pull/871 is merged pod 'Starscream', :git => 'https://github.com/mattermost/Starscream.git', :commit => 'cb83dd247339ff6c155f0e749d6fe2cc145f5283' + pod 'lottie-react-native', :path => '../node_modules/lottie-react-native' + end # Enables Flipper. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e2bc0ee141..fd9a59f503 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -837,6 +837,6 @@ SPEC CHECKSUMS: Yoga: 9a08effa851c1d8cc1647691895540bc168ea65f YoutubePlayer-in-WKWebView: 4fca3b4f6f09940077bfbae7bddb771f2b43aacd -PODFILE CHECKSUM: b69e74be8467386d2b72be99483af3956a8ed0e0 +PODFILE CHECKSUM: 3e0817a7d08c4aed8ac029ae720de65e28de4656 COCOAPODS: 1.10.2 diff --git a/test/setup.ts b/test/setup.ts index 37aaa976be..0f50ce14a3 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -233,6 +233,9 @@ jest.mock('react-native-navigation', () => { ...RNN.Navigation, events: () => ({ registerAppLaunchedListener: jest.fn(), + registerComponentListener: jest.fn(() => { + return {remove: jest.fn()}; + }), bindComponent: jest.fn(() => { return {remove: jest.fn()}; }),