Skip to content

Commit 5eedcc6

Browse files
committed
Merge branch 'release-1.0'
2 parents 022181e + fde5d40 commit 5eedcc6

File tree

13 files changed

+448
-17
lines changed

13 files changed

+448
-17
lines changed

app/App.js

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
import React from 'react'
2-
import { View, Text, StyleSheet } from 'react-native'
2+
import { MainNavigator } from './src/screens/MainNavigator'
3+
import { StyleSheet, View, Text, ActivityIndicator } from 'react-native'
4+
import { useConnection } from './src/hooks/useConnection'
35

46
export default function App () {
5-
return (
6-
<View style={styles.container}>
7-
<Text>Welcome to the workshop!</Text>
8-
</View>
9-
)
7+
const { connected, connectionError } = useConnection()
8+
9+
// use splashscreen here, if you like
10+
if (!connected) {
11+
return (
12+
<View style={styles.container}>
13+
<ActivityIndicator />
14+
<Text>Connecting to our servers...</Text>
15+
</View>
16+
)
17+
}
18+
19+
// use alert or other things here, if you like
20+
if (connectionError) {
21+
return (
22+
<View style={styles.container}>
23+
<Text>Error, while connecting to our servers!</Text>
24+
<Text>{connectionError.message}</Text>
25+
</View>
26+
)
27+
}
28+
29+
return (<MainNavigator />)
1030
}
1131

1232
const styles = StyleSheet.create({
@@ -16,4 +36,4 @@ const styles = StyleSheet.create({
1636
alignItems: 'center',
1737
justifyContent: 'center'
1838
}
19-
});
39+
})

app/babel.config.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
module.exports = function(api) {
2-
api.cache(true);
1+
module.exports = function (api) {
2+
api.cache(true)
33
return {
44
presets: ['babel-preset-expo'],
5-
plugins: ["module:react-native-dotenv"]
6-
};
7-
};
5+
plugins: ['module:react-native-dotenv']
6+
}
7+
}

app/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { registerRootComponent } from 'expo';
1+
import { registerRootComponent } from 'expo'
22

3-
import App from './App';
3+
import App from './App'
44

55
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
66
// It also ensures that whether you load the app in Expo Go or in a native build,
77
// the environment is set up appropriately
8-
registerRootComponent(App);
8+
registerRootComponent(App)

app/metro.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Learn more https://docs.expo.io/guides/customizing-metro
2-
const { getDefaultConfig } = require('expo/metro-config');
2+
const { getDefaultConfig } = require('expo/metro-config')
33

4-
module.exports = getDefaultConfig(__dirname);
4+
module.exports = getDefaultConfig(__dirname)

app/src/components/MyTasks.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react'
2+
import { Text } from 'react-native'
3+
4+
/**
5+
* Here you can implement the logic to subscribe to your tasks and CRUD them.
6+
* See: https://github.com/meteorrn/sample
7+
* @param props
8+
* @returns {JSX.Element}
9+
* @constructor
10+
*/
11+
export const MyTasks = () => (<Text>My Tasks not yet implemented</Text>)

app/src/contexts/AuthContext.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createContext } from 'react'
2+
3+
/**
4+
* Our authentication context provides an API for our components
5+
* that allows them to communicate with the servers in a decoupled way.
6+
* @method signIn
7+
* @method signUp
8+
* @method signOut
9+
* @type {React.Context<object>}
10+
*/
11+
export const AuthContext = createContext()

