Skip to content

Commit

Permalink
Merge branch 'release-1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
jankapunkt committed Oct 13, 2022
2 parents 022181e + fde5d40 commit 5eedcc6
Show file tree
Hide file tree
Showing 13 changed files with 448 additions and 17 deletions.
34 changes: 27 additions & 7 deletions app/App.js
@@ -1,12 +1,32 @@
import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
import { MainNavigator } from './src/screens/MainNavigator'
import { StyleSheet, View, Text, ActivityIndicator } from 'react-native'
import { useConnection } from './src/hooks/useConnection'

export default function App () {
return (
<View style={styles.container}>
<Text>Welcome to the workshop!</Text>
</View>
)
const { connected, connectionError } = useConnection()

// use splashscreen here, if you like
if (!connected) {
return (
<View style={styles.container}>
<ActivityIndicator />
<Text>Connecting to our servers...</Text>
</View>
)
}

// use alert or other things here, if you like
if (connectionError) {
return (
<View style={styles.container}>
<Text>Error, while connecting to our servers!</Text>
<Text>{connectionError.message}</Text>
</View>
)
}

return (<MainNavigator />)
}

const styles = StyleSheet.create({
Expand All @@ -16,4 +36,4 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center'
}
});
})
10 changes: 5 additions & 5 deletions app/babel.config.js
@@ -1,7 +1,7 @@
module.exports = function(api) {
api.cache(true);
module.exports = function (api) {
api.cache(true)
return {
presets: ['babel-preset-expo'],
plugins: ["module:react-native-dotenv"]
};
};
plugins: ['module:react-native-dotenv']
}
}
6 changes: 3 additions & 3 deletions app/index.js
@@ -1,8 +1,8 @@
import { registerRootComponent } from 'expo';
import { registerRootComponent } from 'expo'

import App from './App';
import App from './App'

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
registerRootComponent(App)
4 changes: 2 additions & 2 deletions app/metro.config.js
@@ -1,4 +1,4 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
const { getDefaultConfig } = require('expo/metro-config')

module.exports = getDefaultConfig(__dirname);
module.exports = getDefaultConfig(__dirname)
11 changes: 11 additions & 0 deletions app/src/components/MyTasks.js
@@ -0,0 +1,11 @@
import React from 'react'
import { Text } from 'react-native'

/**
* Here you can implement the logic to subscribe to your tasks and CRUD them.
* See: https://github.com/meteorrn/sample
* @param props
* @returns {JSX.Element}
* @constructor
*/
export const MyTasks = () => (<Text>My Tasks not yet implemented</Text>)
11 changes: 11 additions & 0 deletions app/src/contexts/AuthContext.js
@@ -0,0 +1,11 @@
import { createContext } from 'react'

/**
* Our authentication context provides an API for our components
* that allows them to communicate with the servers in a decoupled way.
* @method signIn
* @method signUp
* @method signOut
* @type {React.Context<object>}
*/
export const AuthContext = createContext()
59 changes: 59 additions & 0 deletions app/src/hooks/useConnection.js
@@ -0,0 +1,59 @@
import { useEffect, useState } from 'react'
import Meteor from '@meteorrn/core'
import * as SecureStore from 'expo-secure-store'
import config from '../../config.json'

// get detailed info about internals
Meteor.isVerbose = true

// connect with Meteor and use a secure store
// to persist our received login token, so it's encrypted
// and only readable for this very app
// read more at: https://docs.expo.dev/versions/latest/sdk/securestore/
Meteor.connect(config.backend.url, {
AsyncStorage: {
getItem: SecureStore.getItemAsync,
setItem: SecureStore.setItemAsync,
removeItem: SecureStore.deleteItemAsync
}
})

/**
* Hook that handle auto-reconnect and updates state accordingly.
* @return {{connected: boolean|null, connectionError: Error|null}}
*/
export const useConnection = () => {
const [connected, setConnected] = useState(null)
const [connectionError, setConnectionError] = useState(null)

// we use separate functions as the handlers, so they get removed
// on unmount, which happens on auto-reload and would cause errors
// if not handled
useEffect(() => {
const onError = (e) => setConnectionError(e)
Meteor.ddp.on('error', onError)

const onConnected = () => connected !== true && setConnected(true)
Meteor.ddp.on('connected', onConnected)

// if the connection is lost, we not only switch the state
// but also force to reconnect to the server
const onDisconnected = () => {
Meteor.ddp.autoConnect = true
if (connected !== false) {
setConnected(false)
}
Meteor.reconnect()
}
Meteor.ddp.on('disconnected', onDisconnected)

// remove all of these listeners on unmount
return () => {
Meteor.ddp.off('error', onError)
Meteor.ddp.off('connected', onConnected)
Meteor.ddp.off('disconnected', onDisconnected)
}
}, [])

return { connected, connectionError }
}
121 changes: 121 additions & 0 deletions app/src/hooks/useLogin.js
@@ -0,0 +1,121 @@
import { useReducer, useEffect, useMemo } from 'react'
import Meteor from '@meteorrn/core'

