Stop using a hardcoded membership price if loading the pricing fails
This commit is contained in:
3 files changed
+70
-14
No files matched your search
@@ -1,4 +1,5 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
import { fireEvent, waitFor } from '@testing-library/react-native';
|
import { fireEvent, waitFor } from '@testing-library/react-native';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -56,12 +57,16 @@ const expiredMember: UserType = {
|
|||||||
last_membership: { membership_type: 'standard', end_date: '2020-01-01' },
|
last_membership: { membership_type: 'standard', end_date: '2020-01-01' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderProof = ({ user = nonMember, fetchUser = jest.fn() }: { user?: UserType; fetchUser?: () => void } = {}) =>
|
const renderProof = ({
|
||||||
|
user = nonMember,
|
||||||
|
fetchUser = jest.fn(),
|
||||||
|
mocks = apolloMocks,
|
||||||
|
}: { user?: UserType; fetchUser?: () => void; mocks?: readonly MockedResponse[] } = {}) =>
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isFetchingUserData={false} onDeletePress={jest.fn()} />,
|
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isFetchingUserData={false} onDeletePress={jest.fn()} />,
|
||||||
{
|
{
|
||||||
preloadedState: { isAuthenticated: true, userToken: 'test-token', user },
|
preloadedState: { isAuthenticated: true, userToken: 'test-token', user },
|
||||||
apolloMocks,
|
apolloMocks: mocks,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -70,6 +75,30 @@ beforeEach(() => {
|
|||||||
(Notifications.getPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'undetermined' });
|
(Notifications.getPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'undetermined' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('disables the purchase button without showing a price while membership types load', async () => {
|
||||||
|
const { getByRole, findByText } = renderProof();
|
||||||
|
|
||||||
|
// The label carries no amount until the query resolves
|
||||||
|
expect(getByRole('button', { name: 'Kjøp medlemskap' })).toBeDisabled();
|
||||||
|
|
||||||
|
// Once the query resolves the real price takes over
|
||||||
|
expect(await findByText('Kjøp medlemskap (300 NOK)')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an error with retry instead of a price when membership types fail to load', async () => {
|
||||||
|
const { findByText, getByText, getByRole } = renderProof({
|
||||||
|
mocks: [{ request: { query: PROOF_QUERY }, error: new Error('network down') }, ...apolloMocks],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await findByText('Kunne ikke hente priser')).toBeTruthy();
|
||||||
|
expect(getByRole('button', { name: 'Kjøp medlemskap' })).toBeDisabled();
|
||||||
|
|
||||||
|
// Retrying consumes the queued successful mock and recovers the real price
|
||||||
|
fireEvent.press(getByText('Prøv igjen'));
|
||||||
|
expect(await findByText('Kjøp medlemskap (300 NOK)')).toBeTruthy();
|
||||||
|
expect(getByRole('button', { name: 'Kjøp medlemskap (300 NOK)' })).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
it('purchases the preselected membership through the stripe payment sheet', async () => {
|
it('purchases the preselected membership through the stripe payment sheet', async () => {
|
||||||
mockPaymentSheetResponse();
|
mockPaymentSheetResponse();
|
||||||
const { findByText } = renderProof();
|
const { findByText } = renderProof();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
@@ -79,8 +79,10 @@ interface PurchaseButtonProps {
|
|||||||
user: UserType;
|
user: UserType;
|
||||||
paymentLoading: boolean;
|
paymentLoading: boolean;
|
||||||
isLoadingMembershipType: boolean;
|
isLoadingMembershipType: boolean;
|
||||||
membershipPrice: number;
|
membershipPrice: number | null;
|
||||||
|
membershipTypesError: boolean;
|
||||||
onPurchasePress: () => void;
|
onPurchasePress: () => void;
|
||||||
|
onRetryPress: () => void;
|
||||||
setMembershipTypeSlug: (value: string) => void;
|
setMembershipTypeSlug: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +92,8 @@ const PurchaseButton = ({
|
|||||||
isLoadingMembershipType,
|
isLoadingMembershipType,
|
||||||
onPurchasePress,
|
onPurchasePress,
|
||||||
membershipPrice,
|
membershipPrice,
|
||||||
|
membershipTypesError,
|
||||||
|
onRetryPress,
|
||||||
setMembershipTypeSlug,
|
setMembershipTypeSlug,
|
||||||
}: PurchaseButtonProps) => {
|
}: PurchaseButtonProps) => {
|
||||||
const options = [
|
const options = [
|
||||||
@@ -104,9 +108,6 @@ const PurchaseButton = ({
|
|||||||
setMembershipTypeSlug(value);
|
setMembershipTypeSlug(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
useEffect(() => onSelect(defaultValue.value), []);
|
|
||||||
|
|
||||||
if (user.is_member) {
|
if (user.is_member) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -121,11 +122,21 @@ const PurchaseButton = ({
|
|||||||
<Button
|
<Button
|
||||||
onPress={onPurchasePress}
|
onPress={onPurchasePress}
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={paymentLoading || isLoadingMembershipType}
|
disabled={paymentLoading || isLoadingMembershipType || membershipPrice == null}
|
||||||
style={[theme.button, styles.purchaseButton]}
|
style={[theme.button, styles.purchaseButton]}
|
||||||
>
|
>
|
||||||
<Text style={theme.buttonText}>{`Kjøp medlemskap (${membershipPrice} NOK)`}</Text>
|
<Text style={theme.buttonText}>
|
||||||
|
{membershipPrice != null ? `Kjøp medlemskap (${membershipPrice} NOK)` : 'Kjøp medlemskap'}
|
||||||
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
{membershipTypesError && (
|
||||||
|
<>
|
||||||
|
<Text style={[theme.text, styles.errorMessage]}>Kunne ikke hente priser</Text>
|
||||||
|
<Button small onPress={onRetryPress} style={[theme.button, styles.retryButton]}>
|
||||||
|
<Text style={theme.buttonText}>Prøv igjen</Text>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<View style={styles.purchaseText}>
|
<View style={styles.purchaseText}>
|
||||||
<Text style={[theme.text, styles.purchaseTextInner]}>Medlemskapet er gyldig i ett år</Text>
|
<Text style={[theme.text, styles.purchaseTextInner]}>Medlemskapet er gyldig i ett år</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -256,6 +267,8 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }:
|
|||||||
isLoadingMembershipType,
|
isLoadingMembershipType,
|
||||||
onPurchasePress,
|
onPurchasePress,
|
||||||
membershipPrice,
|
membershipPrice,
|
||||||
|
membershipTypesError,
|
||||||
|
refetchMembershipTypes,
|
||||||
toast,
|
toast,
|
||||||
setToast,
|
setToast,
|
||||||
user,
|
user,
|
||||||
@@ -314,7 +327,13 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }:
|
|||||||
paymentLoading={paymentLoading}
|
paymentLoading={paymentLoading}
|
||||||
isLoadingMembershipType={isLoadingMembershipType}
|
isLoadingMembershipType={isLoadingMembershipType}
|
||||||
membershipPrice={membershipPrice}
|
membershipPrice={membershipPrice}
|
||||||
|
membershipTypesError={membershipTypesError}
|
||||||
onPurchasePress={onPurchasePress}
|
onPurchasePress={onPurchasePress}
|
||||||
|
onRetryPress={() => {
|
||||||
|
refetchMembershipTypes().catch(() => {
|
||||||
|
// the failure is surfaced through useQuery's error state
|
||||||
|
});
|
||||||
|
}}
|
||||||
setMembershipTypeSlug={setMembershipTypeSlug}
|
setMembershipTypeSlug={setMembershipTypeSlug}
|
||||||
/>
|
/>
|
||||||
<MembershipValidTo user={user} />
|
<MembershipValidTo user={user} />
|
||||||
@@ -404,6 +423,10 @@ const styles = StyleSheet.create({
|
|||||||
purchaseButton: {
|
purchaseButton: {
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
},
|
},
|
||||||
|
retryButton: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
logoutButton: {
|
logoutButton: {
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,18 +24,20 @@ interface UseProofProps {
|
|||||||
const useProof = ({ fetchUser }: UseProofProps) => {
|
const useProof = ({ fetchUser }: UseProofProps) => {
|
||||||
const user = useAppSelector((state) => state.user);
|
const user = useAppSelector((state) => state.user);
|
||||||
const userToken = useAppSelector((state) => state.userToken);
|
const userToken = useAppSelector((state) => state.userToken);
|
||||||
const { data, loading } = useQuery(PROOF_QUERY);
|
const { data, loading, error, refetch } = useQuery(PROOF_QUERY);
|
||||||
const { initPaymentSheet, presentPaymentSheet } = useStripe();
|
const { initPaymentSheet, presentPaymentSheet } = useStripe();
|
||||||
const { scheduleMembershipExpiryNotification, hasPermission } = useNotifications();
|
const { scheduleMembershipExpiryNotification, hasPermission } = useNotifications();
|
||||||
const [paymentLoading, setPaymentLoading] = useState(false);
|
const [paymentLoading, setPaymentLoading] = useState(false);
|
||||||
const [membershipTypeSlug, setMembershipTypeSlug] = useState('standard');
|
// Matches the preselected option in PurchaseButton's radio list
|
||||||
|
const [membershipTypeSlug, setMembershipTypeSlug] = useState('regular');
|
||||||
const [toast, setToast] = useState<ToastProps | null>(null);
|
const [toast, setToast] = useState<ToastProps | null>(null);
|
||||||
|
|
||||||
// WARNING: Hardcoded fallback price for when the query hasn't loaded yet. Should probably be fixed.
|
// null until the query has delivered a price for the selected type; the
|
||||||
|
// purchase button stays disabled rather than showing a guessed amount
|
||||||
const membershipPrice =
|
const membershipPrice =
|
||||||
data?.membershipTypes?.find(
|
data?.membershipTypes?.find(
|
||||||
(membershipTypeOption: { slug: string; price: number }) => membershipTypeOption.slug === membershipTypeSlug,
|
(membershipTypeOption: { slug: string; price: number }) => membershipTypeOption.slug === membershipTypeSlug,
|
||||||
)?.price ?? 25000;
|
)?.price ?? null;
|
||||||
|
|
||||||
const openPaymentSheet = async () => {
|
const openPaymentSheet = async () => {
|
||||||
setPaymentLoading(true);
|
setPaymentLoading(true);
|
||||||
@@ -94,7 +96,9 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isLoadingMembershipType: loading,
|
isLoadingMembershipType: loading,
|
||||||
membershipPrice: membershipPrice ? membershipPrice / 100 : 0,
|
membershipPrice: membershipPrice != null ? membershipPrice / 100 : null,
|
||||||
|
membershipTypesError: !!error,
|
||||||
|
refetchMembershipTypes: refetch,
|
||||||
onPurchasePress: openPaymentSheet,
|
onPurchasePress: openPaymentSheet,
|
||||||
paymentLoading,
|
paymentLoading,
|
||||||
toast,
|
toast,
|
||||||
|
|||||||
Reference in new issue
Block a user