[Gekidou] MM-39736 Login Flow (#5799)
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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 = () => (
|
||||
<View style={styles.container}>
|
||||
<LottieView
|
||||
source={require('./spinner.json')}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
maxHeight: 40,
|
||||
},
|
||||
lottie: {
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
});
|
||||
|
||||
export default Loading;
|
||||
@@ -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":[]}
|
||||
@@ -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",
|
||||
|
||||
@@ -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<FloatingTextInputRef, FloatingTextInputProp
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
blur: () => inputRef.current?.blur(),
|
||||
focus: () => inputRef.current?.focus(),
|
||||
isFocused: () => inputRef.current?.isFocused() || false,
|
||||
}), [inputRef]);
|
||||
@@ -250,7 +252,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
||||
ref={inputRef}
|
||||
underlineColorAndroid='transparent'
|
||||
/>
|
||||
{error && (
|
||||
{Boolean(error) && (
|
||||
<View style={styles.errorContainer}>
|
||||
{showErrorIcon && errorIcon &&
|
||||
<CompassIcon
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
exports[`Loading should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"justifyContent": "center",
|
||||
"maxHeight": 40,
|
||||
"padding": 20,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"justifyContent": "center",
|
||||
"maxHeight": 40,
|
||||
"padding": 20,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
@@ -17,14 +20,14 @@ exports[`Loading should match snapshot 1`] = `
|
||||
Object {
|
||||
"aspectRatio": 1,
|
||||
},
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
},
|
||||
undefined,
|
||||
Array [
|
||||
Object {
|
||||
"height": 32,
|
||||
"width": 32,
|
||||
},
|
||||
undefined,
|
||||
],
|
||||
]
|
||||
}
|
||||
>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -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 (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator
|
||||
style={[styles.loading, style]}
|
||||
animating={true}
|
||||
size={size}
|
||||
color={color}
|
||||
<View style={[styles.container, containerStyle]}>
|
||||
<LottieView
|
||||
source={require('./spinner.json')}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
style={[styles.lottie, style]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
501
app/components/loading/spinner.json
Normal file
@@ -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": []
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ForgotPassword should match snapshot 1`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
testID="forgot.password.screen"
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"paddingHorizontal": 15,
|
||||
"paddingVertical": 50,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/logo.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 72,
|
||||
"resizeMode": "contain",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#d24b4e",
|
||||
"fontSize": 12,
|
||||
"marginBottom": 15,
|
||||
"marginTop": 15,
|
||||
"textAlign": "left",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="forgot.password.error.text"
|
||||
>
|
||||
|
||||
</Text>
|
||||
<View
|
||||
testID="password_send.link.prepare"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "300",
|
||||
"lineHeight": 22,
|
||||
"marginBottom": 15,
|
||||
"textAlign": "center",
|
||||
},
|
||||
Object {
|
||||
"paddingTop": 15,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
To reset your password, enter the email address you used to sign up
|
||||
</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardType="email-address"
|
||||
onChangeText={[Function]}
|
||||
placeholder="Email"
|
||||
placeholderTextColor="rgba(63,67,80,0.5)"
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "gainsboro",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"color": "#3f4350",
|
||||
"fontSize": 16,
|
||||
"height": 45,
|
||||
"marginBottom": 5,
|
||||
"marginTop": 5,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
testID="forgot.password.email"
|
||||
underlineColorAndroid="transparent"
|
||||
/>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={false}
|
||||
nativeID="animatedComponent"
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "#1c58d9",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"marginTop": 10,
|
||||
"opacity": 1,
|
||||
"padding": 15,
|
||||
}
|
||||
}
|
||||
testID="forgot.password.button"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
"fontSize": 17,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Reset my password
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
@@ -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(<ForgotPassword {...baseProps}/>);
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Error on failure of email regex', async () => {
|
||||
const {getByTestId} = renderWithIntl(<ForgotPassword {...baseProps}/>);
|
||||
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(<ForgotPassword {...baseProps}/>);
|
||||
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();
|
||||
});
|
||||
});
|
||||
71
app/screens/forgot_password/inbox.svg
Normal file
|
After Width: | Height: | Size: 38 KiB |
@@ -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<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isPasswordLinkSent, setIsPasswordLinkSent] = useState<boolean>(false);
|
||||
const {formatMessage} = useIntl();
|
||||
const emailIdRef = useRef<TextInput>(null);
|
||||
const keyboardAwareRef = useRef<KeyboardAwareScrollView>();
|
||||
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 (
|
||||
<ErrorText
|
||||
error={error}
|
||||
testID='forgot.password.error.text'
|
||||
textStyle={styles.errorText}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
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 (
|
||||
<View
|
||||
style={styles.resetSuccessContainer}
|
||||
style={styles.successContainer}
|
||||
testID={'password_send.link.sent'}
|
||||
>
|
||||
<Inbox/>
|
||||
<FormattedText
|
||||
style={styles.successTxtColor}
|
||||
style={styles.successTitle}
|
||||
id='password_send.link.title'
|
||||
defaultMessage='Reset Link Sent'
|
||||
/>
|
||||
<FormattedText
|
||||
style={styles.successText}
|
||||
id='password_send.link'
|
||||
defaultMessage='If the account exists, a password reset email will be sent to:'
|
||||
/>
|
||||
<Text style={[styles.successTxtColor, styles.emailId]}>
|
||||
<Text style={styles.successText}>
|
||||
{email}
|
||||
</Text>
|
||||
<FormattedText
|
||||
style={[
|
||||
styles.successTxtColor,
|
||||
styles.defaultTopPadding,
|
||||
]}
|
||||
id='password_send.checkInbox'
|
||||
defaultMessage='Please check your inbox.'
|
||||
/>
|
||||
<Button
|
||||
testID='password_send.return'
|
||||
onPress={onReturn}
|
||||
containerStyle={[styles.returnButton, buttonBackgroundStyle(theme, 'lg', 'primary', 'default')]}
|
||||
>
|
||||
<FormattedText
|
||||
id='password_send.return'
|
||||
defaultMessage='Return to Log In'
|
||||
style={buttonTextStyle(theme, 'lg', 'primary', 'default')}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID={'password_send.link.prepare'}>
|
||||
<FormattedText
|
||||
style={[styles.subheader, styles.defaultTopPadding]}
|
||||
id='password_send.description'
|
||||
defaultMessage='To reset your password, enter the email address you used to sign up'
|
||||
/>
|
||||
<TextInput
|
||||
ref={emailIdRef}
|
||||
style={styles.inputBox}
|
||||
onChangeText={changeEmail}
|
||||
placeholder={formatMessage({
|
||||
id: 'login.email',
|
||||
defaultMessage: 'Email',
|
||||
})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
autoCorrect={false}
|
||||
autoCapitalize='none'
|
||||
keyboardType='email-address'
|
||||
underlineColorAndroid='transparent'
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
testID={'forgot.password.email'}
|
||||
/>
|
||||
<Button
|
||||
testID='forgot.password.button'
|
||||
containerStyle={styles.signupButton}
|
||||
disabled={!email}
|
||||
onPress={submitResetPassword}
|
||||
<KeyboardAwareScrollView
|
||||
bounces={false}
|
||||
contentContainerStyle={styles.innerContainer}
|
||||
enableAutomaticScroll={Platform.OS === 'android'}
|
||||
enableOnAndroid={true}
|
||||
enableResetScrollToCoords={true}
|
||||
extraScrollHeight={0}
|
||||
keyboardDismissMode='on-drag'
|
||||
keyboardShouldPersistTaps='handled'
|
||||
|
||||
// @ts-expect-error legacy ref
|
||||
ref={keyboardAwareRef}
|
||||
scrollToOverflowEnabled={true}
|
||||
style={styles.flex}
|
||||
>
|
||||
<View
|
||||
style={styles.centered}
|
||||
testID={'password_send.link.prepare'}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage='Reset Your Password'
|
||||
id='password_send.reset'
|
||||
defaultMessage='Reset my password'
|
||||
style={styles.signupButtonText}
|
||||
testID='password_send.reset'
|
||||
style={styles.header}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
<FormattedText
|
||||
style={styles.subheader}
|
||||
id='password_send.description'
|
||||
defaultMessage='To reset your password, enter the email address you used to sign up'
|
||||
/>
|
||||
<View style={styles.form}>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
blurOnSubmit={true}
|
||||
containerStyle={styles.inputBoxEmail}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
error={error}
|
||||
keyboardType='email-address'
|
||||
label={formatMessage({id: 'login.email', defaultMessage: 'Email'})}
|
||||
onChangeText={changeEmail}
|
||||
onFocus={onFocus}
|
||||
onSubmitEditing={submitResetPassword}
|
||||
returnKeyType='next'
|
||||
spellCheck={false}
|
||||
testID='forgot.password.email'
|
||||
theme={theme}
|
||||
value={email}
|
||||
/>
|
||||
<Button
|
||||
testID='forgot.password.button'
|
||||
containerStyle={[styles.returnButton, buttonBackgroundStyle(theme, 'lg', 'primary', email ? 'default' : 'disabled'), error ? styles.error : undefined]}
|
||||
disabled={!email}
|
||||
onPress={submitResetPassword}
|
||||
>
|
||||
<FormattedText
|
||||
id='password_send.reset'
|
||||
defaultMessage='Reset my password'
|
||||
style={buttonTextStyle(theme, 'lg', 'primary', email ? 'default' : 'disabled')}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<SafeAreaView
|
||||
testID='forgot.password.screen'
|
||||
style={styles.container}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={onBlur}>
|
||||
<View style={styles.innerContainer}>
|
||||
<Image
|
||||
source={require('@assets/images/logo.png')}
|
||||
style={styles.innerContainerImage}
|
||||
/>
|
||||
{getDisplayErrorView()}
|
||||
{getCenterContent()}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</SafeAreaView>
|
||||
<View style={styles.flex}>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea
|
||||
testID='forgot.password.screen'
|
||||
style={[styles.container, transform]}
|
||||
>
|
||||
{getCenterContent()}
|
||||
</AnimatedSafeArea>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Login Login screen should match snapshot 1`] = `
|
||||
<RCTSafeAreaView
|
||||
emulateUnlessSupported={true}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<RCTScrollView
|
||||
accessible={false}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"paddingHorizontal": 15,
|
||||
"paddingVertical": 50,
|
||||
}
|
||||
}
|
||||
contentInset={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
}
|
||||
}
|
||||
enableAutomaticScroll={true}
|
||||
enableOnAndroid={true}
|
||||
enableResetScrollToCoords={true}
|
||||
extraHeight={75}
|
||||
extraScrollHeight={0}
|
||||
focusable={true}
|
||||
getScrollResponder={[Function]}
|
||||
handleOnScroll={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardOpeningTime={250}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardSpace={0}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onScroll={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
resetKeyboardSpace={[Function]}
|
||||
scrollEventThrottle={1}
|
||||
scrollForExtraHeightOnAndroid={[Function]}
|
||||
scrollIntoView={[Function]}
|
||||
scrollToEnd={[Function]}
|
||||
scrollToFocusedInput={[Function]}
|
||||
scrollToPosition={[Function]}
|
||||
showsVerticalScrollIndicator={true}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
update={[Function]}
|
||||
viewIsInsideTabBar={false}
|
||||
>
|
||||
<View>
|
||||
<Image
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/logo.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 72,
|
||||
"resizeMode": "contain",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardType="email-address"
|
||||
onChangeText={[Function]}
|
||||
onSubmitEditing={[Function]}
|
||||
placeholder="Email or Username"
|
||||
placeholderTextColor="rgba(63,67,80,0.5)"
|
||||
returnKeyType="next"
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "gainsboro",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"color": "#3f4350",
|
||||
"fontSize": 16,
|
||||
"height": 45,
|
||||
"marginBottom": 5,
|
||||
"marginTop": 5,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
testID="login.username.input"
|
||||
underlineColorAndroid="transparent"
|
||||
value=""
|
||||
/>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
disableFullscreenUI={true}
|
||||
onChangeText={[Function]}
|
||||
onSubmitEditing={[Function]}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="rgba(63,67,80,0.5)"
|
||||
returnKeyType="go"
|
||||
secureTextEntry={true}
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "gainsboro",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"color": "#3f4350",
|
||||
"fontSize": 16,
|
||||
"height": 45,
|
||||
"marginBottom": 5,
|
||||
"marginTop": 5,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
testID="login.password.input"
|
||||
underlineColorAndroid="transparent"
|
||||
value=""
|
||||
/>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
nativeID="animatedComponent"
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "#1c58d9",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"marginTop": 10,
|
||||
"opacity": 1,
|
||||
"padding": 15,
|
||||
}
|
||||
}
|
||||
testID="login.signin.button"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
"fontSize": 17,
|
||||
"textAlign": "center",
|
||||
},
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
>
|
||||
Sign in
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
nativeID="animatedComponent"
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"borderColor": "transparent",
|
||||
"marginTop": 15,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#386fe5",
|
||||
}
|
||||
}
|
||||
testID="login.forgot"
|
||||
>
|
||||
I forgot my password
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
</RCTSafeAreaView>
|
||||
`;
|
||||
396
app/screens/login/form.tsx
Normal file
@@ -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<ClientConfig>;
|
||||
keyboardAwareRef: MutableRefObject<KeyboardAwareScrollView | undefined>;
|
||||
license: Partial<ClientLicense>;
|
||||
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<TextInput>(null);
|
||||
const passwordRef = useRef<TextInput>(null);
|
||||
const intl = useIntl();
|
||||
const managedConfig = useManagedConfig();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const [loginId, setLoginId] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
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 = (
|
||||
<Loading
|
||||
containerStyle={styles.loadingContainerStyle}
|
||||
style={styles.loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
testID='login.signin.button'
|
||||
disabled={buttonDisabled}
|
||||
onPress={onLogin}
|
||||
containerStyle={[styles.loginButton, styleButtonBackground]}
|
||||
>
|
||||
{buttonIcon}
|
||||
<FormattedText
|
||||
id={buttonID}
|
||||
defaultMessage={buttonText}
|
||||
style={styleButtonText}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}, [buttonDisabled, isLoading, theme]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
blurOnSubmit={false}
|
||||
containerStyle={styles.inputBoxEmail}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
error={error ? ' ' : ''}
|
||||
keyboardType='email-address'
|
||||
label={createLoginPlaceholder()}
|
||||
onBlur={onBlur}
|
||||
onChangeText={onLoginChange}
|
||||
onFocus={onFocus}
|
||||
onSubmitEditing={focusPassword}
|
||||
ref={loginRef}
|
||||
returnKeyType='next'
|
||||
showErrorIcon={false}
|
||||
spellCheck={false}
|
||||
testID='login.username.input'
|
||||
theme={theme}
|
||||
value={loginId}
|
||||
/>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
blurOnSubmit={false}
|
||||
containerStyle={styles.inputBoxPassword}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
error={error}
|
||||
keyboardType='default'
|
||||
label={intl.formatMessage({id: 'login.password', defaultMessage: 'Password'})}
|
||||
onBlur={onBlur}
|
||||
onChangeText={onPasswordChange}
|
||||
onFocus={onFocus}
|
||||
onSubmitEditing={onLogin}
|
||||
ref={passwordRef}
|
||||
returnKeyType='join'
|
||||
spellCheck={false}
|
||||
secureTextEntry={true}
|
||||
testID='login.password.input'
|
||||
theme={theme}
|
||||
value={password}
|
||||
/>
|
||||
|
||||
{(emailEnabled || usernameEnabled) && (
|
||||
<Button
|
||||
onPress={onPressForgotPassword}
|
||||
containerStyle={[styles.forgotPasswordBtn, error ? styles.forgotPasswordError : undefined]}
|
||||
>
|
||||
<FormattedText
|
||||
id='login.forgot'
|
||||
defaultMessage='Forgot your password?'
|
||||
style={styles.forgotPasswordTxt}
|
||||
testID={'login.forgot'}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{renderProceedButton}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
@@ -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<string, boolean>;
|
||||
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<TextInput>(null);
|
||||
const passwordRef = useRef<TextInput>(null);
|
||||
const scrollRef = useRef<KeyboardAwareScrollView>(null);
|
||||
|
||||
const intl = useIntl();
|
||||
const managedConfig = useManagedConfig();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Partial<ClientErrorProps> | string | undefined | null>();
|
||||
|
||||
const [loginId, setLoginId] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
|
||||
// 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<KeyboardAwareScrollView>();
|
||||
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 (
|
||||
<ActivityIndicator
|
||||
animating={true}
|
||||
size='small'
|
||||
<FormattedText
|
||||
style={styles.subheader}
|
||||
id='mobile.login_options.enter_credentials'
|
||||
testID='mobile.login_options.enter_credentials'
|
||||
defaultMessage='Enter your login details below.'
|
||||
/>
|
||||
);
|
||||
} else if (numberSSOs) {
|
||||
return (
|
||||
<FormattedText
|
||||
style={styles.subheader}
|
||||
id='mobile.login_options.select_option'
|
||||
testID='mobile.login_options.select_option'
|
||||
defaultMessage='Select a login option below.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const additionalStyle: StyleProp<ViewStyle> = {
|
||||
...(config.EmailLoginButtonColor && {
|
||||
backgroundColor: config.EmailLoginButtonColor,
|
||||
}),
|
||||
...(config.EmailLoginButtonBorderColor && {
|
||||
borderColor: config.EmailLoginButtonBorderColor,
|
||||
}),
|
||||
};
|
||||
|
||||
const additionalTextStyle: StyleProp<TextStyle> = {
|
||||
...(config.EmailLoginButtonTextColor && {
|
||||
color: config.EmailLoginButtonTextColor,
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
testID='login.signin.button'
|
||||
onPress={preSignIn}
|
||||
containerStyle={[styles.signupButton, additionalStyle]}
|
||||
>
|
||||
<FormattedText
|
||||
id='login.signIn'
|
||||
defaultMessage='Sign in'
|
||||
style={[styles.signupButtonText, additionalTextStyle]}
|
||||
/>
|
||||
</Button>
|
||||
<FormattedText
|
||||
style={styles.subheader}
|
||||
id='mobile.login_options.none'
|
||||
testID='mobile.login_options.none'
|
||||
defaultMessage="You can't log in to your account yet. At least one login option must be configured. Contact your System Admin for assistance."
|
||||
/>
|
||||
);
|
||||
};
|
||||
}, [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) && (
|
||||
<LoginOptionsSeparator
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
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 = (
|
||||
<FormattedText
|
||||
defaultMessage='Log In to Your Account'
|
||||
id={'mobile.login_options.heading'}
|
||||
testID={'mobile.login_options.heading'}
|
||||
style={styles.header}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
title = (
|
||||
<FormattedText
|
||||
defaultMessage="Can't Log In"
|
||||
id={'mobile.login_options.cant_heading'}
|
||||
testID={'mobile.login_options.cant_heading'}
|
||||
style={styles.header}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onBlur}
|
||||
accessible={false}
|
||||
>
|
||||
<View style={styles.flex}>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea style={[styles.container, transform]}>
|
||||
<KeyboardAwareScrollView
|
||||
ref={scrollRef}
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.innerContainer}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
bounces={false}
|
||||
contentContainerStyle={[styles.innerContainer, additionalContainerStyle]}
|
||||
enableAutomaticScroll={Platform.OS === 'android'}
|
||||
enableOnAndroid={true}
|
||||
enableResetScrollToCoords={true}
|
||||
extraScrollHeight={0}
|
||||
keyboardDismissMode='on-drag'
|
||||
keyboardShouldPersistTaps='handled'
|
||||
|
||||
// @ts-expect-error legacy ref
|
||||
ref={keyboardAwareRef}
|
||||
scrollToOverflowEnabled={true}
|
||||
style={styles.flex}
|
||||
>
|
||||
<Image
|
||||
source={require('@assets/images/logo.png')}
|
||||
style={{height: 72, resizeMode: 'contain'}}
|
||||
/>
|
||||
{config?.SiteName && (<View testID='login.screen'>
|
||||
<Text style={styles.header}>{config?.SiteName}</Text>
|
||||
<FormattedText
|
||||
style={styles.subheader}
|
||||
id='web.root.signup_info'
|
||||
defaultMessage='All team communication in one place, searchable and accessible anywhere'
|
||||
<View style={styles.centered}>
|
||||
{title}
|
||||
{description}
|
||||
{hasLoginForm &&
|
||||
<Form
|
||||
config={config}
|
||||
keyboardAwareRef={keyboardAwareRef}
|
||||
license={license}
|
||||
launchError={launchError}
|
||||
launchType={launchType}
|
||||
numberSSOs={numberSSOs}
|
||||
theme={theme}
|
||||
serverDisplayName={serverDisplayName}
|
||||
serverUrl={serverUrl}
|
||||
/>
|
||||
</View>)}
|
||||
{error && (
|
||||
<ErrorText
|
||||
testID='login.error.text'
|
||||
error={error}
|
||||
}
|
||||
{optionsSeparator}
|
||||
{numberSSOs > 0 &&
|
||||
<SsoOptions
|
||||
goToSso={goToSso}
|
||||
ssoOnly={!hasLoginForm}
|
||||
ssoOptions={ssoOptions}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
testID='login.username.input'
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardType='email-address'
|
||||
onChangeText={onLoginChange}
|
||||
onSubmitEditing={onPasswordFocus}
|
||||
placeholder={createLoginPlaceholder()}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
ref={loginRef}
|
||||
returnKeyType='next'
|
||||
style={styles.inputBox}
|
||||
underlineColorAndroid='transparent'
|
||||
value={loginId} //to remove
|
||||
/>
|
||||
<TextInput
|
||||
testID='login.password.input'
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
disableFullscreenUI={true}
|
||||
onChangeText={onPasswordChange}
|
||||
onSubmitEditing={preSignIn}
|
||||
style={styles.inputBox}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'login.password',
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
ref={passwordRef}
|
||||
returnKeyType='go'
|
||||
secureTextEntry={true}
|
||||
underlineColorAndroid='transparent'
|
||||
value={password} //to remove
|
||||
/>
|
||||
{renderProceedButton()}
|
||||
{(config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true') && (
|
||||
<Button
|
||||
onPress={onPressForgotPassword}
|
||||
containerStyle={[styles.forgotPasswordBtn]}
|
||||
>
|
||||
<FormattedText
|
||||
id='login.forgot'
|
||||
defaultMessage='I forgot my password'
|
||||
style={styles.forgotPasswordTxt}
|
||||
testID={'login.forgot'}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
}
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</TouchableWithoutFeedback>
|
||||
</SafeAreaView>
|
||||
</AnimatedSafeArea>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -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(
|
||||
<Login {...baseProps}/>,
|
||||
);
|
||||
|
||||
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(<Login {...props}/>, {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(<Login {...props}/>, {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(<Login {...props}/>);
|
||||
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(<Login {...baseProps}/>);
|
||||
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(<Login {...baseProps}/>);
|
||||
const forgot = getByTestId('login.forgot');
|
||||
|
||||
fireEvent.press(forgot);
|
||||
|
||||
expect(goToScreen).
|
||||
toHaveBeenCalledWith(
|
||||
'ForgotPassword',
|
||||
'Password Reset',
|
||||
{
|
||||
serverUrl: baseProps.serverUrl,
|
||||
theme: baseProps.theme,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
53
app/screens/login/login_options_separator.tsx
Normal file
@@ -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 (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.line}/>
|
||||
<FormattedText
|
||||
id='mobile.login_options.separator_text'
|
||||
defaultMessage='or log in with'
|
||||
style={styles.text}
|
||||
testID='mobile.login_options.separator_text'
|
||||
/>
|
||||
<View style={styles.line}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginOptionsSeparator;
|
||||
172
app/screens/login/sso_options.tsx
Normal file
@@ -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<string, boolean>;
|
||||
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(
|
||||
<Button
|
||||
key={ssoType}
|
||||
onPress={handlePress}
|
||||
containerStyle={[styleButtonBackground, styleButtonContainer, styles.button]}
|
||||
>
|
||||
{imageSrc && (
|
||||
<Image
|
||||
key={'image' + ssoType}
|
||||
source={imageSrc}
|
||||
style={styles.logoStyle}
|
||||
/>
|
||||
)}
|
||||
{compassIcon &&
|
||||
<CompassIcon
|
||||
name={compassIcon}
|
||||
size={16}
|
||||
color={theme.centerChannelColor}
|
||||
/>
|
||||
}
|
||||
<View
|
||||
style={styles.buttonTextContainer}
|
||||
>
|
||||
{ssoOnly && (
|
||||
<FormattedText
|
||||
key={'pretext' + id}
|
||||
id='mobile.login_options.sso_continue'
|
||||
style={styles.buttonText}
|
||||
defaultMessage={'Continue with '}
|
||||
testID={'pretext' + id}
|
||||
/>
|
||||
)}
|
||||
<FormattedText
|
||||
key={ssoType}
|
||||
id={id}
|
||||
style={styles.buttonText}
|
||||
defaultMessage={defaultMessage}
|
||||
testID={id}
|
||||
/>
|
||||
</View>
|
||||
</Button>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styleViewContainer, styles.container]}>
|
||||
{componentArray}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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<ViewStyle> = {
|
||||
backgroundColor,
|
||||
};
|
||||
|
||||
if (config.EmailLoginButtonBorderColor) {
|
||||
additionalStyle.borderColor = config.EmailLoginButtonBorderColor;
|
||||
}
|
||||
|
||||
const textColor = config.EmailLoginButtonTextColor || 'white';
|
||||
|
||||
return (
|
||||
<Button
|
||||
key='email'
|
||||
onPress={onPress}
|
||||
containerStyle={[styles.button, additionalStyle]}
|
||||
>
|
||||
<FormattedText
|
||||
id='signup.email'
|
||||
defaultMessage='Email and Password'
|
||||
style={[styles.buttonText, {color: textColor}]}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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 (
|
||||
<Button
|
||||
key='gitlab'
|
||||
onPress={handlePress}
|
||||
containerStyle={[styles.button, additionalButtonStyle]}
|
||||
>
|
||||
<Image
|
||||
source={require('@assets/images/gitlab.png')}
|
||||
style={logoStyle}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.buttonText, {color: textColor}]}
|
||||
>
|
||||
{'GitLab'}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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 (
|
||||
<Button
|
||||
key='google'
|
||||
onPress={handlePress}
|
||||
containerStyle={[styles.button, additionalButtonStyle]}
|
||||
>
|
||||
<Image
|
||||
source={require('@assets/images/google.png')}
|
||||
style={logoStyle}
|
||||
/>
|
||||
<FormattedText
|
||||
id='signup.google'
|
||||
defaultMessage='Google Apps'
|
||||
style={[styles.buttonText, {color: textColor}]}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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 (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.innerContainer}
|
||||
>
|
||||
<Image
|
||||
source={require('@assets/images/logo.png')}
|
||||
style={{height: 72, resizeMode: 'contain'}}
|
||||
/>
|
||||
<Text style={styles.header}>
|
||||
{config.SiteName}
|
||||
</Text>
|
||||
<FormattedText
|
||||
style={styles.subheader}
|
||||
id='web.root.signup_info'
|
||||
defaultMessage='All team communication in one place, searchable and accessible anywhere'
|
||||
/>
|
||||
<FormattedText
|
||||
style={[styles.subheader, {fontWeight: 'bold', marginTop: 10}]}
|
||||
id='mobile.login_options.choose_title'
|
||||
defaultMessage='Choose your login method'
|
||||
/>
|
||||
<EmailOption
|
||||
config={config}
|
||||
onPress={displayLogin}
|
||||
theme={theme}
|
||||
/>
|
||||
<LdapOption
|
||||
config={config}
|
||||
license={license}
|
||||
onPress={displayLogin}
|
||||
theme={theme}
|
||||
/>
|
||||
<GitLabOption
|
||||
config={config}
|
||||
onPress={displaySSO}
|
||||
theme={theme}
|
||||
/>
|
||||
<GoogleOption
|
||||
config={config}
|
||||
onPress={displaySSO}
|
||||
theme={theme}
|
||||
/>
|
||||
<Office365Option
|
||||
config={config}
|
||||
license={license}
|
||||
onPress={displaySSO}
|
||||
theme={theme}
|
||||
/>
|
||||
<OpenIdOption
|
||||
config={config}
|
||||
license={license}
|
||||
onPress={displaySSO}
|
||||
theme={theme}
|
||||
/>
|
||||
<SamlOption
|
||||
config={config}
|
||||
license={license}
|
||||
onPress={displaySSO}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginOptions;
|
||||
@@ -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 = (
|
||||
<Text style={[styles.buttonText, {color: textColor}]}>
|
||||
{config.LdapLoginFieldName}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
buttonText = (
|
||||
<FormattedText
|
||||
id='login.ldapUsernameLower'
|
||||
defaultMessage='AD/LDAP username'
|
||||
style={[styles.buttonText, {color: textColor}]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key='ldap'
|
||||
onPress={onPress}
|
||||
containerStyle={[styles.button, additionalButtonStyle]}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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 (
|
||||
<Button
|
||||
key='o365'
|
||||
onPress={handlePress}
|
||||
containerStyle={[styles.button, additionalButtonStyle]}
|
||||
>
|
||||
<FormattedText
|
||||
id='signup.office365'
|
||||
defaultMessage='Office 365'
|
||||
style={[styles.buttonText, {color: textColor}]}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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 (
|
||||
<Button
|
||||
key='openId'
|
||||
onPress={handlePress}
|
||||
containerStyle={[styles.button, additionalButtonStyle]}
|
||||
>
|
||||
<FormattedText
|
||||
id='signup.openid'
|
||||
defaultMessage={config.OpenIdButtonText || 'OpenID'}
|
||||
style={[styles.buttonText, {color: textColor}]}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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 (
|
||||
<Button
|
||||
key='saml'
|
||||
onPress={handlePress}
|
||||
containerStyle={[styles.button, additionalStyle]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.buttonText, {color: textColor}]}
|
||||
>
|
||||
{config.SamlLoginButtonText}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1,172 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`*** MFA Screen *** MFA screen should match snapshot 1`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
testID="mfa.screen"
|
||||
>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
Object {
|
||||
"paddingBottom": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
Object {
|
||||
"paddingLeft": 15,
|
||||
"paddingRight": 15,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Image
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/logo.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 72,
|
||||
"resizeMode": "contain",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans-Semibold",
|
||||
"fontSize": 32,
|
||||
"marginBottom": 15,
|
||||
"marginTop": 15,
|
||||
"textAlign": "center",
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"fontSize": 20,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
To complete the sign in process, please enter a token from your smartphone's authenticator
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#d24b4e",
|
||||
"fontSize": 12,
|
||||
"marginBottom": 15,
|
||||
"marginTop": 15,
|
||||
"textAlign": "left",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="mfa.error.text"
|
||||
>
|
||||
|
||||
</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardType="numeric"
|
||||
onChangeText={[Function]}
|
||||
onSubmitEditing={[Function]}
|
||||
placeholder="MFA Token"
|
||||
placeholderTextColor="rgba(63,67,80,0.5)"
|
||||
returnKeyType="go"
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "gainsboro",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"color": "#3f4350",
|
||||
"fontSize": 16,
|
||||
"height": 45,
|
||||
"marginBottom": 5,
|
||||
"marginTop": 5,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
testID="login_mfa.input"
|
||||
underlineColorAndroid="transparent"
|
||||
value=""
|
||||
/>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
nativeID="animatedComponent"
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "#1c58d9",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"marginTop": 10,
|
||||
"opacity": 1,
|
||||
"padding": 15,
|
||||
}
|
||||
}
|
||||
testID="login_mfa.submit"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#1c58d9",
|
||||
"fontSize": 17,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Proceed
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
@@ -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<ClientConfig>;
|
||||
@@ -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<KeyboardAwareScrollView>();
|
||||
const intl = useIntl();
|
||||
const [token, setToken] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const {formatMessage} = useIntl();
|
||||
const textInputRef = useRef<TextInput>(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 (
|
||||
<ActivityIndicator
|
||||
animating={true}
|
||||
size='small'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
testID={'login_mfa.submit'}
|
||||
onPress={submit}
|
||||
containerStyle={styles.signupButton}
|
||||
>
|
||||
<FormattedText
|
||||
style={styles.signupButtonText}
|
||||
id='mobile.components.select_server_view.proceed'
|
||||
defaultMessage='Proceed'
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
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 (
|
||||
<SafeAreaView
|
||||
testID='mfa.screen'
|
||||
style={styles.flex}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior='padding'
|
||||
style={styles.flex}
|
||||
keyboardVerticalOffset={5}
|
||||
enabled={Platform.OS === 'ios'}
|
||||
<View style={styles.flex}>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea
|
||||
testID='mfa.screen'
|
||||
style={[styles.container, transform]}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={onBlur}>
|
||||
<View style={[styles.container, styles.signupContainer]}>
|
||||
<Image
|
||||
source={require('@assets/images/logo.png')}
|
||||
style={styles.containerImage}
|
||||
/>
|
||||
<View>
|
||||
<FormattedText
|
||||
style={[styles.header, styles.label]}
|
||||
id='login_mfa.enterToken'
|
||||
defaultMessage="To complete the sign in process, please enter a token from your smartphone's authenticator"
|
||||
/>
|
||||
<KeyboardAwareScrollView
|
||||
bounces={false}
|
||||
contentContainerStyle={styles.innerContainer}
|
||||
enableAutomaticScroll={Platform.OS === 'android'}
|
||||
enableOnAndroid={true}
|
||||
enableResetScrollToCoords={true}
|
||||
extraScrollHeight={0}
|
||||
keyboardDismissMode='on-drag'
|
||||
keyboardShouldPersistTaps='handled'
|
||||
|
||||
// @ts-expect-error legacy ref
|
||||
ref={keyboardAwareRef}
|
||||
scrollToOverflowEnabled={true}
|
||||
style={styles.flex}
|
||||
>
|
||||
<View style={styles.centered}>
|
||||
<View style={styles.shield}>
|
||||
<Shield/>
|
||||
</View>
|
||||
<ErrorText
|
||||
error={error}
|
||||
testID='mfa.error.text'
|
||||
theme={theme}
|
||||
<FormattedText
|
||||
defaultMessage='Enter MFA Token'
|
||||
id='login_mfa.token'
|
||||
testID='login_mfa.token'
|
||||
style={styles.header}
|
||||
/>
|
||||
<TextInput
|
||||
testID={'login_mfa.input'}
|
||||
ref={textInputRef}
|
||||
value={token}
|
||||
onChangeText={handleInput}
|
||||
onSubmitEditing={submit}
|
||||
style={styles.inputBox}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
keyboardType='numeric'
|
||||
placeholder={formatMessage({id: 'login_mfa.token', defaultMessage: 'MFA Token'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
returnKeyType='go'
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
<FormattedText
|
||||
style={styles.subheader}
|
||||
id='login_mfa.enterToken'
|
||||
defaultMessage="To complete the sign in process, please enter the code from your mobile device's authenticator app."
|
||||
/>
|
||||
{getProceedView()}
|
||||
<View style={styles.form}>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
blurOnSubmit={true}
|
||||
containerStyle={styles.inputBoxEmail}
|
||||
disableFullscreenUI={true}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
error={error}
|
||||
keyboardType='numeric'
|
||||
label={formatMessage({id: 'login_mfa.token', defaultMessage: 'Enter MFA Token'})}
|
||||
onChangeText={handleInput}
|
||||
onFocus={onFocus}
|
||||
onSubmitEditing={submit}
|
||||
returnKeyType='go'
|
||||
spellCheck={false}
|
||||
testID='login_mfa.input'
|
||||
theme={theme}
|
||||
value={token}
|
||||
/>
|
||||
<Button
|
||||
testID='login_mfa.submit'
|
||||
containerStyle={[styles.proceedButton, buttonBackgroundStyle(theme, 'lg', 'primary', token ? 'default' : 'disabled'), error ? styles.error : undefined]}
|
||||
disabled={!token}
|
||||
onPress={submit}
|
||||
>
|
||||
{isLoading &&
|
||||
<Loading
|
||||
containerStyle={styles.loadingContainerStyle}
|
||||
style={styles.loading}
|
||||
/>
|
||||
}
|
||||
<FormattedText
|
||||
id='mobile.components.select_server_view.proceed'
|
||||
defaultMessage='Proceed'
|
||||
style={buttonTextStyle(theme, 'lg', 'primary', token ? 'default' : 'disabled')}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</KeyboardAwareScrollView>
|
||||
</AnimatedSafeArea>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
10
app/screens/mfa/mfa.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="102" height="123" viewBox="0 0 102 123" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M101.675 27.9958L99.2951 16.0841C98.9372 14.3239 97.8962 12.7705 96.3932 11.7537C85.2271 4.1723 70.4235 0.223633 51.0198 0.223633C31.6161 0.223633 16.7992 4.17233 5.67313 11.7801C4.17424 12.8008 3.13447 14.3524 2.77129 16.1104L0.337456 27.9958C0.185123 28.743 0.245609 29.5171 0.512286 30.2325C0.778963 30.948 1.24145 31.5769 1.84857 32.0498C5.05801 34.5111 6.51563 37.7622 7.19763 41.8688C7.96388 46.1871 7.78145 50.6168 6.66269 54.8599C-3.27319 93.2541 13.456 110.72 50.9529 122.777C88.3963 110.72 105.166 93.2541 95.2298 54.8599C94.118 50.6158 93.9356 46.1876 94.6949 41.8688C95.4304 37.7622 96.888 34.5111 100.044 32.0498C100.675 31.593 101.163 30.9705 101.451 30.2536C101.74 29.5366 101.818 28.7542 101.675 27.9958Z" fill="#CC8F00"/>
|
||||
<path d="M51.02 113.063C16.9465 101.507 8.05362 87.1076 15.8098 57.1241C17.2615 51.6123 17.4941 45.858 16.4918 40.2501C15.7625 35.3002 13.535 30.6815 10.0996 26.9959L11.7578 18.8353C21.4931 12.4911 34.3443 9.41113 51.02 9.41113C67.6957 9.41113 80.5334 12.4911 90.2687 18.8353L91.9269 26.9959C88.4957 30.6827 86.2728 35.3015 85.5481 40.2501C84.5457 45.858 84.7783 51.6123 86.2301 57.1241C93.9461 87.0944 85.0935 101.573 51.02 113.063Z" fill="#FFBC1F"/>
|
||||
<path d="M51.02 113.063C16.9465 101.507 8.05362 87.1076 15.8098 57.1241C17.2615 51.6123 17.4941 45.858 16.4918 40.2501C15.7625 35.3002 13.535 30.6815 10.0996 26.9959L11.7578 18.8353C21.4931 12.4911 34.3443 9.41113 51.02 9.41113C67.6957 9.41113 80.5334 12.4911 90.2687 18.8353L91.9269 26.9959C88.4957 30.6827 86.2728 35.3015 85.5481 40.2501C84.5457 45.858 84.7783 51.6123 86.2301 57.1241C93.9461 87.0944 85.0935 101.573 51.02 113.063Z" fill="#FFBC1F"/>
|
||||
<path d="M51.02 52.4516V9.4375C34.3443 9.4375 21.4931 12.5174 11.7578 18.8616L10.0996 27.0222C13.535 30.7078 15.7625 35.3266 16.4918 40.2765C17.2095 44.3075 17.2861 48.4238 16.7191 52.4779L51.02 52.4516Z" fill="#FFBC1F"/>
|
||||
<path d="M51.0197 113.063C85.0932 101.573 93.9458 87.0941 86.2298 57.1237C85.8323 55.5847 85.5242 54.0247 85.3071 52.4512H51.0197V113.063Z" fill="#FFBC1F"/>
|
||||
<path d="M15.8099 57.1237C8.05375 87.1072 16.9466 101.507 51.0201 113.063V52.4512H16.7192C16.5066 54.0245 16.203 55.5845 15.8099 57.1237V57.1237Z" fill="#FFD470"/>
|
||||
<path d="M51.0197 9.4375V52.4779H85.3071C84.7378 48.4235 84.8191 44.3062 85.5478 40.2765C86.2725 35.3279 88.4954 30.709 91.9266 27.0222L90.2684 18.8616C80.5732 12.4911 67.682 9.4375 51.0197 9.4375Z" fill="#FFD470"/>
|
||||
<path d="M71.0652 38.9336L43.464 68.8118L35.4538 62.8361H31.0007L43.464 82.7505L75.5183 38.9336H71.0652Z" fill="#6F370B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -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(<Mfa {...baseProps}/>);
|
||||
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(<Mfa {...props}/>);
|
||||
const submitBtn = getByTestId('login_mfa.submit');
|
||||
const inputText = getByTestId('login_mfa.input');
|
||||
fireEvent.changeText(inputText, 'token123');
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.press(submitBtn);
|
||||
});
|
||||
|
||||
expect(spyOnGoToHome).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = (
|
||||
<ActivityIndicator
|
||||
animating={true}
|
||||
size='small'
|
||||
color={'white'}
|
||||
style={styles.connectingIndicator}
|
||||
<Loading
|
||||
containerStyle={styles.loadingContainerStyle}
|
||||
style={styles.loading}
|
||||
/>
|
||||
);
|
||||
} else if (buttonDisabled) {
|
||||
styleButtonText = buttonTextStyle(theme, 'lg', 'primary', 'disabled');
|
||||
styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', 'disabled');
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -150,6 +165,7 @@ const ServerForm = ({
|
||||
<View style={[styles.fullWidth, urlError?.length ? styles.error : undefined]}>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
blurOnSubmit={false}
|
||||
containerStyle={styles.enterServer}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
@@ -165,6 +181,7 @@ const ServerForm = ({
|
||||
onSubmitEditing={onUrlSubmit}
|
||||
ref={urlRef}
|
||||
returnKeyType='next'
|
||||
spellCheck={false}
|
||||
testID='select_server.server_url.input'
|
||||
theme={theme}
|
||||
value={url}
|
||||
@@ -173,6 +190,7 @@ const ServerForm = ({
|
||||
<View style={[styles.fullWidth, displayNameError?.length ? styles.error : undefined]}>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
error={displayNameError}
|
||||
keyboardType='url'
|
||||
@@ -183,9 +201,10 @@ const ServerForm = ({
|
||||
onBlur={onBlur}
|
||||
onChangeText={handleDisplayNameTextChanged}
|
||||
onFocus={onFocus}
|
||||
onSubmitEditing={handleConnect}
|
||||
onSubmitEditing={onConnect}
|
||||
ref={displayNameRef}
|
||||
returnKeyType='done'
|
||||
spellCheck={false}
|
||||
testID='select_server.server_display_name.input'
|
||||
theme={theme}
|
||||
value={displayName}
|
||||
@@ -202,7 +221,7 @@ const ServerForm = ({
|
||||
<Button
|
||||
containerStyle={[styles.connectButton, styleButtonBackground]}
|
||||
disabled={buttonDisabled}
|
||||
onPress={handleConnect}
|
||||
onPress={onConnect}
|
||||
testID='select_server.connect.button'
|
||||
>
|
||||
{buttonIcon}
|
||||
|
||||
@@ -37,7 +37,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
description: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
...typography('Body', 100, 'Regular'),
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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<ManagedConfig>();
|
||||
const dimensions = useWindowDimensions();
|
||||
const translateX = useSharedValue(0);
|
||||
const keyboardAwareRef = useRef<KeyboardAwareScrollView>();
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [displayName, setDisplayName] = useState<string>('');
|
||||
@@ -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<string, boolean> = {
|
||||
[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 (
|
||||
<View style={styles.flex}>
|
||||
<Background theme={theme}/>
|
||||
<SafeAreaView
|
||||
<AnimatedSafeArea
|
||||
key={'server_content'}
|
||||
style={styles.flex}
|
||||
style={[styles.flex, transform]}
|
||||
testID='select_server.screen'
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
@@ -304,7 +318,7 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp
|
||||
/>
|
||||
</KeyboardAwareScrollView>
|
||||
<AppVersion textStyle={styles.appInfo}/>
|
||||
</SafeAreaView>
|
||||
</AnimatedSafeArea>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -317,9 +331,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
flex: 1,
|
||||
},
|
||||
scrollContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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<ClientLicense>;
|
||||
ssoType: string;
|
||||
serverDisplayName: string;
|
||||
theme: Partial<Theme>;
|
||||
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<string>('');
|
||||
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 = (
|
||||
<SSOWithWebView
|
||||
{...props}
|
||||
completeUrlPath={completeUrlPath}
|
||||
@@ -111,12 +146,22 @@ const SSO = ({config, extra, launchError, launchType, serverDisplayName, serverU
|
||||
ssoType={ssoType}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
ssoComponent = (
|
||||
<SSOWithRedirectURL
|
||||
{...props}
|
||||
serverUrl={serverUrl!}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SSOWithRedirectURL
|
||||
{...props}
|
||||
serverUrl={serverUrl!}
|
||||
/>
|
||||
<View style={styles.flex}>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea style={[styles.flex, transform]}>
|
||||
{ssoComponent}
|
||||
</AnimatedSafeArea>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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(<SSOLogin {...props}/>);
|
||||
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(<SSOLogin {...props}/>);
|
||||
|
||||
@@ -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: 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<string>('');
|
||||
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 (
|
||||
<SafeAreaView
|
||||
<View
|
||||
style={style.container}
|
||||
testID='sso.redirect_url'
|
||||
>
|
||||
{loginError || error ? (
|
||||
<View style={style.errorContainer}>
|
||||
<View style={style.errorTextContainer}>
|
||||
<Text style={style.errorText}>
|
||||
{`${loginError || error}.`}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => init()}>
|
||||
<View style={style.infoContainer}>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser.error_title'
|
||||
testID='mobile.oauth.switch_to_browser.error_title'
|
||||
defaultMessage='Sign in error'
|
||||
style={style.infoTitle}
|
||||
/>
|
||||
<Text style={style.errorText}>
|
||||
{`${loginError || error}.`}
|
||||
</Text>
|
||||
<Button
|
||||
testID='mobile.oauth.try_again'
|
||||
onPress={() => init()}
|
||||
containerStyle={[style.button, buttonBackgroundStyle(theme, 'lg', 'primary', 'default')]}
|
||||
>
|
||||
<FormattedText
|
||||
id='mobile.oauth.try_again'
|
||||
testID='mobile.oauth.try_again'
|
||||
defaultMessage='Try again'
|
||||
style={style.button}
|
||||
style={buttonTextStyle(theme, 'lg', 'primary', 'default')}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View style={style.infoContainer}>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser.title'
|
||||
testID='mobile.oauth.switch_to_browser.title'
|
||||
defaultMessage='Redirecting...'
|
||||
style={style.infoTitle}
|
||||
/>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser'
|
||||
testID='mobile.oauth.switch_to_browser'
|
||||
defaultMessage='Please use your browser to complete the login'
|
||||
defaultMessage='You are being redirected to your login provider'
|
||||
style={style.infoText}
|
||||
/>
|
||||
<Loading/>
|
||||
</View>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -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<Theme>;
|
||||
}
|
||||
|
||||
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<Theme>;
|
||||
}
|
||||
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 (
|
||||
<SafeAreaView
|
||||
<View
|
||||
style={style.container}
|
||||
testID='sso.webview'
|
||||
>
|
||||
@@ -236,27 +255,8 @@ const SSOWithWebView = ({completeUrlPath, doSSOLogin, loginError, loginUrl, serv
|
||||
<Text style={style.errorText}>{error || loginError}</Text>
|
||||
</View>
|
||||
) : renderWebView()}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export function concatStyles<T>(...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) {
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
BIN
assets/base/images/Icon_Gitlab.png
Normal file
|
After Width: | Height: | Size: 636 B |
BIN
assets/base/images/Icon_Gitlab@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/base/images/Icon_Gitlab@3x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/base/images/Icon_Google.png
Normal file
|
After Width: | Height: | Size: 710 B |
BIN
assets/base/images/Icon_Google@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/base/images/Icon_Google@3x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/base/images/Icon_Office.png
Normal file
|
After Width: | Height: | Size: 395 B |
BIN
assets/base/images/Icon_Office@2x.png
Normal file
|
After Width: | Height: | Size: 607 B |
BIN
assets/base/images/Icon_Office@3x.png
Normal file
|
After Width: | Height: | Size: 895 B |
|
Before Width: | Height: | Size: 509 B |
|
Before Width: | Height: | Size: 601 B |
@@ -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.
|
||||
|
||||
@@ -837,6 +837,6 @@ SPEC CHECKSUMS:
|
||||
Yoga: 9a08effa851c1d8cc1647691895540bc168ea65f
|
||||
YoutubePlayer-in-WKWebView: 4fca3b4f6f09940077bfbae7bddb771f2b43aacd
|
||||
|
||||
PODFILE CHECKSUM: b69e74be8467386d2b72be99483af3956a8ed0e0
|
||||
PODFILE CHECKSUM: 3e0817a7d08c4aed8ac029ae720de65e28de4656
|
||||
|
||||
COCOAPODS: 1.10.2
|
||||
|
||||
@@ -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()};
|
||||
}),
|
||||
|
||||