Test and fix type issues for membership type selection, remove unused react-native-simple-radio-button
This commit is contained in:
6 files changed
+69
-29
No files matched your search
@@ -82,10 +82,6 @@ history, code and binary blobs related to React Native functionality filtered ou
|
||||
`src/reducers.ts`, the fetch thunks in `src/api.ts` and the `isFetching*`/error flags
|
||||
- replace [moment](https://momentjs.com/docs/#/-project-status/) (maintenance mode) with
|
||||
`date-fns`
|
||||
- replace unmaintained UI packages:
|
||||
- `react-native-simple-radio-button` (last published in 2018, and untyped — it accounts for
|
||||
5 of the errors `npm run check-types` reports); `@expo/ui` ships a native `Picker` in
|
||||
segmented/radio style, see the SDK 56 follow-ups below
|
||||
- follow-ups from the [Expo SDK 54 upgrade](https://expo.dev/changelog/sdk-54):
|
||||
- decide on Sentry: `@sentry/react-native` is installed but never initialized (see the
|
||||
commented-out init in `index.ts`), and its config plugin (now applied via `app.json`)
|
||||
@@ -127,7 +123,9 @@ history, code and binary blobs related to React Native functionality filtered ou
|
||||
`expo-build-properties` if it causes trouble
|
||||
- now on TypeScript 6: `baseUrl` is deprecated (gone in TS 7) and was removed from
|
||||
`tsconfig.json`, and TS 6 stopped auto-including `@types/*`, hence the explicit `types` field
|
||||
- fix the 19 pre-existing `npm run check-types` errors (unchanged by the SDK upgrade)
|
||||
- fix the 13 remaining `npm run check-types` errors: the `*.svg` module declaration in
|
||||
`src/types/svg.ts` isn't picked up (12 errors), plus one expo-notifications
|
||||
`NotificationBehavior` mismatch in `src/App.tsx`
|
||||
- add [Apple Pay](https://docs.stripe.com/apple-pay?platform=react-native) for membership
|
||||
payments: either natively in the payment sheet (register the merchant ID
|
||||
`merchant.no.neuf.chateau` + Apple Pay certificate, set `merchantIdentifier` in the
|
||||
|
||||
Generated
-19
@@ -36,7 +36,6 @@
|
||||
"react-native-reanimated": "4.3.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-simple-radio-button": "^2.7.4",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-svg-transformer": "^1.5.3",
|
||||
"react-native-worklets": "0.8.3",
|
||||
@@ -51,7 +50,6 @@
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "~19.2.10",
|
||||
"@types/react-native-simple-radio-button": "^2.7.7",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-expo": "~56.0.4",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@@ -4109,17 +4107,6 @@
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-native-simple-radio-button": {
|
||||
"version": "2.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native-simple-radio-button/-/react-native-simple-radio-button-2.7.7.tgz",
|
||||
"integrity": "sha512-cDDpMb+MyYDrZmQX/phRdpbW4SJynTv165MWkwjqa39jbuSkMAxuvBdiTCTBTiugikPRvQRFBG1wPRF/+0Lprw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-test-renderer": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
|
||||
@@ -13656,12 +13643,6 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-simple-radio-button": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-simple-radio-button/-/react-native-simple-radio-button-2.7.4.tgz",
|
||||
"integrity": "sha512-QOZNmJUn0ViBGU+at7wAG/uGzd5PfXi0kMHr9lfTCLXknW/f9f2fOVjxjdHvOCdf/zA/eMJBjGjtusSDzQf1+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native-skia-android": {
|
||||
"version": "147.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-skia-android/-/react-native-skia-android-147.1.0.tgz",
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"react-native-reanimated": "4.3.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-simple-radio-button": "^2.7.4",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-svg-transformer": "^1.5.3",
|
||||
"react-native-worklets": "0.8.3",
|
||||
@@ -58,7 +57,6 @@
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "~19.2.10",
|
||||
"@types/react-native-simple-radio-button": "^2.7.7",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-expo": "~56.0.4",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
|
||||
@@ -3,10 +3,21 @@ import { Text, Pressable, StyleSheet } from 'react-native';
|
||||
|
||||
import theme from '@/theme';
|
||||
|
||||
export default function RadioButton({ data, onSelect, defaultValue }) {
|
||||
interface RadioOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface RadioButtonProps {
|
||||
data: RadioOption[];
|
||||
onSelect: (value: string) => void;
|
||||
defaultValue: RadioOption;
|
||||
}
|
||||
|
||||
export default function RadioButton({ data, onSelect, defaultValue }: RadioButtonProps) {
|
||||
const [userOption, setUserOption] = useState(defaultValue.value);
|
||||
|
||||
const selectHandler = (value) => {
|
||||
const selectHandler = (value: string) => {
|
||||
onSelect(value);
|
||||
setUserOption(value);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { fireEvent } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import Membership from '@/modules/membership/Membership';
|
||||
import { renderWithProviders, testUser } from '@/testUtils';
|
||||
import type { UserType } from '@/types/data';
|
||||
|
||||
// Must match PROOF_QUERY in useProof.tsx
|
||||
const PROOF_QUERY = gql`
|
||||
@@ -30,6 +32,56 @@ it('shows the membership pitch when logged out', async () => {
|
||||
expect(await findByText('Logg inn')).toBeTruthy();
|
||||
});
|
||||
|
||||
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 };
|
||||
|
||||
// Serve the non-member from the fake API so the refetch on mount does not
|
||||
// clobber the preloaded state.
|
||||
(global.fetch as jest.Mock).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => nonMember,
|
||||
});
|
||||
|
||||
const { findByText, getByText } = renderWithProviders(<Membership />, {
|
||||
preloadedState: {
|
||||
isAuthenticated: true,
|
||||
userToken: 'test-token',
|
||||
user: 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
|
||||
expect(await findByText('Velg medlemskapstype')).toBeTruthy();
|
||||
expect(getByText('Student')).toBeTruthy();
|
||||
expect(getByText('Ordinær')).toBeTruthy();
|
||||
expect(getByText('Støtte')).toBeTruthy();
|
||||
|
||||
// 'Ordinær' (slug: regular) is preselected, so the purchase button shows its price
|
||||
expect(await findByText('Kjøp medlemskap (300 NOK)')).toBeTruthy();
|
||||
|
||||
// Selecting another type updates the price
|
||||
fireEvent.press(getByText('Student'));
|
||||
expect(await findByText('Kjøp medlemskap (150 NOK)')).toBeTruthy();
|
||||
|
||||
fireEvent.press(getByText('Støtte'));
|
||||
expect(await findByText('Kjøp medlemskap (500 NOK)')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the membership proof when logged in', async () => {
|
||||
// Mounting Membership with a token triggers requestUserData; serve the
|
||||
// logged in user from the fake API so the refetch does not clobber state.
|
||||
|
||||
@@ -115,7 +115,7 @@ const PurchaseButton = ({
|
||||
<View>
|
||||
<Text style={[theme.text, styles.selectText]}>Velg medlemskapstype</Text>
|
||||
<View style={styles.radioForm}>
|
||||
<RadioButton data={options} onSelect={onSelect} defaultValue={defaultValue} style={styles.radioForm} />
|
||||
<RadioButton data={options} onSelect={onSelect} defaultValue={defaultValue} />
|
||||
</View>
|
||||
{paymentLoading && <ActivityIndicator color={theme.colors.neufOrange} />}
|
||||
<Button
|
||||
|
||||
Reference in new issue
Block a user