/** @private */
const initialState = {
isLoading: true,
isSignout: false,
userToken: null
}

/** @private */
const reducer = (state, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...state,
userToken: action.token,
isLoading: false
}
case 'SIGN_IN':
return {
...state,
isSignOut: false,
userToken: action.token
}
case 'SIGN_OUT':
return {
...state,
isSignout: true,
userToken: null
}
}
}

/** @private */
const Data = Meteor.getData()

/**
* Provides a state and authentication context for components to decide, whether
* the user is authenticated and also to run several authentication actions.
*
* The returned state contains the following structure:
* {{
* isLoading: boolean,
* isSignout: boolean,
* userToken: string|null
* }
* }}
*
* the authcontext provides the following methods:
* {{
* signIn: function,
* signOut: function,
* signUp: function
* }}
*
* @returns {{
* state:object,
* authContext: object
* }}
*/
export const useLogin = () => {
const [state, dispatch] = useReducer(reducer, initialState, undefined)

// Case 1: restore token already exists
// MeteorRN loads the token on connection automatically,
// in case it exists, but we need to "know" that for our auth workflow
useEffect(() => {
const handleOnLogin = () => dispatch({ type: 'RESTORE_TOKEN', token: Meteor.getAuthToken() })
Data.on('onLogin', handleOnLogin)
return () => Data.off('onLogin', handleOnLogin)
}, [])

// the auth can be referenced via useContext in the several
// screens later on
const authContext = useMemo(() => ({
signIn: ({ email, password, onError }) => {
Meteor.loginWithPassword(email, password, async (err) => {
if (err) {
if (err.message === 'Match failed [400]') {
err.message = 'Login failed, please check your credentials and retry.'
}
return onError(err)
}
const token = Meteor.getAuthToken()
const type = 'SIGN_IN'
dispatch({ type, token })
})
},
signOut: () => {
Meteor.logout(err => {
if (err) {
// TODO display error, merge into the above workflow
return console.error(err)
}
dispatch({ type: 'SIGN_OUT' })
})
},
signUp: ({ email, password, onError }) => {
Meteor.call('register', { email, password }, (err, res) => {
if (err) {
return onError(err)
}
// TODO move the below code and the code from signIn into an own function
Meteor.loginWithPassword(email, password, async (err) => {
if (err) {
if (err.message === 'Match failed [400]') {
err.message = 'Login failed, please check your credentials and retry.'
}
return onError(err)
}
const token = Meteor.getAuthToken()
const type = 'SIGN_IN'
dispatch({ type, token })
})
})
}
}), [])

return { state, authContext }
}
37 changes: 37 additions & 0 deletions app/src/screens/HomeScreen.js
@@ -0,0 +1,37 @@
import React, { useContext, useState } from 'react'
import { View, Text, Button, StyleSheet } from 'react-native'
import { AuthContext } from '../contexts/AuthContext'
import { MyTasks } from '../components/MyTasks'

export const HomeScreen = () => {
const [error, setError] = useState(null)
const { signOut } = useContext(AuthContext)
const onError = err => setError(err)
const handleSignOut = () => signOut({ onError })

const renderError = () => {
if (!error) { return null }
return (
<View style={{ alignItems: 'center' }}>
<Text>{error.message}</Text>
</View>
)
}

return (
<View style={styles.container}>
<MyTasks />
{renderError()}
<Button title='Sign out' onPress={handleSignOut} />
</View>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#efefef',
alignItems: 'center',
justifyContent: 'center'
}
})
56 changes: 56 additions & 0 deletions app/src/screens/LoginScreen.js
@@ -0,0 +1,56 @@
import React, { useState, useContext } from 'react'
import { View, Text, TextInput, Button } from 'react-native'
import { AuthContext } from '../contexts/AuthContext'
import { inputStyles } from '../styles/inputStyles'

/**
* Provides a login form and links to RegisterScreen
* @param navigation {object} automatically passed from our Navigator, use to move to RegisterScreen
* @component
* @returns {JSX.Element}
*/
export const LoginScreen = ({ navigation }) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const { signIn } = useContext(AuthContext)

// handlers
const onError = err => setError(err)
const onSignIn = () => signIn({ email, password, onError })
const renderError = () => {
if (!error) { return null }
return (
<View style={{ alignItems: 'center', padding: 15 }}>
<Text style={{ color: 'red' }}>{error.message}</Text>
</View>
)
}

// render login form
return (
<View>
<TextInput
placeholder='Your Email'
placeholderTextColor='#8a8a8a'
style={inputStyles.text}
value={email}
onChangeText={setEmail}
/>
<TextInput
placeholder='Password'
placeholderTextColor='#8a8a8a'
style={inputStyles.text}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
{renderError()}
<Button title='Sign in' onPress={onSignIn} />
<View style={{ alignItems: 'center', padding: 15 }}>
<Text>or</Text>
</View>
<Button title='Sign up' onPress={() => navigation.navigate('SignUp')} />
</View>
)
}

0 comments on commit 5eedcc6

Please sign in to comment.