app/src/hooks/useConnection.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useEffect, useState } from 'react'
2+
import Meteor from '@meteorrn/core'
3+
import * as SecureStore from 'expo-secure-store'
4+
import config from '../../config.json'
5+
6+
// get detailed info about internals
7+
Meteor.isVerbose = true
8+
9+
// connect with Meteor and use a secure store
10+
// to persist our received login token, so it's encrypted
11+
// and only readable for this very app
12+
// read more at: https://docs.expo.dev/versions/latest/sdk/securestore/
13+
Meteor.connect(config.backend.url, {
14+
AsyncStorage: {
15+
getItem: SecureStore.getItemAsync,
16+
setItem: SecureStore.setItemAsync,
17+
removeItem: SecureStore.deleteItemAsync
18+
}
19+
})
20+
21+
/**
22+
* Hook that handle auto-reconnect and updates state accordingly.
23+
* @return {{connected: boolean|null, connectionError: Error|null}}
24+
*/
25+
export const useConnection = () => {
26+
const [connected, setConnected] = useState(null)
27+
const [connectionError, setConnectionError] = useState(null)
28+
29+
// we use separate functions as the handlers, so they get removed
30+
// on unmount, which happens on auto-reload and would cause errors
31+
// if not handled
32+
useEffect(() => {
33+
const onError = (e) => setConnectionError(e)
34+
Meteor.ddp.on('error', onError)
35+
36+
const onConnected = () => connected !== true && setConnected(true)
37+
Meteor.ddp.on('connected', onConnected)
38+
39+
// if the connection is lost, we not only switch the state
40+
// but also force to reconnect to the server
41+
const onDisconnected = () => {
42+
Meteor.ddp.autoConnect = true
43+
if (connected !== false) {
44+
setConnected(false)
45+
}
46+
Meteor.reconnect()
47+
}
48+
Meteor.ddp.on('disconnected', onDisconnected)
49+
50+
// remove all of these listeners on unmount
51+
return () => {
52+
Meteor.ddp.off('error', onError)
53+
Meteor.ddp.off('connected', onConnected)
54+
Meteor.ddp.off('disconnected', onDisconnected)
55+
}
56+
}, [])
57+
58+
return { connected, connectionError }
59+
}

app/src/hooks/useLogin.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useReducer, useEffect, useMemo } from 'react'
2+
import Meteor from '@meteorrn/core'
3+
4+
/** @private */
5+
const initialState = {
6+
isLoading: true,
7+
isSignout: false,
8+
userToken: null
9+
}
10+
11+
/** @private */
12+
const reducer = (state, action) => {
13+
switch (action.type) {
14+
case 'RESTORE_TOKEN':
15+
return {
16+
...state,
17+
userToken: action.token,
18+
isLoading: false
19+
}
20+
case 'SIGN_IN':
21+
return {
22+
...state,
23+
isSignOut: false,
24+
userToken: action.token
25+
}
26+
case 'SIGN_OUT':
27+
return {
28+
...state,
29+
isSignout: true,
30+
userToken: null
31+
}
32+
}
33+
}
34+
35+
/** @private */
36+
const Data = Meteor.getData()
37+
38+
/**
39+
* Provides a state and authentication context for components to decide, whether
40+
* the user is authenticated and also to run several authentication actions.
41+
*
42+
* The returned state contains the following structure:
43+
* {{
44+
* isLoading: boolean,
45+
* isSignout: boolean,
46+
* userToken: string|null
47+
* }
48+
* }}
49+
*
50+
* the authcontext provides the following methods:
51+
* {{
52+
* signIn: function,
53+
* signOut: function,
54+
* signUp: function
55+
* }}
56+
*
57+
* @returns {{
58+
* state:object,
59+
* authContext: object
60+
* }}
61+
*/
62+
export const useLogin = () => {
63+
const [state, dispatch] = useReducer(reducer, initialState, undefined)
64+
65+
// Case 1: restore token already exists
66+
// MeteorRN loads the token on connection automatically,
67+
// in case it exists, but we need to "know" that for our auth workflow
68+
useEffect(() => {
69+
const handleOnLogin = () => dispatch({ type: 'RESTORE_TOKEN', token: Meteor.getAuthToken() })
70+
Data.on('onLogin', handleOnLogin)
71+
return () => Data.off('onLogin', handleOnLogin)
72+
}, [])
73+
74+
// the auth can be referenced via useContext in the several
75+
// screens later on
76+
const authContext = useMemo(() => ({
77+
signIn: ({ email, password, onError }) => {
78+
Meteor.loginWithPassword(email, password, async (err) => {
79+
if (err) {
80+
if (err.message === 'Match failed [400]') {
81+
err.message = 'Login failed, please check your credentials and retry.'
82+
}
83+
return onError(err)
84+
}
85+
const token = Meteor.getAuthToken()
86+
const type = 'SIGN_IN'
87+
dispatch({ type, token })
88+
})
89+
},
90+
signOut: () => {
91+
Meteor.logout(err => {
92+
if (err) {
93+
// TODO display error, merge into the above workflow
94+
return console.error(err)
95+
}
96+
dispatch({ type: 'SIGN_OUT' })
97+
})
98+
},
99+
signUp: ({ email, password, onError }) => {
100+
Meteor.call('register', { email, password }, (err, res) => {
101+
if (err) {
102+
return onError(err)
103+
}
104+
// TODO move the below code and the code from signIn into an own function
105+
Meteor.loginWithPassword(email, password, async (err) => {
106+
if (err) {
107+
if (err.message === 'Match failed [400]') {
108+
err.message = 'Login failed, please check your credentials and retry.'
109+
}
110+
return onError(err)
111+
}
112+
const token = Meteor.getAuthToken()
113+
const type = 'SIGN_IN'
114+
dispatch({ type, token })
115+
})
116+
})
117+
}
118+
}), [])
119+
120+
return { state, authContext }
121+
}

