diff --git a/src/modules/membership/Proof.test.tsx b/src/modules/membership/Proof.test.tsx
index 34a5978..f6e57af 100644
--- a/src/modules/membership/Proof.test.tsx
+++ b/src/modules/membership/Proof.test.tsx
@@ -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(
,
{
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();
diff --git a/src/modules/membership/Proof.tsx b/src/modules/membership/Proof.tsx
index 5853b93..a309531 100644
--- a/src/modules/membership/Proof.tsx
+++ b/src/modules/membership/Proof.tsx
@@ -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 = ({
+ {membershipTypesError && (
+ <>
+ Kunne ikke hente priser
+
+ >
+ )}
Medlemskapet er gyldig i ett år
@@ -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}
/>
@@ -404,6 +423,10 @@ const styles = StyleSheet.create({
purchaseButton: {
marginTop: 8,
},
+ retryButton: {
+ alignSelf: 'center',
+ marginTop: 8,
+ },
logoutButton: {
marginBottom: 8,
},
diff --git a/src/modules/membership/useProof.tsx b/src/modules/membership/useProof.tsx
index b17b173..2a0f4f5 100644
--- a/src/modules/membership/useProof.tsx
+++ b/src/modules/membership/useProof.tsx
@@ -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(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,