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()};
}),