diff --git a/README.md b/README.md index 66dec5b..760e138 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 17517a5..c4e81b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5fcf090..fc96227 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/RadioButton.tsx b/src/components/RadioButton.tsx index 4542ab4..232b60e 100644 --- a/src/components/RadioButton.tsx +++ b/src/components/RadioButton.tsx @@ -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); }; diff --git a/src/modules/membership/Membership.test.tsx b/src/modules/membership/Membership.test.tsx index 92ffddd..a03acf5 100644 --- a/src/modules/membership/Membership.test.tsx +++ b/src/modules/membership/Membership.test.tsx @@ -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(, { + 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. diff --git a/src/modules/membership/Proof.tsx b/src/modules/membership/Proof.tsx index 620228b..5853b93 100644 --- a/src/modules/membership/Proof.tsx +++ b/src/modules/membership/Proof.tsx @@ -115,7 +115,7 @@ const PurchaseButton = ({ Velg medlemskapstype - + {paymentLoading && }