Skip to content

Commit 5554912

Browse files
authored
fix(stablesats): region restriction is enforced inconsistently and can be bypassed
(#3847)
1 parent 54c5358 commit 5554912

27 files changed

Lines changed: 797 additions & 188 deletions

.storybook/views/story-screen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const PersistentStateWrapper: React.FC<React.PropsWithChildren> = ({ children })
55
<PersistentStateContext.Provider
66
value={{
77
persistentState: {
8-
schemaVersion: 13,
8+
schemaVersion: 14,
99
galoyInstance: {
1010
id: "Main",
1111
},

__tests__/components/stablesats-restriction-modal/stablesats-restriction-modal.spec.tsx

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -77,41 +77,6 @@ describe("StablesatsRestrictionModal", () => {
7777
expect(mockNavigate).not.toHaveBeenCalled()
7878
})
7979

80-
it("fires onDismiss when Close is pressed", () => {
81-
const onDismiss = jest.fn()
82-
const { getByText } = render(
83-
wrap(
84-
<StablesatsRestrictionModal
85-
isVisible={true}
86-
toggleModal={jest.fn()}
87-
onDismiss={onDismiss}
88-
/>,
89-
),
90-
)
91-
92-
fireEvent.press(getByText("Close"))
93-
94-
expect(onDismiss).toHaveBeenCalledTimes(1)
95-
})
96-
97-
it("does not fire onDismiss when Create new is pressed (navigates instead)", () => {
98-
const onDismiss = jest.fn()
99-
const { getByText } = render(
100-
wrap(
101-
<StablesatsRestrictionModal
102-
isVisible={true}
103-
toggleModal={jest.fn()}
104-
onDismiss={onDismiss}
105-
/>,
106-
),
107-
)
108-
109-
fireEvent.press(getByText("Create new"))
110-
111-
expect(onDismiss).not.toHaveBeenCalled()
112-
expect(mockNavigate).toHaveBeenCalledWith("getStarted")
113-
})
114-
11580
it("renders nothing when isVisible is false", () => {
11681
const { queryByText } = render(
11782
wrap(<StablesatsRestrictionModal isVisible={false} toggleModal={jest.fn()} />),

__tests__/components/usd-convert-to-btc-modal/usd-convert-to-btc-modal.spec.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ jest.mock("@app/hooks/use-price-conversion", () => ({
3232
}))
3333

3434
const mockExecute = jest.fn()
35-
const mockUseIntraLedgerConversion = jest.fn(() => ({
35+
const mockUseIntraLedgerConversion = jest.fn((_config?: { onSuccess: () => void }) => ({
3636
execute: mockExecute,
3737
loading: false,
3838
errorMessage: undefined as string | undefined,
3939
}))
4040

4141
jest.mock("@app/hooks/use-intra-ledger-conversion", () => ({
42-
useIntraLedgerConversion: () => mockUseIntraLedgerConversion(),
42+
useIntraLedgerConversion: (config: { onSuccess: () => void }) =>
43+
mockUseIntraLedgerConversion(config),
4344
}))
4445

4546
import { UsdConvertToBtcModal } from "@app/components/usd-convert-to-btc-modal"
@@ -146,4 +147,16 @@ describe("UsdConvertToBtcModal", () => {
146147

147148
expect(queryByTestId("icon-close")).toBeNull()
148149
})
150+
151+
it("closes only on success: the conversion onSuccess handler is wired to toggleModal", () => {
152+
const toggleModal = jest.fn()
153+
renderModal({ toggleModal })
154+
155+
const { onSuccess } = mockUseIntraLedgerConversion.mock.calls[0][0] as {
156+
onSuccess: () => void
157+
}
158+
onSuccess()
159+
160+
expect(toggleModal).toHaveBeenCalledTimes(1)
161+
})
149162
})

__tests__/hooks/use-default-account-modal-shown.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jest.mock("@app/store/persistent-state", () => ({
1414
}))
1515

1616
const baseState: PersistentState = {
17-
schemaVersion: 13,
17+
schemaVersion: 14,
1818
galoyInstance: { id: "Main" },
1919
galoyAuthToken: "",
2020
}

__tests__/hooks/use-device-location.spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { renderHook, act } from "@testing-library/react-hooks"
22
import axios from "axios"
33

4-
import useDeviceLocation from "@app/hooks/use-device-location"
4+
import useDeviceLocation, { useIpCountryCode } from "@app/hooks/use-device-location"
55

66
const mockLogError = jest.fn()
77
const mockUpdateCountryCode = jest.fn()
@@ -62,6 +62,7 @@ describe("useDeviceLocation", () => {
6262
expect(result.current.loading).toBe(false)
6363
expect(result.current.countryCode).toBe("DE")
6464
expect(result.current.detectionFailed).toBe(false)
65+
expect(result.current.source).toBe("phone")
6566
expect(mockedAxios.get).not.toHaveBeenCalled()
6667
})
6768

@@ -123,6 +124,7 @@ describe("useDeviceLocation", () => {
123124
expect(result.current.loading).toBe(false)
124125
expect(result.current.countryCode).toBe("PL")
125126
expect(result.current.detectionFailed).toBe(false)
127+
expect(result.current.source).toBe("ip")
126128
expect(mockedAxios.get).toHaveBeenCalled()
127129
})
128130

@@ -256,3 +258,39 @@ describe("useDeviceLocation", () => {
256258
expect(mockLogError).toHaveBeenCalled()
257259
})
258260
})
261+
262+
describe("useIpCountryCode", () => {
263+
beforeEach(() => {
264+
jest.clearAllMocks()
265+
})
266+
267+
it("does not call ipapi while disabled", () => {
268+
const { result } = renderHook(() => useIpCountryCode(false))
269+
270+
expect(mockedAxios.get).not.toHaveBeenCalled()
271+
expect(result.current).toBeUndefined()
272+
})
273+
274+
it("resolves the country from ipapi when enabled", async () => {
275+
// eslint-disable-next-line camelcase
276+
mockedAxios.get.mockResolvedValue({ data: { country_code: "HK" } })
277+
278+
const { result } = renderHook(() => useIpCountryCode(true))
279+
280+
await act(async () => {})
281+
282+
expect(mockedAxios.get).toHaveBeenCalled()
283+
expect(result.current).toBe("HK")
284+
})
285+
286+
it("stays undefined when ipapi fails", async () => {
287+
mockedAxios.get.mockRejectedValue(new Error("403 Forbidden"))
288+
289+
const { result } = renderHook(() => useIpCountryCode(true))
290+
291+
await act(async () => {})
292+
293+
expect(result.current).toBeUndefined()
294+
expect(mockLogError).toHaveBeenCalled()
295+
})
296+
})

0 commit comments

Comments
 (0)