Replace apollo with tanstack query
This commit is contained in:
12 files changed
+135
-326
No files matched your search
@@ -1,10 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { fireEvent } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
import { RefreshControl } from 'react-native';
|
||||
|
||||
import Membership from '@/modules/membership/Membership';
|
||||
import { renderWithProviders, testUser } from '@/testUtils';
|
||||
import { mockMembershipTypesOnce, renderWithProviders, testUser } from '@/testUtils';
|
||||
import type { UserType } from '@/types/data';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
@@ -13,25 +12,6 @@ jest.mock('@react-navigation/native', () => ({
|
||||
useNavigation: () => ({ navigate: mockNavigate }),
|
||||
}));
|
||||
|
||||
// Must match PROOF_QUERY in useProof.tsx
|
||||
const PROOF_QUERY = gql`
|
||||
{
|
||||
membershipTypes {
|
||||
slug
|
||||
price
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const apolloMocks = [
|
||||
{
|
||||
request: { query: PROOF_QUERY },
|
||||
result: {
|
||||
data: { membershipTypes: [{ __typename: 'MembershipType', slug: 'standard', price: 25000 }] },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('shows the membership pitch when logged out', async () => {
|
||||
const { findByText } = renderWithProviders(<Membership />);
|
||||
|
||||
@@ -42,23 +22,10 @@ it('shows the membership pitch when logged out', async () => {
|
||||
it('lets a logged in non-member pick a membership type and shows its price', async () => {
|
||||
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
||||
|
||||
mockMembershipTypesOnce();
|
||||
const { findByText, getByText } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: nonMember,
|
||||
apolloMocks: [
|
||||
{
|
||||
request: { query: PROOF_QUERY },
|
||||
result: {
|
||||
data: {
|
||||
membershipTypes: [
|
||||
{ __typename: 'MembershipType', slug: 'student', price: 15000 },
|
||||
{ __typename: 'MembershipType', slug: 'regular', price: 30000 },
|
||||
{ __typename: 'MembershipType', slug: 'support', price: 50000 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// All three membership types are offered
|
||||
@@ -79,10 +46,10 @@ it('lets a logged in non-member pick a membership type and shows its price', asy
|
||||
});
|
||||
|
||||
it('shows the membership proof when logged in', async () => {
|
||||
mockMembershipTypesOnce();
|
||||
const { findByText } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: testUser,
|
||||
apolloMocks,
|
||||
});
|
||||
|
||||
expect(await findByText('MEDLEM')).toBeTruthy();
|
||||
@@ -90,15 +57,16 @@ it('shows the membership proof when logged in', async () => {
|
||||
});
|
||||
|
||||
it('fetches the user with the stored token when only the token survives a cold start', async () => {
|
||||
(global.fetch as jest.Mock).mockResolvedValue({
|
||||
// The user fetch resolves first; Proof then mounts and fetches membership types
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => testUser,
|
||||
});
|
||||
mockMembershipTypesOnce();
|
||||
|
||||
const { findByText } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
apolloMocks,
|
||||
});
|
||||
|
||||
expect(await findByText('MEDLEM')).toBeTruthy();
|
||||
@@ -116,10 +84,10 @@ it('refetches and renders updated user data on pull-to-refresh', async () => {
|
||||
const fetchMock = global.fetch as jest.Mock;
|
||||
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
||||
|
||||
mockMembershipTypesOnce();
|
||||
const { findByText, UNSAFE_getByType } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: nonMember,
|
||||
apolloMocks,
|
||||
});
|
||||
expect(await findByText('Velg medlemskapstype')).toBeTruthy();
|
||||
|
||||
@@ -146,17 +114,16 @@ it('signs out when the stored token has been revoked', async () => {
|
||||
|
||||
const { findByText } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'revoked-token',
|
||||
apolloMocks,
|
||||
});
|
||||
|
||||
expect(await findByText('Bli medlem')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('logs out and shows the logged-out membership screen again', async () => {
|
||||
mockMembershipTypesOnce();
|
||||
const { findByText } = renderWithProviders(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: testUser,
|
||||
apolloMocks,
|
||||
});
|
||||
|
||||
fireEvent.press(await findByText('Logg ut'));
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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';
|
||||
|
||||
import Proof from '@/modules/membership/Proof';
|
||||
import { renderWithProviders, testUser } from '@/testUtils';
|
||||
import { mockMembershipTypesOnce, renderWithProviders, testUser } from '@/testUtils';
|
||||
import type { UserType } from '@/types/data';
|
||||
|
||||
// The setup-wide stripe mock resolves initPaymentSheet/presentPaymentSheet
|
||||
@@ -24,31 +22,6 @@ const mockPaymentSheetResponse = () =>
|
||||
json: async () => ({ paymentIntent: 'pi_secret', ephemeralKey: 'ek_secret', customer: 'cus_123' }),
|
||||
});
|
||||
|
||||
// Must match PROOF_QUERY in useProof.tsx
|
||||
const PROOF_QUERY = gql`
|
||||
{
|
||||
membershipTypes {
|
||||
slug
|
||||
price
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const apolloMocks = [
|
||||
{
|
||||
request: { query: PROOF_QUERY },
|
||||
result: {
|
||||
data: {
|
||||
membershipTypes: [
|
||||
{ __typename: 'MembershipType', slug: 'student', price: 15000 },
|
||||
{ __typename: 'MembershipType', slug: 'regular', price: 30000 },
|
||||
{ __typename: 'MembershipType', slug: 'support', price: 50000 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
||||
|
||||
const expiredMember: UserType = {
|
||||
@@ -57,19 +30,18 @@ const expiredMember: UserType = {
|
||||
last_membership: { membership_type: 'standard', end_date: '2020-01-01' },
|
||||
};
|
||||
|
||||
const renderProof = ({
|
||||
user = nonMember,
|
||||
fetchUser = jest.fn(),
|
||||
mocks = apolloMocks,
|
||||
}: { user?: UserType; fetchUser?: () => void; mocks?: readonly MockedResponse[] } = {}) =>
|
||||
renderWithProviders(
|
||||
// The membership type query fires on mount, so its response must be queued
|
||||
// before render; later mockResolvedValueOnce calls queue up behind it.
|
||||
const renderProof = ({ user = nonMember, fetchUser = jest.fn() }: { user?: UserType; fetchUser?: () => void } = {}) => {
|
||||
mockMembershipTypesOnce();
|
||||
return renderWithProviders(
|
||||
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isRefetching={false} onDeletePress={jest.fn()} />,
|
||||
{
|
||||
initialToken: 'test-token',
|
||||
initialUser: user,
|
||||
apolloMocks: mocks,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -87,9 +59,8 @@ it('disables the purchase button without showing a price while membership types
|
||||
});
|
||||
|
||||
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],
|
||||
});
|
||||
fetchMock.mockRejectedValueOnce(new Error('network down'));
|
||||
const { findByText, getByText, getByRole } = renderProof();
|
||||
|
||||
expect(await findByText('Kunne ikke hente priser')).toBeTruthy();
|
||||
expect(getByRole('button', { name: 'Kjøp medlemskap' })).toBeDisabled();
|
||||
@@ -101,8 +72,8 @@ it('shows an error with retry instead of a price when membership types fail to l
|
||||
});
|
||||
|
||||
it('purchases the preselected membership through the stripe payment sheet', async () => {
|
||||
mockPaymentSheetResponse();
|
||||
const { findByText } = renderProof();
|
||||
mockPaymentSheetResponse();
|
||||
|
||||
// 'Ordinær' (slug: regular) is preselected; the button enables once the
|
||||
// membership type query has resolved and its price is shown
|
||||
@@ -132,8 +103,8 @@ it('purchases the preselected membership through the stripe payment sheet', asyn
|
||||
});
|
||||
|
||||
it('purchases the selected membership type, not the default', async () => {
|
||||
mockPaymentSheetResponse();
|
||||
const { findByText } = renderProof();
|
||||
mockPaymentSheetResponse();
|
||||
|
||||
fireEvent.press(await findByText('Student'));
|
||||
fireEvent.press(await findByText('Kjøp medlemskap (150 NOK)'));
|
||||
@@ -146,11 +117,11 @@ it('purchases the selected membership type, not the default', async () => {
|
||||
});
|
||||
|
||||
it('shows the stripe error when the payment sheet is not completed', async () => {
|
||||
mockPaymentSheetResponse();
|
||||
presentPaymentSheet.mockResolvedValueOnce({
|
||||
error: { code: 'Canceled', message: 'The payment flow has been canceled' },
|
||||
});
|
||||
const { findByText, queryByText } = renderProof();
|
||||
mockPaymentSheetResponse();
|
||||
|
||||
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
||||
|
||||
@@ -160,12 +131,12 @@ it('shows the stripe error when the payment sheet is not completed', async () =>
|
||||
|
||||
it('shows an error and does not present the sheet when the backend rejects the purchase', async () => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const { findByText, getByRole } = renderProof();
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: async () => ({ detail: 'Ugyldig medlemskapstype' }),
|
||||
});
|
||||
const { findByText, getByRole } = renderProof();
|
||||
|
||||
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
||||
|
||||
@@ -178,9 +149,9 @@ it('shows an error and does not present the sheet when the backend rejects the p
|
||||
|
||||
it('shows an error and does not present the sheet when stripe fails to initialise it', async () => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
mockPaymentSheetResponse();
|
||||
initPaymentSheet.mockResolvedValueOnce({ error: { code: 'Failed', message: 'Invalid client secret' } });
|
||||
const { findByText } = renderProof();
|
||||
mockPaymentSheetResponse();
|
||||
|
||||
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
||||
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { useStripe } from '@stripe/stripe-react-native';
|
||||
import Constants from 'expo-constants';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { useMembershipTypes } from '@/queries/useMembershipTypes';
|
||||
import { usePaymentSheetMutation } from '@/queries/usePaymentSheetMutation';
|
||||
import { useUser } from '@/queries/useUser';
|
||||
import { ToastProps } from '@/types/toast';
|
||||
|
||||
const PROOF_QUERY = gql`
|
||||
{
|
||||
membershipTypes {
|
||||
slug
|
||||
price
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface UseProofProps {
|
||||
fetchUser: () => void;
|
||||
}
|
||||
|
||||
const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
const { user } = useUser();
|
||||
const { data, loading, error, refetch } = useQuery(PROOF_QUERY);
|
||||
const { data: membershipTypes, isPending, isError, refetch } = useMembershipTypes();
|
||||
const { initPaymentSheet, presentPaymentSheet } = useStripe();
|
||||
const { mutateAsync: createPaymentSheet } = usePaymentSheetMutation();
|
||||
const { scheduleMembershipExpiryNotification, hasPermission } = useNotifications();
|
||||
@@ -35,9 +26,7 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
// 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 ?? null;
|
||||
membershipTypes?.find((membershipTypeOption) => membershipTypeOption.slug === membershipTypeSlug)?.price ?? null;
|
||||
|
||||
const openPaymentSheet = async () => {
|
||||
setPaymentLoading(true);
|
||||
@@ -92,9 +81,9 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
}, [membershipType, membershipEndDate, fetchUser, hasPermission, scheduleMembershipExpiryNotification]);
|
||||
|
||||
return {
|
||||
isLoadingMembershipType: loading,
|
||||
isLoadingMembershipType: isPending,
|
||||
membershipPrice: membershipPrice != null ? membershipPrice / 100 : null,
|
||||
membershipTypesError: !!error,
|
||||
membershipTypesError: isError,
|
||||
refetchMembershipTypes: refetch,
|
||||
onPurchasePress: openPaymentSheet,
|
||||
paymentLoading,
|
||||
|
||||
Reference in new issue
Block a user