app/src/screens/HomeScreen.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React, { useContext, useState } from 'react'
2+
import { View, Text, Button, StyleSheet } from 'react-native'
3+
import { AuthContext } from '../contexts/AuthContext'
4+
import { MyTasks } from '../components/MyTasks'
5+
6+
export const HomeScreen = () => {
7+
const [error, setError] = useState(null)
8+
const { signOut } = useContext(AuthContext)
9+
const onError = err => setError(err)
10+
const handleSignOut = () => signOut({ onError })
11+
12+
const renderError = () => {
13+
if (!error) { return null }
14+
return (
15+
<View style={{ alignItems: 'center' }}>
16+
<Text>{error.message}</Text>
17+
</View>
18+
)
19+
}
20+
21+
return (
22+
<View style={styles.container}>
23+
<MyTasks />
24+
{renderError()}
25+
<Button title='Sign out' onPress={handleSignOut} />
26+
</View>
27+
)
28+
}
29+
30+
const styles = StyleSheet.create({
31+
container: {
32+
flex: 1,
33+
backgroundColor: '#efefef',
34+
alignItems: 'center',
35+
justifyContent: 'center'
36+
}
37+
})

app/src/screens/LoginScreen.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React, { useState, useContext } from 'react'
2+
import { View, Text, TextInput, Button } from 'react-native'
3+
import { AuthContext } from '../contexts/AuthContext'
4+
import { inputStyles } from '../styles/inputStyles'
5+
6+
/**
7+
* Provides a login form and links to RegisterScreen
8+
* @param navigation {object} automatically passed from our Navigator, use to move to RegisterScreen
9+
* @component
10+
* @returns {JSX.Element}
11+
*/
12+
export const LoginScreen = ({ navigation }) => {
13+
const [email, setEmail] = useState('')
14+
const [password, setPassword] = useState('')
15+
const [error, setError] = useState(null)
16+
const { signIn } = useContext(AuthContext)
17+
18+
// handlers
19+
const onError = err => setError(err)
20+
const onSignIn = () => signIn({ email, password, onError })
21+
const renderError = () => {
22+
if (!error) { return null }
23+
return (
24+
<View style={{ alignItems: 'center', padding: 15 }}>
25+
<Text style={{ color: 'red' }}>{error.message}</Text>
26+
</View>
27+
)
28+
}
29+
30+
// render login form
31+
return (
32+
<View>
33+
<TextInput
34+
placeholder='Your Email'
35+
placeholderTextColor='#8a8a8a'
36+
style={inputStyles.text}
37+
value={email}
38+
onChangeText={setEmail}
39+
/>
40+
<TextInput
41+
placeholder='Password'
42+
placeholderTextColor='#8a8a8a'
43+
style={inputStyles.text}
44+
value={password}
45+
onChangeText={setPassword}
46+
secureTextEntry
47+
/>
48+
{renderError()}
49+
<Button title='Sign in' onPress={onSignIn} />
50+
<View style={{ alignItems: 'center', padding: 15 }}>
51+
<Text>or</Text>
52+
</View>
53+
<Button title='Sign up' onPress={() => navigation.navigate('SignUp')} />
54+
</View>
55+
)
56+
}

0 commit comments

Comments
 (0)