Fix last typescript strict issues

This uses the `any` escape hatch a few more places, which should be tidied up later.
Suggest upgrading redux usage to the latest version and start using slices to make a few things easier. Also the confetti component might be worth replacing

Remove dead code from the old membership charge flow++
This commit is contained in:
nikolark committed 2024-07-07 22:33:57 +02:00
1 parent 7a1fb19f56
commit fc8e6c9745
19 files changed
+80 -178

No files matched your search

+7 -34
View File
@@ -1,3 +1,5 @@
import { UserType } from './types/data';
const LOGIN_REQUEST = 'LOGIN_REQUEST';
const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
const LOGIN_FAILURE = 'LOGIN_FAILURE';
@@ -8,9 +10,6 @@ const USER_DATA_FAILURE = 'USER_DATA_FAILURE';
const REGISTER_USER_REQUEST = 'REGISTER_USER_REQUEST';
const REGISTER_USER_SUCCESS = 'REGISTER_USER_SUCCESS';
const REGISTER_USER_FAILURE = 'REGISTER_USER_FAILURE';
const MEMBERSHIP_CHARGE_REQUEST = 'MEMBERSHIP_CHARGE_REQUEST';
const MEMBERSHIP_CHARGE_SUCCESS = 'MEMBERSHIP_CHARGE_SUCCESS';
const MEMBERSHIP_CHARGE_FAILURE = 'MEMBERSHIP_CHARGE_FAILURE';
function loginRequest() {
return {
@@ -26,7 +25,7 @@ function loginSuccess(username: string, token: string) {
};
}
function loginFailure(loginError) {
function loginFailure(loginError: string) {
return {
type: LOGIN_FAILURE,
loginError,
@@ -45,14 +44,14 @@ function userDataRequest() {
};
}
function userDataSuccess(data) {
function userDataSuccess(data: UserType) {
return {
type: USER_DATA_SUCCESS,
data,
};
}
function userDataFailure(userError) {
function userDataFailure(userError: string) {
return {
type: USER_DATA_FAILURE,
userError,
@@ -65,38 +64,19 @@ function registerUserRequest() {
};
}
function registerUserSuccess(data) {
function registerUserSuccess(data: UserType) {
return {
type: REGISTER_USER_SUCCESS,
data,
};
}
function registerUserFailure(registerError) {
function registerUserFailure(registerError: Record<string, string>) {
return {
type: REGISTER_USER_FAILURE,
registerError,
};
}
function membershipChargeRequest() {
return {
type: MEMBERSHIP_CHARGE_REQUEST,
};
}
function membershipChargeSuccess(data) {
return {
type: MEMBERSHIP_CHARGE_SUCCESS,
data,
};
}
function membershipChargeFailure(chargeError) {
return {
type: MEMBERSHIP_CHARGE_FAILURE,
chargeError,
};
}
export {
/* Login */
@@ -123,11 +103,4 @@ export {
registerUserSuccess,
REGISTER_USER_FAILURE,
registerUserFailure,
/* Charge */
MEMBERSHIP_CHARGE_REQUEST,
membershipChargeRequest,
MEMBERSHIP_CHARGE_SUCCESS,
membershipChargeSuccess,
MEMBERSHIP_CHARGE_FAILURE,
membershipChargeFailure,
};
-38
View File
@@ -8,9 +8,6 @@ import {
userDataFailure,
userDataRequest,
userDataSuccess,
membershipChargeFailure,
membershipChargeRequest,
membershipChargeSuccess,
} from './actions';
import type { UserRegisterFormValues } from './modules/register/useUserRegister';
import { AppDispatch } from './reduxStore';
@@ -118,41 +115,6 @@ export function requestRegisterUser({ firstName, lastName, email, phoneNumber, p
};
}
export function requestMembershipCharge(authToken: string, stripeToken: string, membershipType: string) {
return (dispatch: AppDispatch) => {
dispatch(membershipChargeRequest());
return fetchWithTimeout(`${process.env.EXPO_PUBLIC_DUSKEN_API_URL}/api/membership/charge/`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${authToken}`,
},
body: JSON.stringify({
stripe_token: stripeToken,
membership_type: membershipType,
payment_method: 'app',
}),
})
.then((response) => response.json().then((json) => ({ json, response })))
.then(({ json, response }) => {
if (response.ok === false) {
return Promise.reject(json);
}
return json;
})
.then(
(data) => {
dispatch(membershipChargeSuccess(data));
},
(data) => {
dispatch(membershipChargeFailure(data.detail || 'Kunne ikke belaste bankkortet, prøv igjen'));
},
);
};
}
/** Create a stripe payment sheet. Note that this is not using redux */
export async function requestStripePaymentSheet(authToken: string, membershipType: string) {
const body = JSON.stringify({
+2 -1
View File
@@ -2,7 +2,8 @@ import { Alert, HStack } from 'native-base';
import React from 'react';
import { Text } from 'react-native';
const FormErrors = ({ errors }) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const FormErrors = ({ errors }: { errors: Record<string, any> }) => {
if (!Object.keys(errors).length || !['non_field_errors', 'detail'].some((errorField) => errorField in errors)) {
return null;
}
+1 -1
View File
@@ -41,7 +41,7 @@ const FormInput = ({
/>
{fieldError && (
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
<Text>{fieldError.message.toString()}</Text>
<Text>{fieldError.message?.toString()}</Text>
</FormControl.ErrorMessage>
)}
</FormControl>
+3 -3
View File
@@ -6,8 +6,8 @@ import { useDispatch, useSelector, useStore } from 'react-redux';
import type { AppDispatch, AppStore, RootState } from './reduxStore';
export function usePrevious(value) {
const ref = useRef();
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
// Store current value in ref
useEffect(() => {
@@ -23,7 +23,7 @@ export const useDeepLinking = () => {
const { handleURLCallback } = useStripe();
const handleDeepLink = useCallback(
async (url) => {
async (url: string | null) => {
if (!url) {
return;
}
+15 -12
View File
@@ -21,10 +21,11 @@ const CHATEAU_NEUF_ADDRESS = 'Det Norske Studentersamfund, Slemdalsveien 15, 036
const openFacebook = () => {
/* Ref: https://stackoverflow.com/questions/4810803/open-facebook-page-from-android-app */
const url = Platform.select({
ios: 'fb://page/?id=23664032291',
android: 'fb://page/23664032291/',
});
const url =
Platform.select({
ios: 'fb://page/?id=23664032291',
android: 'fb://page/23664032291/',
}) || '';
Linking.canOpenURL(url)
.then((supported) => {
if (!supported) {
@@ -36,10 +37,11 @@ const openFacebook = () => {
};
const openInstagram = () => {
const url = Platform.select({
ios: 'instagram://user?username=studentersamfundet',
android: 'https://instagram.com/_u/studentersamfundet',
});
const url =
Platform.select({
ios: 'instagram://user?username=studentersamfundet',
android: 'https://instagram.com/_u/studentersamfundet',
}) || '';
Linking.canOpenURL(url)
.then((supported) => {
@@ -52,10 +54,11 @@ const openInstagram = () => {
};
const openMap = () => {
const url = Platform.select({
ios: `comgooglemaps://?q=${CHATEAU_NEUF_ADDRESS}`,
android: `https://www.google.com/maps/search/?api=1&query=${CHATEAU_NEUF_ADDRESS}`,
});
const url =
Platform.select({
ios: `comgooglemaps://?q=${CHATEAU_NEUF_ADDRESS}`,
android: `https://www.google.com/maps/search/?api=1&query=${CHATEAU_NEUF_ADDRESS}`,
}) || '';
Linking.canOpenURL(url)
.then((supported) => {
if (!supported) {
+5 -4
View File
@@ -69,10 +69,11 @@ const onFacebookUrlPress = (link: string) => {
if (matches && matches.length) {
iosURL = `fb://event/?id=${matches[1]}`;
}
const url = Platform.select({
ios: iosURL,
android: link,
});
const url =
Platform.select({
ios: iosURL,
android: link,
}) || '';
Linking.canOpenURL(url)
.then((supported) => {
if (!supported) {
+4 -3
View File
@@ -6,6 +6,7 @@ import { Button } from 'react-native-ui-lib';
import 'moment/locale/nb';
import theme from 'src/theme';
import { EventType } from 'src/types/data';
import useEventList from './useEventList';
@@ -15,7 +16,7 @@ const formatTime = (time: string) => {
return moment(time).format('llll');
};
const renderSectionHeader = ({ section }) => (
const renderSectionHeader = ({ section }: { section: { title: string } }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
@@ -25,7 +26,7 @@ const Header = () => <View style={styles.listHeader} />;
const EventList = () => {
const { eventsSectioned, refreshing, handleRefresh, handleLoadMore, showEvent, error, loading } = useEventList();
const renderItem = ({ item }) => (
const renderItem = ({ item }: { item: EventType }) => (
<TouchableOpacity
onPress={() => {
showEvent(item);
@@ -77,7 +78,7 @@ const EventList = () => {
sections={eventsSectioned}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item) => item.id}
keyExtractor={(item: EventType) => item.id}
refreshing={refreshing}
onRefresh={handleRefresh}
onEndReached={handleLoadMore}
+11 -8
View File
@@ -8,25 +8,28 @@ import { fetchWithTimeout } from 'src/utils';
moment.locale('nb');
const formatDate = (time) => {
const formatDate = (time: string) => {
const m = moment(time);
return moment().year() === m.year() ? m.format('dddd D. MMMM') : m.format('ll');
};
type SectionedEvents = {
data: EventType[];
title: string;
};
/* Ref: https://medium.com/react-native-development/how-to-use-the-flatlist-component-react-native-basics-92c482816fe6 */
const useEventList = () => {
const { navigate } = useNavigation();
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState([]);
const [eventsSectioned, setEventsSectioned] = useState([]);
const [events, setEvents] = useState<EventType[]>([]);
const [eventsSectioned, setEventsSectioned] = useState<SectionedEvents[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [error, setError] = useState(null);
const [error, setError] = useState<unknown>(null);
const [refreshing, setRefreshing] = useState(false);
const toSectionFormat = (freshEvents) => {
const sectioned = {};
const toSectionFormat = (freshEvents: EventType[]) => {
const sectioned: Record<string, EventType[]> = {};
freshEvents.forEach((event) => {
const title = formatDate(event.start_time);
if (title in sectioned) {
@@ -62,7 +65,7 @@ const useEventList = () => {
let eventData;
try {
const res = await fetchWithTimeout(url);
setTotalPages(parseInt(res.headers.get('x-wp-totalpages'), 10));
setTotalPages(parseInt(res.headers.get('x-wp-totalpages') || '0', 10));
eventData = await res.json();
} catch (err) {
setError(err);
+6 -28
View File
@@ -75,14 +75,14 @@ interface PurchaseButtonProps {
paymentLoading: boolean;
isLoadingMembershipType: boolean;
membershipPrice: number;
onChargePress: () => void;
onPurchasePress: () => void;
}
const PurchaseButton = ({
user,
paymentLoading,
isLoadingMembershipType,
onChargePress,
onPurchasePress,
membershipPrice,
}: PurchaseButtonProps) => {
if (user.is_member) {
@@ -93,7 +93,7 @@ const PurchaseButton = ({
<View>
{paymentLoading && <ActivityIndicator color={theme.colors.neufOrange} />}
<Button
onPress={onChargePress}
onPress={onPurchasePress}
fullWidth
disabled={paymentLoading || isLoadingMembershipType}
style={styles.purchaseButton}
@@ -128,18 +128,6 @@ const Logo = ({ user }: { user: UserType }) => {
);
};
const ChargeError = ({ chargeError }) => {
if (!chargeError) {
return null;
}
return (
<View>
<Text style={styles.errorMessage}>{chargeError}</Text>
</View>
);
};
const MembershipName = ({ user }: { user: UserType }) => {
let name = `${user.first_name} ${user.last_name}`;
if (name === ' ') {
@@ -160,16 +148,7 @@ interface ProofProps {
const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData }: ProofProps) => {
const CONFETTI_TIMEOUT = 15000;
const confettiRef = useRef<Confetti>(null);
const {
chargeError,
paymentLoading,
isLoadingMembershipType,
onChargePress,
membershipPrice,
toast,
setToast,
user,
} = useProof({
const { paymentLoading, isLoadingMembershipType, onPurchasePress, membershipPrice, toast, setToast, user } = useProof({
fetchUser,
});
@@ -183,7 +162,7 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData }: ProofProps) =>
});
const onMembershipPress = () => {
confettiRef.current.startConfetti();
confettiRef.current?.startConfetti();
setTimeout(() => {
if (confettiRef && confettiRef.current) {
confettiRef.current.stopConfetti();
@@ -208,13 +187,12 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData }: ProofProps) =>
<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}
onPurchasePress={onPurchasePress}
/>
<MembershipValidTo user={user} />
</View>
+2 -8
View File
@@ -10,10 +10,8 @@ const useMembership = () => {
const dispatch = useAppDispatch();
const isAuthenticated = useAppSelector((store) => store.isAuthenticated);
const isFetchingUserData = useAppSelector((store) => store.isFetchingUserData);
const lastOrder = useAppSelector((store) => store.lastOrder);
const userToken = useAppSelector((store) => store.userToken);
const prevUserToken = usePrevious(userToken);
const prevLastOrder = usePrevious(lastOrder);
const onLogoutPress = () => {
dispatch(logout());
@@ -22,23 +20,19 @@ const useMembership = () => {
const onRegisterPress = () => navigation.navigate('UserStack', { screen: 'UserRegister' });
const onLoginPress = () => navigation.navigate('UserStack', { screen: 'Login' });
const fetchUser = useCallback(() => {
dispatch(requestUserData(userToken));
dispatch(requestUserData(userToken || ''));
}, [dispatch, userToken]);
useEffect(() => {
if (userToken !== prevUserToken && userToken) {
fetchUser();
}
if (prevLastOrder !== lastOrder && userToken) {
fetchUser();
}
}, [userToken, prevUserToken, lastOrder, prevLastOrder, fetchUser]);
}, [userToken, prevUserToken, fetchUser]);
return {
isAuthenticated,
isFetchingUserData,
userToken,
lastOrder,
onLogoutPress,
onRegisterPress,
onLoginPress,
+5 -8
View File
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
import { requestStripePaymentSheet } from 'src/api';
import { useAppSelector } from 'src/hooks';
import { ToastProps } from 'src/types/toast';
const PROOF_QUERY = gql`
{
@@ -15,16 +16,13 @@ const PROOF_QUERY = gql`
}
`;
interface ToastProps {
message: string;
visible: boolean;
preset: 'failure' | 'success';
interface UseProofProps {
fetchUser: () => void;
}
const useProof = ({ fetchUser }) => {
const useProof = ({ fetchUser }: UseProofProps) => {
const user = useAppSelector((state) => state.user);
const userToken = useAppSelector((state) => state.userToken);
const chargeError = useAppSelector((state) => state.chargeError);
const { data, loading } = useQuery(PROOF_QUERY);
const { initPaymentSheet, presentPaymentSheet } = useStripe();
const [paymentLoading, setPaymentLoading] = useState(false);
@@ -81,10 +79,9 @@ const useProof = ({ fetchUser }) => {
const membershipPrice = data?.membershipTypes?.[0]?.price;
return {
chargeError: chargeError || '',
isLoadingMembershipType: loading,
membershipPrice: membershipPrice ? membershipPrice / 100 : 0,
onChargePress: openPaymentSheet,
onPurchasePress: openPaymentSheet,
paymentLoading,
toast,
setToast,
+2 -1
View File
@@ -4,6 +4,7 @@ import { useForm } from 'react-hook-form';
import { requestRegisterUser } from 'src/api';
import { useAppDispatch, useAppSelector, usePrevious } from 'src/hooks';
import { ToastProps } from 'src/types/toast';
export interface UserRegisterFormValues {
firstName: string;
@@ -19,7 +20,7 @@ const useUserRegister = () => {
const isAuthenticated = useAppSelector((store) => store.isAuthenticated);
const isRegisteringUser = useAppSelector((store) => store.isRegisteringUser);
const registerError = useAppSelector((store) => store.registerError) || {};
const [toast, setToast] = useState(null);
const [toast, setToast] = useState<ToastProps | null>(null);
const isAuthenticatedPrev = usePrevious(isAuthenticated);
+8 -27
View File
@@ -9,9 +9,6 @@ import {
REGISTER_USER_REQUEST,
REGISTER_USER_FAILURE,
REGISTER_USER_SUCCESS,
MEMBERSHIP_CHARGE_REQUEST,
MEMBERSHIP_CHARGE_FAILURE,
MEMBERSHIP_CHARGE_SUCCESS,
} from './actions';
import type { UserType } from './types/data';
import { snakeToCamelCase } from './utils';
@@ -21,15 +18,11 @@ interface RootState {
isLoggingIn: boolean;
isFetchingUserData: boolean;
isRegisteringUser: boolean;
isChargingMembership: boolean;
userToken: string | null;
user: UserType | null;
// FIXME: Type rest
loginError: unknown | null;
userError: unknown | null;
registerError: unknown | null;
chargeError: unknown | null;
lastOrder: unknown | null;
loginError: Record<string, string> | null;
userError: Record<string, string> | null;
registerError: Record<string, string> | null;
}
const initialState: RootState = {
@@ -37,26 +30,24 @@ const initialState: RootState = {
isLoggingIn: false,
isFetchingUserData: false,
isRegisteringUser: false,
isChargingMembership: false,
userToken: null,
user: null,
loginError: null,
userError: null,
registerError: null,
chargeError: null,
lastOrder: null,
};
function formatErrors(errs) {
const errors = {};
function formatErrors(errs: Record<string, string | string[]>) {
const errors: Record<string, string> = {};
Object.entries(errs).forEach(([key, value]) => {
errors[snakeToCamelCase(key)] = Array.isArray(value) ? value.join('\n') : value;
});
return errors;
}
// eslint-disable-next-line @typescript-eslint/default-param-last
export default function duskenApp(state = initialState, action) {
// FIXME: refactor using redux slices
// eslint-disable-next-line @typescript-eslint/default-param-last, @typescript-eslint/no-explicit-any
export default function duskenApp(state = initialState, action: any) {
switch (action.type) {
/* Login */
case LOGIN_REQUEST:
@@ -97,16 +88,6 @@ export default function duskenApp(state = initialState, action) {
return { ...state, isRegisteringUser: false, isAuthenticated: true, user: userData, userToken: token };
/* Membership charge */
case MEMBERSHIP_CHARGE_REQUEST:
return { ...state, isChargingMembership: true, chargeError: null };
case MEMBERSHIP_CHARGE_FAILURE:
return { ...state, isChargingMembership: false, chargeError: action.chargeError };
case MEMBERSHIP_CHARGE_SUCCESS:
return { ...state, isChargingMembership: false, lastOrder: action.data };
default:
return state;
}
+1 -1
View File
@@ -8,7 +8,7 @@ import rootReducer from './reducers';
const persistConfig = {
key: 'root',
storage: AsyncStorage,
blacklist: ['isLoggingIn', 'isFetchingUserData', 'isRegisteringUser', 'isChargingMembership', 'loginError'],
blacklist: ['isLoggingIn', 'isFetchingUserData', 'isRegisteringUser', 'loginError'],
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
+1
View File
@@ -1,4 +1,5 @@
export interface EventType {
id: string;
start_time: string;
end_time: string;
content: {
+5
View File
@@ -0,0 +1,5 @@
export interface ToastProps {
message: string;
visible: boolean;
preset: 'failure' | 'success';
}
+1 -1
View File
@@ -1,4 +1,4 @@
export function snakeToCamelCase(val: string) {
export function snakeToCamelCase(val: string): string {
return val.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
}
+1
View File
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"baseUrl": "./",
"strict": true
},
"extends": "expo/tsconfig.base"
}