stripe: Implement payment intent flow
- Remove remaining uses of native-base Toast - Try supporting stripe hardlinks - Tweaks to app.json - Add expo-constants to expose app.json / expoConfig to app code
This commit is contained in:
13 files changed
+1309
-1504
No files matched your search
@@ -11,6 +11,7 @@
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"scheme": "no-neuf-chateau",
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
@@ -18,6 +19,7 @@
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"package": "no.neuf.chateau",
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
|
||||
Generated
+1027
-1351
File diff suppressed because it is too large.
Load diff
+6
-5
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@babel/plugin-transform-private-methods": "^7.24.7",
|
||||
"@expo/vector-icons": "^14.0.0",
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@react-native-async-storage/async-storage": "^1.23.1",
|
||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||
"@react-navigation/native": "^6.1.7",
|
||||
@@ -20,8 +20,9 @@
|
||||
"@sentry/react-native": "^5.22.0",
|
||||
"@stripe/stripe-react-native": "^0.37.2",
|
||||
"email-validator": "^2.0.4",
|
||||
"expo": "^51.0.14",
|
||||
"expo-asset": "^10.0.9",
|
||||
"expo": "~51.0.17",
|
||||
"expo-asset": "~10.0.10",
|
||||
"expo-constants": "~16.0.2",
|
||||
"expo-status-bar": "^1.12.1",
|
||||
"graphql": "^15.8.0",
|
||||
"metro-react-native-babel-preset": "^0.77.0",
|
||||
@@ -29,9 +30,9 @@
|
||||
"native-base": "^3.4.28",
|
||||
"react": "18.2.0",
|
||||
"react-hook-form": "^7.45.2",
|
||||
"react-native": "^0.74.2",
|
||||
"react-native": "0.74.3",
|
||||
"react-native-confetti": "^0.1.0",
|
||||
"react-native-gesture-handler": "^2.16.1",
|
||||
"react-native-gesture-handler": "~2.16.1",
|
||||
"react-native-reanimated": "^3.10.1",
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "^4.10.1",
|
||||
|
||||
+21
-14
@@ -3,9 +3,12 @@ import { Provider } from 'react-redux';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import { ApolloClient, HttpLink, InMemoryCache, ApolloProvider } from '@apollo/client';
|
||||
import { StripeProvider } from '@stripe/stripe-react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { LogBox } from 'react-native';
|
||||
|
||||
import Constants from 'expo-constants';
|
||||
import { loadTheme } from './theme';
|
||||
import { useDeepLinking } from './hooks';
|
||||
|
||||
import Dusken from './Dusken';
|
||||
import { store, persistor } from './reduxStore';
|
||||
@@ -20,20 +23,24 @@ LogBox.ignoreLogs(['In React 18, SSRProvider is not necessary and is a noop. You
|
||||
loadTheme();
|
||||
|
||||
export default function App() {
|
||||
useDeepLinking();
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
{/* FIXME: Read urlScheme and merchantIdentifier from .Config */}
|
||||
<StripeProvider
|
||||
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
|
||||
urlScheme="no-neuf-chateau"
|
||||
merchantIdentifier="merchant.com.dusken"
|
||||
>
|
||||
<Dusken />
|
||||
</StripeProvider>
|
||||
</ApolloProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
<GestureHandlerRootView>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
{/* FIXME: Read urlScheme and merchantIdentifier from .Config */}
|
||||
<StripeProvider
|
||||
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
|
||||
urlScheme={Constants.expoConfig.scheme}
|
||||
merchantIdentifier="merchant.no.neuf.chateau" // https://developer.apple.com/account/resources/identifiers/list/merchant
|
||||
>
|
||||
<Dusken />
|
||||
</StripeProvider>
|
||||
</ApolloProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
/** YOLO */
|
||||
@@ -99,26 +99,31 @@ function membershipChargeFailure(chargeError) {
|
||||
}
|
||||
|
||||
export {
|
||||
/* Login */
|
||||
LOGIN_REQUEST,
|
||||
loginRequest,
|
||||
LOGIN_SUCCESS,
|
||||
loginSuccess,
|
||||
LOGIN_FAILURE,
|
||||
loginFailure,
|
||||
/* Logout */
|
||||
LOGOUT,
|
||||
logout,
|
||||
/* User data */
|
||||
USER_DATA_REQUEST,
|
||||
userDataRequest,
|
||||
USER_DATA_SUCCESS,
|
||||
userDataSuccess,
|
||||
USER_DATA_FAILURE,
|
||||
userDataFailure,
|
||||
/* Register */
|
||||
REGISTER_USER_REQUEST,
|
||||
registerUserRequest,
|
||||
REGISTER_USER_SUCCESS,
|
||||
registerUserSuccess,
|
||||
REGISTER_USER_FAILURE,
|
||||
registerUserFailure,
|
||||
/* Charge */
|
||||
MEMBERSHIP_CHARGE_REQUEST,
|
||||
membershipChargeRequest,
|
||||
MEMBERSHIP_CHARGE_SUCCESS,
|
||||
|
||||
+32
@@ -150,3 +150,35 @@ export function requestMembershipCharge(authToken, stripeToken, membershipType)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a stripe payment sheet. Note that this is not using redux */
|
||||
export async function requestStripePaymentSheet(authToken, membershipType) {
|
||||
const body = JSON.stringify({
|
||||
membership_type: membershipType,
|
||||
});
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Token ${authToken}`,
|
||||
};
|
||||
return fetchWithTimeout(`${process.env.EXPO_PUBLIC_DUSKEN_API_URL}/api/stripe/payment-sheet/`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
.then((response) => response.json().then((json) => ({ json, response })))
|
||||
.then(({ json, response }) => {
|
||||
if (response.ok === false) {
|
||||
return Promise.reject(json);
|
||||
}
|
||||
return json;
|
||||
})
|
||||
.then(
|
||||
(data) => {
|
||||
return data;
|
||||
},
|
||||
(data) => {
|
||||
return data.detail || 'Kunne ikke belaste bankkortet, prøv igjen';
|
||||
},
|
||||
);
|
||||
}
|
||||
+36
-2
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import { Linking } from 'react-native';
|
||||
import { useStripe } from '@stripe/stripe-react-native';
|
||||
|
||||
export function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
@@ -12,3 +13,36 @@ export function usePrevious(value) {
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
/** Handles deep links. Currently only used by stripe */
|
||||
export const useDeepLinking = () => {
|
||||
const { handleURLCallback } = useStripe();
|
||||
|
||||
const handleDeepLink = useCallback(
|
||||
async (url) => {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass url to stripe
|
||||
// Note: This return value can be used to target non-stripe URLs in the future
|
||||
await handleURLCallback(url);
|
||||
},
|
||||
[handleURLCallback],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const getUrlAsync = async () => {
|
||||
const initialUrl = await Linking.getInitialURL();
|
||||
handleDeepLink(initialUrl);
|
||||
};
|
||||
|
||||
getUrlAsync();
|
||||
|
||||
const deepLinkListener = Linking.addEventListener('url', (event) => {
|
||||
handleDeepLink(event.url);
|
||||
});
|
||||
|
||||
return () => deepLinkListener.remove();
|
||||
}, [handleDeepLink]);
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { Button } from 'react-native-ui-lib';
|
||||
import { Button, Incubator } from 'react-native-ui-lib';
|
||||
import Confetti from 'react-native-confetti';
|
||||
|
||||
import theme from '../../theme';
|
||||
@@ -18,6 +18,8 @@ import useProof from './useProof';
|
||||
|
||||
import logo from '../../../assets/DNS-logo-svart.png';
|
||||
|
||||
const { Toast } = Incubator;
|
||||
|
||||
const MembershipValidTo = ({ user }) => {
|
||||
if (!user.last_membership) {
|
||||
return null;
|
||||
@@ -60,18 +62,18 @@ const MembershipStatus = ({ user, onMembershipPress }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PurchaseButton = ({ user, isChargingMembership, isLoadingMembershipType, onChargePress, membershipPrice }) => {
|
||||
const PurchaseButton = ({ user, paymentLoading, isLoadingMembershipType, onChargePress, membershipPrice }) => {
|
||||
if (user.is_member) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{isChargingMembership && <ActivityIndicator color={theme.colors.neufOrange} />}
|
||||
{paymentLoading && <ActivityIndicator color={theme.colors.neufOrange} />}
|
||||
<Button
|
||||
onPress={onChargePress}
|
||||
fullWidth
|
||||
disabled={isChargingMembership || isLoadingMembershipType}
|
||||
disabled={paymentLoading || isLoadingMembershipType}
|
||||
style={styles.purchaseButton}
|
||||
>
|
||||
<Text>{`Kjøp medlemskap (${membershipPrice} NOK)`}</Text>
|
||||
@@ -130,10 +132,11 @@ const MembershipName = ({ user }) => {
|
||||
const Proof = ({ onLogoutPress, user, fetchUser, isFetchingUserData }) => {
|
||||
const CONFETTI_TIMEOUT = 15000;
|
||||
const confettiRef = useRef(null);
|
||||
const { chargeError, isChargingMembership, isLoadingMembershipType, onChargePress, membershipPrice } = useProof({
|
||||
user,
|
||||
fetchUser,
|
||||
});
|
||||
const { chargeError, paymentLoading, isLoadingMembershipType, onChargePress, membershipPrice, toast, setToast } =
|
||||
useProof({
|
||||
user,
|
||||
fetchUser,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const currentRef = confettiRef.current;
|
||||
@@ -162,30 +165,41 @@ const Proof = ({ onLogoutPress, user, fetchUser, isFetchingUserData }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<UserConfetti user={user} confettiRef={confettiRef} />
|
||||
<ScrollView refreshControl={<RefreshControl refreshing={isFetchingUserData} onRefresh={fetchUser} />}>
|
||||
<View style={[styles.card, { paddingVertical: 16 }]}>
|
||||
<MembershipName user={user} />
|
||||
<Logo user={user} />
|
||||
<MembershipStatus user={user} onMembershipPress={onMembershipPress} />
|
||||
<ChargeError chargeError={chargeError} />
|
||||
<PurchaseButton
|
||||
user={user}
|
||||
isChargingMembership={isChargingMembership}
|
||||
isLoadingMembershipType={isLoadingMembershipType}
|
||||
membershipPrice={membershipPrice}
|
||||
onChargePress={onChargePress}
|
||||
/>
|
||||
<MembershipValidTo user={user} />
|
||||
</View>
|
||||
<View>
|
||||
<Button onPress={onLogoutPress} style={styles.logoutButton} small>
|
||||
<Text>Logg ut</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<>
|
||||
<View>
|
||||
<UserConfetti user={user} confettiRef={confettiRef} />
|
||||
<ScrollView refreshControl={<RefreshControl refreshing={isFetchingUserData} onRefresh={fetchUser} />}>
|
||||
<View style={[styles.card, { paddingVertical: 16 }]}>
|
||||
<MembershipName user={user} />
|
||||
<Logo user={user} />
|
||||
<MembershipStatus user={user} onMembershipPress={onMembershipPress} />
|
||||
<ChargeError chargeError={chargeError} />
|
||||
<PurchaseButton
|
||||
user={user}
|
||||
paymentLoading={paymentLoading}
|
||||
isLoadingMembershipType={isLoadingMembershipType}
|
||||
membershipPrice={membershipPrice}
|
||||
onChargePress={onChargePress}
|
||||
/>
|
||||
<MembershipValidTo user={user} />
|
||||
</View>
|
||||
<View>
|
||||
<Button onPress={onLogoutPress} style={styles.logoutButton} small>
|
||||
<Text>Logg ut</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<Toast
|
||||
message={toast?.message}
|
||||
visible={toast?.visible}
|
||||
position="bottom"
|
||||
autoDismiss={5 * 1000}
|
||||
onDismiss={() => {
|
||||
setToast(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Proof;
|
||||
|
||||
@@ -45,6 +45,7 @@ const useMembership = () => {
|
||||
onLogoutPress,
|
||||
onRegisterPress,
|
||||
onLoginPress,
|
||||
fetchUser,
|
||||
};
|
||||
};
|
||||
export default useMembership;
|
||||
@@ -1,10 +1,10 @@
|
||||
// FIXME: fix lint violations
|
||||
/* eslint-disable no-unused-vars, react-hooks/exhaustive-deps */
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { useStripe } from '@stripe/stripe-react-native';
|
||||
import Constants from 'expo-constants';
|
||||
|
||||
import { requestMembershipCharge } from '../../api';
|
||||
import { requestStripePaymentSheet } from '../../api';
|
||||
|
||||
const PROOF_QUERY = gql`
|
||||
{
|
||||
@@ -24,48 +24,67 @@ function getPrice(membershipTypes) {
|
||||
}
|
||||
|
||||
const useProof = ({ user, fetchUser }) => {
|
||||
const dispatch = useDispatch();
|
||||
const userToken = useSelector((state) => state.userToken);
|
||||
const isChargingMembership = useSelector((state) => state.isChargingMembership);
|
||||
const chargeError = useSelector((state) => state.chargeError);
|
||||
const { data, loading } = useQuery(PROOF_QUERY);
|
||||
const { initPaymentSheet, presentPaymentSheet } = useStripe();
|
||||
const [paymentLoading, setPaymentLoading] = useState(false);
|
||||
const membershipTypeSlug = data?.membershipTypes?.[0]?.slug ?? 'standard';
|
||||
const [toast, setToast] = useState(null);
|
||||
|
||||
const openStripe = () => {
|
||||
const options = {
|
||||
prefilledInformation: {
|
||||
email: user.email,
|
||||
},
|
||||
smsAutofillDisabled: true,
|
||||
};
|
||||
|
||||
const membershipTypeSlug = data?.membershipTypes?.[0]?.slug ?? 'standard';
|
||||
|
||||
// TODO: Implement stripe payment
|
||||
// stripe.paymentRequestWithCardForm(options).then(
|
||||
// (token) => {
|
||||
// // returns a token.tokenId which should be saved
|
||||
// dispatch(requestMembershipCharge(userToken, { id: token.tokenId, email: user.email }, membershipTypeSlug));
|
||||
// },
|
||||
// () => {
|
||||
// /* FIXME: Happens only if card dialog is canceled? */
|
||||
// }
|
||||
// );
|
||||
const openPaymentSheet = async () => {
|
||||
try {
|
||||
await initializePaymentSheet();
|
||||
} catch (err) {
|
||||
console.error('🔥', err);
|
||||
setToast({ message: 'Oops! Could not initialise payment', visible: true, preset: 'failure' });
|
||||
}
|
||||
const { error } = await presentPaymentSheet();
|
||||
const message = error ? `Error code: ${error.code} ${error.message}` : 'Your order is confirmed!';
|
||||
setToast({ message, visible: true, preset: error ? 'failure' : 'success' });
|
||||
setPaymentLoading(false);
|
||||
};
|
||||
|
||||
const initializePaymentSheet = async () => {
|
||||
setPaymentLoading(true);
|
||||
const { paymentIntent, ephemeralKey, customer } = await requestStripePaymentSheet(userToken, membershipTypeSlug);
|
||||
|
||||
const { error } = await initPaymentSheet({
|
||||
merchantDisplayName: 'Det Norske Studentersamfund',
|
||||
customerId: customer,
|
||||
customerEphemeralKeySecret: ephemeralKey,
|
||||
paymentIntentClientSecret: paymentIntent,
|
||||
defaultBillingDetails: {
|
||||
email: user.email,
|
||||
name: `${user.first_name} ${user.last_name}`,
|
||||
address: {
|
||||
country: 'NO',
|
||||
},
|
||||
},
|
||||
returnURL: `${Constants.expoConfig.scheme}://stripe-redirect`,
|
||||
});
|
||||
if (!error) {
|
||||
setPaymentLoading(true);
|
||||
}
|
||||
};
|
||||
|
||||
/* Refetech user data if memberships has expired */
|
||||
useEffect(() => {
|
||||
const todaysDate = new Date().toISOString().substring(0, 10);
|
||||
const lastMembership = user?.last_membership;
|
||||
if (lastMembership && lastMembership.membership_type !== 'lifelong' && lastMembership.end_date < todaysDate) {
|
||||
fetchUser();
|
||||
}
|
||||
}, []);
|
||||
}, [fetchUser, user?.last_membership]);
|
||||
|
||||
return {
|
||||
chargeError: chargeError || '',
|
||||
isChargingMembership,
|
||||
isLoadingMembershipType: loading,
|
||||
membershipPrice: getPrice(data?.membershipTypes),
|
||||
onChargePress: openStripe,
|
||||
onChargePress: openPaymentSheet,
|
||||
paymentLoading,
|
||||
toast,
|
||||
setToast,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ScrollView } from 'native-base';
|
||||
import { Button } from 'react-native-ui-lib';
|
||||
import { Button, Incubator } from 'react-native-ui-lib';
|
||||
|
||||
import { ActivityIndicator, StyleSheet, View, Text } from 'react-native';
|
||||
import { ActivityIndicator, StyleSheet, View, Text, ScrollView } from 'react-native';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
|
||||
import theme from '../../theme';
|
||||
@@ -10,73 +9,87 @@ import useUserRegister from './useUserRegister';
|
||||
import FormInput from '../../components/FormInput';
|
||||
import FormErrors from '../../components/FormErrors';
|
||||
|
||||
const { Toast } = Incubator;
|
||||
|
||||
const UserRegister = () => {
|
||||
const { isRegisteringUser, onSubmit, control, isValid, errors, registerError, setFocus } = useUserRegister();
|
||||
const { isRegisteringUser, onSubmit, control, isValid, errors, registerError, setFocus, toast, setToast } =
|
||||
useUserRegister();
|
||||
const isDisabled = isRegisteringUser || !isValid;
|
||||
// TODO: validate on type
|
||||
// TODO: highlight errorfields
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<>
|
||||
<View style={styles.card}>
|
||||
<FormInput
|
||||
name="firstName"
|
||||
label="Fornavn"
|
||||
control={control}
|
||||
errors={errors}
|
||||
autoFocus={true}
|
||||
onSubmitEditing={() => setFocus('lastName')}
|
||||
/>
|
||||
<FormInput
|
||||
name="lastName"
|
||||
label="Etternavn"
|
||||
control={control}
|
||||
errors={errors}
|
||||
onSubmitEditing={() => setFocus('email')}
|
||||
/>
|
||||
<FormInput
|
||||
name="email"
|
||||
label="E-post"
|
||||
control={control}
|
||||
errors={errors}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
rules={{
|
||||
required: 'Kan ikke være tomt!',
|
||||
validate: (v) => EmailValidator.validate(v) || 'er ikke en gyldig e-post',
|
||||
}}
|
||||
onSubmitEditing={() => setFocus('phoneNumber')}
|
||||
/>
|
||||
<FormInput
|
||||
name="phoneNumber"
|
||||
label="Mobilnummer"
|
||||
control={control}
|
||||
errors={errors}
|
||||
keyboardType="phone-pad"
|
||||
onSubmitEditing={() => setFocus('password')}
|
||||
/>
|
||||
<FormInput
|
||||
name="password"
|
||||
label="Passord"
|
||||
control={control}
|
||||
errors={errors}
|
||||
secureTextEntry={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
returnKeyType="send"
|
||||
onSubmitEditing={onSubmit}
|
||||
/>
|
||||
<View style={styles.errorBox}>
|
||||
<FormErrors errors={errors} />
|
||||
<FormErrors errors={registerError} />
|
||||
</View>
|
||||
<Button fullWidth disabled={isDisabled} onPress={onSubmit} style={styles.registerButton}>
|
||||
<Text>Registrer meg</Text>
|
||||
</Button>
|
||||
{isRegisteringUser && <ActivityIndicator color={theme.colors.neufOrange} />}
|
||||
<ScrollView>
|
||||
<FormInput
|
||||
name="firstName"
|
||||
label="Fornavn"
|
||||
control={control}
|
||||
errors={errors}
|
||||
autoFocus={true}
|
||||
onSubmitEditing={() => setFocus('lastName')}
|
||||
/>
|
||||
<FormInput
|
||||
name="lastName"
|
||||
label="Etternavn"
|
||||
control={control}
|
||||
errors={errors}
|
||||
onSubmitEditing={() => setFocus('email')}
|
||||
/>
|
||||
<FormInput
|
||||
name="email"
|
||||
label="E-post"
|
||||
control={control}
|
||||
errors={errors}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
rules={{
|
||||
required: 'Kan ikke være tomt!',
|
||||
validate: (v) => EmailValidator.validate(v) || 'er ikke en gyldig e-post',
|
||||
}}
|
||||
onSubmitEditing={() => setFocus('phoneNumber')}
|
||||
/>
|
||||
<FormInput
|
||||
name="phoneNumber"
|
||||
label="Mobilnummer"
|
||||
control={control}
|
||||
errors={errors}
|
||||
keyboardType="phone-pad"
|
||||
onSubmitEditing={() => setFocus('password')}
|
||||
/>
|
||||
<FormInput
|
||||
name="password"
|
||||
label="Passord"
|
||||
control={control}
|
||||
errors={errors}
|
||||
secureTextEntry={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
returnKeyType="send"
|
||||
onSubmitEditing={onSubmit}
|
||||
/>
|
||||
<View style={styles.errorBox}>
|
||||
<FormErrors errors={errors} />
|
||||
<FormErrors errors={registerError} />
|
||||
</View>
|
||||
<Button fullWidth disabled={isDisabled} onPress={onSubmit} style={styles.registerButton}>
|
||||
<Text>Registrer meg</Text>
|
||||
</Button>
|
||||
{isRegisteringUser && <ActivityIndicator color={theme.colors.neufOrange} />}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<Toast
|
||||
message={toast?.message}
|
||||
visible={toast?.visible}
|
||||
position="bottom"
|
||||
autoDismiss={5 * 1000}
|
||||
onDismiss={() => {
|
||||
setToast(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Toast } from 'native-base';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -13,6 +13,7 @@ const useUserRegister = () => {
|
||||
const isAuthenticated = useSelector((store) => store.isAuthenticated);
|
||||
const isRegisteringUser = useSelector((store) => store.isRegisteringUser);
|
||||
const registerError = useSelector((store) => store.registerError) || {};
|
||||
const [toast, setToast] = useState(null);
|
||||
|
||||
const isAuthenticatedPrev = usePrevious(isAuthenticated);
|
||||
|
||||
@@ -25,14 +26,14 @@ const useUserRegister = () => {
|
||||
} = useForm();
|
||||
const onSubmit = handleSubmit((values) => {
|
||||
if (!isValid) {
|
||||
Toast.show({
|
||||
text: 'Noen av feltene er ikke fylt ut riktig',
|
||||
position: 'bottom',
|
||||
buttonText: 'OK',
|
||||
duration: 1500,
|
||||
setToast({
|
||||
message: 'Noen av feltene er ikke fylt ut riktig',
|
||||
visible: true,
|
||||
preset: 'failure',
|
||||
});
|
||||
return;
|
||||
}
|
||||
setToast(null);
|
||||
|
||||
dispatch(requestRegisterUser(values));
|
||||
|
||||
@@ -48,6 +49,6 @@ const useUserRegister = () => {
|
||||
}
|
||||
}, [isAuthenticatedPrev, isAuthenticated, navigation]);
|
||||
|
||||
return { isRegisteringUser, onSubmit, control, isValid, registerError, errors, setFocus };
|
||||
return { isRegisteringUser, onSubmit, control, isValid, registerError, errors, setFocus, toast, setToast };
|
||||
};
|
||||
export default useUserRegister;
|
||||
+1
-1
@@ -81,7 +81,7 @@ export default function duskenApp(state = initialState, action) {
|
||||
|
||||
return { ...state, isRegisteringUser: false, isAuthenticated: true, user: userData, userToken: token };
|
||||
|
||||
/* User data */
|
||||
/* Membership charge */
|
||||
case MEMBERSHIP_CHARGE_REQUEST:
|
||||
return { ...state, isChargingMembership: true, chargeError: null };
|
||||
|
||||
|
||||
Reference in new issue
Block a user