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 type { MockedResponse } from '@apollo/client/testing';
|
||||
import { fireEvent, waitFor } from '@testing-library/react-native';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import React from 'react';
|
||||
@@ -56,12 +57,16 @@ const expiredMember: UserType = {
|
||||
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(
|
||||
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isFetchingUserData={false} onDeletePress={jest.fn()} />,
|
||||
{
|
||||
preloadedState: { isAuthenticated: true, userToken: 'test-token', user },
|
||||
apolloMocks,
|
||||
apolloMocks: mocks,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -70,6 +75,30 @@ beforeEach(() => {
|
||||
(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 () => {
|
||||
mockPaymentSheetResponse();
|
||||
const { findByText } = renderProof();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
Image,
|
||||
ScrollView,
|
||||
@@ -79,8 +79,10 @@ interface PurchaseButtonProps {
|
||||
user: UserType;
|
||||
paymentLoading: boolean;
|
||||
isLoadingMembershipType: boolean;
|
||||
membershipPrice: number;
|
||||
membershipPrice: number | null;
|
||||
membershipTypesError: boolean;
|
||||
onPurchasePress: () => void;
|
||||
onRetryPress: () => void;
|
||||
setMembershipTypeSlug: (value: string) => void;
|
||||
}
|
||||
|
||||
@@ -90,6 +92,8 @@ const PurchaseButton = ({
|
||||
isLoadingMembershipType,
|
||||
onPurchasePress,
|
||||
membershipPrice,
|
||||
membershipTypesError,
|
||||
onRetryPress,
|
||||
setMembershipTypeSlug,
|
||||
}: PurchaseButtonProps) => {
|
||||
const options = [
|
||||
@@ -104,9 +108,6 @@ const PurchaseButton = ({
|
||||
setMembershipTypeSlug(value);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => onSelect(defaultValue.value), []);
|
||||
|
||||
if (user.is_member) {
|
||||
return null;
|
||||
}
|
||||
@@ -121,11 +122,21 @@ const PurchaseButton = ({
|
||||
<Button
|
||||
onPress={onPurchasePress}
|
||||
fullWidth
|
||||
disabled={paymentLoading || isLoadingMembershipType}
|
||||
disabled={paymentLoading || isLoadingMembershipType || membershipPrice == null}
|
||||
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>
|
||||
{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}>
|
||||
<Text style={[theme.text, styles.purchaseTextInner]}>Medlemskapet er gyldig i ett år</Text>
|
||||
</View>
|
||||
@@ -256,6 +267,8 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }:
|
||||
isLoadingMembershipType,
|
||||
onPurchasePress,
|
||||
membershipPrice,
|
||||
membershipTypesError,
|
||||
refetchMembershipTypes,
|
||||
toast,
|
||||
setToast,
|
||||
user,
|
||||
@@ -314,7 +327,13 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }:
|
||||
paymentLoading={paymentLoading}
|
||||
isLoadingMembershipType={isLoadingMembershipType}
|
||||
membershipPrice={membershipPrice}
|
||||
membershipTypesError={membershipTypesError}
|
||||
onPurchasePress={onPurchasePress}
|
||||
onRetryPress={() => {
|
||||
refetchMembershipTypes().catch(() => {
|
||||
// the failure is surfaced through useQuery's error state
|
||||
});
|
||||
}}
|
||||
setMembershipTypeSlug={setMembershipTypeSlug}
|
||||
/>
|
||||
<MembershipValidTo user={user} />
|
||||
@@ -404,6 +423,10 @@ const styles = StyleSheet.create({
|
||||
purchaseButton: {
|
||||
marginTop: 8,
|
||||
},
|
||||
retryButton: {
|
||||
alignSelf: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
logoutButton: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
@@ -24,18 +24,20 @@ interface UseProofProps {
|
||||
const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
const user = useAppSelector((state) => state.user);
|
||||
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 { scheduleMembershipExpiryNotification, hasPermission } = useNotifications();
|
||||
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);
|
||||
|
||||
// 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 =
|
||||
data?.membershipTypes?.find(
|
||||
(membershipTypeOption: { slug: string; price: number }) => membershipTypeOption.slug === membershipTypeSlug,
|
||||
)?.price ?? 25000;
|
||||
)?.price ?? null;
|
||||
|
||||
const openPaymentSheet = async () => {
|
||||
setPaymentLoading(true);
|
||||
@@ -94,7 +96,9 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
|
||||
return {
|
||||
isLoadingMembershipType: loading,
|
||||
membershipPrice: membershipPrice ? membershipPrice / 100 : 0,
|
||||
membershipPrice: membershipPrice != null ? membershipPrice / 100 : null,
|
||||
membershipTypesError: !!error,
|
||||
refetchMembershipTypes: refetch,
|
||||
onPurchasePress: openPaymentSheet,
|
||||
paymentLoading,
|
||||
toast,
|
||||
|
||||
Reference in new issue
Block a user