From 770d09ac8c05e0a6e15d0023270b464c119cdeab Mon Sep 17 00:00:00 2001
From: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com>
Date: Mon, 27 Nov 2023 15:44:57 +0000
Subject: [PATCH] wip: claim feature
---
.../.storybook/storybook.requires.js | 6 +
.../src/TxHistory/TxHistoryNavigator.tsx | 428 +++++------
.../src/api/useApiErrorResolver.tsx | 27 +
apps/wallet-mobile/src/api/useDialogsApi.tsx | 62 ++
.../src/api/useStringsApiErrors.tsx | 26 +
.../LoadingOverlay/LoadingOverlay.tsx | 7 +-
.../src/components/Modal/ModalContext.tsx | 22 +-
.../src/components/Modal/ModalScreen.tsx | 13 +-
.../ModalScreenWrapper.stories.tsx | 23 +
.../ModalScreenWrapper/ModalScreenWrapper.tsx | 141 ++++
.../PressableIcon/PressableIcon.stories.tsx | 10 +
.../PressableIcon/PressableIcon.tsx | 11 +
.../Claim/common/useClaimErrorResolver.tsx | 29 +
.../src/features/Claim/common/useDialogs.tsx | 47 ++
.../features/Claim/common/useNavigateTo.tsx | 14 +
.../src/features/Claim/common/useStrings.tsx | 122 ++++
.../ClaimSuccessIllustration.tsx | 79 +++
.../illustrations/Ilustrations.stories.tsx | 8 +
.../features/Claim/module/ClaimProvider.tsx | 52 ++
.../features/Claim/module/api-faucet.mocks.ts | 34 +
.../src/features/Claim/module/api.mocks.ts | 78 ++
.../src/features/Claim/module/api.tests.ts | 16 +
.../src/features/Claim/module/api.ts | 48 ++
.../src/features/Claim/module/errors.ts | 28 +
.../src/features/Claim/module/state.mocks.ts | 37 +
.../src/features/Claim/module/state.ts | 57 ++
.../src/features/Claim/module/transformers.ts | 47 ++
.../src/features/Claim/module/types.ts | 52 ++
.../features/Claim/module/useClaimTokens.tsx | 21 +
.../src/features/Claim/module/validators.ts | 33 +
.../useCases/AskConfirmation.stories.tsx | 9 +
.../Claim/useCases/AskConfirmation.tsx | 122 ++++
.../useCases/ShowSuccessScreen.stories.tsx | 44 ++
.../Claim/useCases/ShowSuccessScreen.tsx | 187 +++++
.../Scan/common/CodeScannerButton.tsx | 14 +-
.../src/features/Scan/common/types.ts | 51 +-
.../features/Scan/common/useNavigateTo.tsx | 1 +
.../Scan/common/useTriggerScanAction.tsx | 49 +-
.../CameraPermissionDeniedIlustration.tsx | 0
.../illustrations/Ilustrations.stories.tsx | 8 +
.../ShowCameraPermissionDeniedScreen.tsx | 2 +-
.../wallet-mobile/src/i18n/global-messages.ts | 59 ++
.../wallet-mobile/src/i18n/locales/en-US.json | 32 +
apps/wallet-mobile/src/navigation.tsx | 6 +-
apps/wallet-mobile/src/utils/fixtures.ts | 14 +
.../src/TxHistory/TxHistoryNavigator.json | 135 ++--
.../src/features/Claim/common/useStrings.json | 287 ++++++++
.../messages/src/i18n/global-messages.json | 666 ++++++++++++------
metro.config.js | 1 +
packages/common/src/api/fetchData.test.ts | 104 +++
packages/common/src/api/fetchData.ts | 106 +++
packages/common/src/api/fetcher.ts | 4 +
.../common/src/api/handleApiError.test.ts | 85 +++
packages/common/src/api/handleApiError.ts | 32 +
packages/common/src/helpers/monads.test.ts | 27 +
packages/common/src/helpers/monads.ts | 13 +
packages/common/src/index.ts | 3 +
packages/types/src/api/errors.ts | 13 +
packages/types/src/api/response.ts | 13 +
packages/types/src/helpers/types.ts | 12 +
packages/types/src/index.ts | 39 +
61 files changed, 3178 insertions(+), 538 deletions(-)
create mode 100644 apps/wallet-mobile/src/api/useApiErrorResolver.tsx
create mode 100644 apps/wallet-mobile/src/api/useDialogsApi.tsx
create mode 100644 apps/wallet-mobile/src/api/useStringsApiErrors.tsx
create mode 100644 apps/wallet-mobile/src/components/ModalScreenWrapper/ModalScreenWrapper.stories.tsx
create mode 100644 apps/wallet-mobile/src/components/ModalScreenWrapper/ModalScreenWrapper.tsx
create mode 100644 apps/wallet-mobile/src/components/PressableIcon/PressableIcon.stories.tsx
create mode 100644 apps/wallet-mobile/src/components/PressableIcon/PressableIcon.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/common/useDialogs.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/common/useNavigateTo.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/common/useStrings.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/illustrations/ClaimSuccessIllustration.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/illustrations/Ilustrations.stories.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/module/api.mocks.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/module/api.tests.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/module/api.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/module/errors.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/module/state.mocks.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/module/state.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/module/transformers.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/module/types.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/module/validators.ts
create mode 100644 apps/wallet-mobile/src/features/Claim/useCases/AskConfirmation.stories.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/useCases/AskConfirmation.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx
create mode 100644 apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx
rename apps/wallet-mobile/src/features/Scan/{common => }/illustrations/CameraPermissionDeniedIlustration.tsx (100%)
create mode 100644 apps/wallet-mobile/src/features/Scan/illustrations/Ilustrations.stories.tsx
create mode 100644 apps/wallet-mobile/src/utils/fixtures.ts
create mode 100644 apps/wallet-mobile/translations/messages/src/features/Claim/common/useStrings.json
create mode 100644 packages/common/src/api/fetchData.test.ts
create mode 100644 packages/common/src/api/fetchData.ts
create mode 100644 packages/common/src/api/handleApiError.test.ts
create mode 100644 packages/common/src/api/handleApiError.ts
create mode 100644 packages/common/src/helpers/monads.test.ts
create mode 100644 packages/common/src/helpers/monads.ts
create mode 100644 packages/types/src/api/errors.ts
create mode 100644 packages/types/src/api/response.ts
diff --git a/apps/wallet-mobile/.storybook/storybook.requires.js b/apps/wallet-mobile/.storybook/storybook.requires.js
index 84ff74a253..d2c35fc4b2 100644
--- a/apps/wallet-mobile/.storybook/storybook.requires.js
+++ b/apps/wallet-mobile/.storybook/storybook.requires.js
@@ -86,9 +86,11 @@ const getStories = () => {
"./src/components/LanguagePicker/LanguagePickerWarning.stories.tsx": require("../src/components/LanguagePicker/LanguagePickerWarning.stories.tsx"),
"./src/components/Link/Link.stories.tsx": require("../src/components/Link/Link.stories.tsx"),
"./src/components/LoadingOverlay/LoadingOverlay.stories.tsx": require("../src/components/LoadingOverlay/LoadingOverlay.stories.tsx"),
+ "./src/components/ModalScreenWrapper/ModalScreenWrapper.stories.tsx": require("../src/components/ModalScreenWrapper/ModalScreenWrapper.stories.tsx"),
"./src/components/NftImageGallery/NftImageGallery.stories.tsx": require("../src/components/NftImageGallery/NftImageGallery.stories.tsx"),
"./src/components/NftPreview/NftPreview.stories.tsx": require("../src/components/NftPreview/NftPreview.stories.tsx"),
"./src/components/PairedBalance/PairedBalance.stories.tsx": require("../src/components/PairedBalance/PairedBalance.stories.tsx"),
+ "./src/components/PressableIcon/PressableIcon.stories.tsx": require("../src/components/PressableIcon/PressableIcon.stories.tsx"),
"./src/components/StandardModal/StandardModal.stories.tsx": require("../src/components/StandardModal/StandardModal.stories.tsx"),
"./src/components/TextInput/TextInput.stories.tsx": require("../src/components/TextInput/TextInput.stories.tsx"),
"./src/components/TokenIcon/ModeratedNftIcon.stories.tsx": require("../src/components/TokenIcon/ModeratedNftIcon.stories.tsx"),
@@ -101,12 +103,16 @@ const getStories = () => {
"./src/Dashboard/WithdrawStakingRewards/ConfirmTx/ConfirmTxWithPassword.stories.tsx": require("../src/Dashboard/WithdrawStakingRewards/ConfirmTx/ConfirmTxWithPassword.stories.tsx"),
"./src/Dashboard/WithdrawStakingRewards/TransferSummary/TransferSummary.stories.tsx": require("../src/Dashboard/WithdrawStakingRewards/TransferSummary/TransferSummary.stories.tsx"),
"./src/Dashboard/WithdrawStakingRewards/WithdrawStakingRewards.stories.tsx": require("../src/Dashboard/WithdrawStakingRewards/WithdrawStakingRewards.stories.tsx"),
+ "./src/features/Claim/illustrations/Ilustrations.stories.tsx": require("../src/features/Claim/illustrations/Ilustrations.stories.tsx"),
+ "./src/features/Claim/useCases/AskConfirmation.stories.tsx": require("../src/features/Claim/useCases/AskConfirmation.stories.tsx"),
+ "./src/features/Claim/useCases/ShowSuccessScreen.stories.tsx": require("../src/features/Claim/useCases/ShowSuccessScreen.stories.tsx"),
"./src/features/Initialization/AnalyticsChangedScreen/AnalyticsChangedScreen.stories.tsx": require("../src/features/Initialization/AnalyticsChangedScreen/AnalyticsChangedScreen.stories.tsx"),
"./src/features/Initialization/InitialScreen/InitialScreen.stories.tsx": require("../src/features/Initialization/InitialScreen/InitialScreen.stories.tsx"),
"./src/features/Initialization/LanguagePickerScreen/LanguagePickerScreen.stories.tsx": require("../src/features/Initialization/LanguagePickerScreen/LanguagePickerScreen.stories.tsx"),
"./src/features/Initialization/TermsOfServiceChangedScreen/TermsOfServiceChangedScreen.stories.tsx": require("../src/features/Initialization/TermsOfServiceChangedScreen/TermsOfServiceChangedScreen.stories.tsx"),
"./src/features/Menu/Menu.stories.tsx": require("../src/features/Menu/Menu.stories.tsx"),
"./src/features/Scan/common/CodeScannerButton.stories.tsx": require("../src/features/Scan/common/CodeScannerButton.stories.tsx"),
+ "./src/features/Scan/illustrations/Ilustrations.stories.tsx": require("../src/features/Scan/illustrations/Ilustrations.stories.tsx"),
"./src/features/Scan/useCases/ScanCodeScreen.stories.tsx": require("../src/features/Scan/useCases/ScanCodeScreen.stories.tsx"),
"./src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/OpenDeviceAppSettingsButton.stories.tsx": require("../src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/OpenDeviceAppSettingsButton.stories.tsx"),
"./src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.stories.tsx": require("../src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.stories.tsx"),
diff --git a/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx b/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx
index 8c2407a465..6ed7a9fe92 100644
--- a/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx
+++ b/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx
@@ -14,6 +14,9 @@ import {defineMessages, useIntl} from 'react-intl'
import {StyleSheet, Text, TouchableOpacity, TouchableOpacityProps, View, ViewProps} from 'react-native'
import {Boundary, Icon, Spacer} from '../components'
+import {claimApiMaker} from '../features/Claim/module/api'
+import {ClaimProvider} from '../features/Claim/module/ClaimProvider'
+import {ShowSuccessScreen} from '../features/Claim/useCases/ShowSuccessScreen'
import {CodeScannerButton} from '../features/Scan/common/CodeScannerButton'
import {ScanCodeScreen} from '../features/Scan/useCases/ScanCodeScreen'
import {ShowCameraPermissionDeniedScreen} from '../features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen'
@@ -80,216 +83,232 @@ export const TxHistoryNavigator = () => {
return swapManagerMaker({swapStorage, swapApi, frontendFeeTiers, aggregator, aggregatorTokenId})
}, [wallet.networkId, wallet.primaryTokenInfo.id, stakingKey, frontendFees, aggregatorTokenId])
+ // claim
+ const claimApi = React.useMemo(() => {
+ return claimApiMaker({
+ address: wallet.externalAddresses[0],
+ primaryTokenId: wallet.primaryTokenInfo.id,
+ })
+ }, [wallet.externalAddresses, wallet.primaryTokenInfo.id])
+
// navigator components
const headerRightHistory = React.useCallback(() => , [])
return (
-
-
-
-
-
- {() => (
-
-
-
- )}
-
-
- ,
- headerStyle: {
- elevation: 0,
- shadowOpacity: 0,
- backgroundColor: '#fff',
- },
- }}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {() => (
-
-
-
- )}
-
-
-
- {() => (
-
-
-
- )}
-
-
-
+
+
- {() => (
-
-
-
- )}
-
-
-
- {() => (
-
-
-
- )}
-
-
-
-
-
-
-
-
- ,
- }}
- />
-
-
-
-
-
- {strings.receiveInfoText}
-
-
+
+
+
+ {() => (
+
+
+
+ )}
+
+
+ ,
+ headerStyle: {
+ elevation: 0,
+ shadowOpacity: 0,
+ backgroundColor: '#fff',
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {() => (
+
+
+
+ )}
+
+
+
+ {() => (
+
+
+
+ )}
+
+
+
+ {() => (
+
+
+
+ )}
+
+
+
+ {() => (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ ,
+ }}
+ />
+
+
+
+
+
+
+
+ {strings.receiveInfoText}
+
+
+
)
@@ -359,6 +378,10 @@ const messages = defineMessages({
id: 'scan.title',
defaultMessage: '!!!Please scan a QR code',
},
+ claimShowSuccessTitle: {
+ id: 'claim.showSuccess.title',
+ defaultMessage: '!!!Success',
+ },
})
const useStrings = () => {
@@ -380,6 +403,7 @@ const useStrings = () => {
listAmountsToSendTitle: intl.formatMessage(messages.listAmountsToSendTitle),
confirmationTransaction: intl.formatMessage(messages.confirmationTransaction),
scanTitle: intl.formatMessage(messages.scanTitle),
+ claimShowSuccess: intl.formatMessage(messages.claimShowSuccessTitle),
}
}
diff --git a/apps/wallet-mobile/src/api/useApiErrorResolver.tsx b/apps/wallet-mobile/src/api/useApiErrorResolver.tsx
new file mode 100644
index 0000000000..a0c6b67651
--- /dev/null
+++ b/apps/wallet-mobile/src/api/useApiErrorResolver.tsx
@@ -0,0 +1,27 @@
+import {Api} from '@yoroi/types'
+
+import {useDialogsApi} from './useDialogsApi'
+
+export const useApiErrorResolver = () => {
+ const dialogs = useDialogsApi()
+
+ const resolver = (error: unknown) => {
+ if (error instanceof Api.Errors.BadRequest) return dialogs.errorBadRequest
+ if (error instanceof Api.Errors.Conflict) return dialogs.errorConflict
+ if (error instanceof Api.Errors.Forbidden) return dialogs.errorForbidden
+ if (error instanceof Api.Errors.Gone) return dialogs.errorGone
+ if (error instanceof Api.Errors.InvalidState) return dialogs.errorInvalidState
+ if (error instanceof Api.Errors.Network) return dialogs.errorNetwork
+ if (error instanceof Api.Errors.NotFound) return dialogs.errorNotFound
+ if (error instanceof Api.Errors.ResponseMalformed) return dialogs.errorResponseMalformed
+ if (error instanceof Api.Errors.ServerSide) return dialogs.errorServerSide
+ if (error instanceof Api.Errors.TooEarly) return dialogs.errorTooEarly
+ if (error instanceof Api.Errors.TooManyRequests) return dialogs.errorTooManyRequests
+ if (error instanceof Api.Errors.Unauthorized) return dialogs.errorUnauthorized
+ if (error instanceof Api.Errors.Unknown) return dialogs.errorUnknown
+
+ return dialogs.errorUnknown
+ }
+
+ return resolver
+}
diff --git a/apps/wallet-mobile/src/api/useDialogsApi.tsx b/apps/wallet-mobile/src/api/useDialogsApi.tsx
new file mode 100644
index 0000000000..9dc09e0341
--- /dev/null
+++ b/apps/wallet-mobile/src/api/useDialogsApi.tsx
@@ -0,0 +1,62 @@
+import * as React from 'react'
+
+import {useStringsApiErrors} from './useStringsApiErrors'
+
+export const useDialogsApi = () => {
+ const strings = useStringsApiErrors()
+
+ return React.useRef({
+ errorBadRequest: {
+ title: strings.errorTitle,
+ message: strings.badRequest,
+ },
+ errorUnauthorized: {
+ title: strings.errorTitle,
+ message: strings.unauthorized,
+ },
+ errorForbidden: {
+ title: strings.errorTitle,
+ message: strings.forbidden,
+ },
+ errorNotFound: {
+ title: strings.errorTitle,
+ message: strings.notFound,
+ },
+ errorConflict: {
+ title: strings.errorTitle,
+ message: strings.conflict,
+ },
+ errorGone: {
+ title: strings.errorTitle,
+ message: strings.gone,
+ },
+ errorTooEarly: {
+ title: strings.errorTitle,
+ message: strings.tooEarly,
+ },
+ errorTooManyRequests: {
+ title: strings.errorTitle,
+ message: strings.tooManyRequests,
+ },
+ errorServerSide: {
+ title: strings.errorTitle,
+ message: strings.serverSide,
+ },
+ errorUnknown: {
+ title: strings.errorTitle,
+ message: strings.unknown,
+ },
+ errorNetwork: {
+ title: strings.errorTitle,
+ message: strings.network,
+ },
+ errorInvalidState: {
+ title: strings.errorTitle,
+ message: strings.invalidState,
+ },
+ errorResponseMalformed: {
+ title: strings.errorTitle,
+ message: strings.responseMalformed,
+ },
+ } as const).current
+}
diff --git a/apps/wallet-mobile/src/api/useStringsApiErrors.tsx b/apps/wallet-mobile/src/api/useStringsApiErrors.tsx
new file mode 100644
index 0000000000..16b8b20265
--- /dev/null
+++ b/apps/wallet-mobile/src/api/useStringsApiErrors.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react'
+import {useIntl} from 'react-intl'
+
+import {apiErrors} from '../i18n/global-messages'
+
+export const useStringsApiErrors = () => {
+ const intl = useIntl()
+
+ return React.useRef({
+ errorTitle: intl.formatMessage(apiErrors.title),
+
+ badRequest: intl.formatMessage(apiErrors.badRequest),
+ conflict: intl.formatMessage(apiErrors.conflict),
+ forbidden: intl.formatMessage(apiErrors.forbidden),
+ gone: intl.formatMessage(apiErrors.gone),
+ invalidState: intl.formatMessage(apiErrors.invalidState),
+ network: intl.formatMessage(apiErrors.network),
+ notFound: intl.formatMessage(apiErrors.notFound),
+ responseMalformed: intl.formatMessage(apiErrors.responseMalformed),
+ serverSide: intl.formatMessage(apiErrors.serverSide),
+ tooEarly: intl.formatMessage(apiErrors.tooEarly),
+ tooManyRequests: intl.formatMessage(apiErrors.tooManyRequests),
+ unauthorized: intl.formatMessage(apiErrors.unauthorized),
+ unknown: intl.formatMessage(apiErrors.unknown),
+ } as const).current
+}
diff --git a/apps/wallet-mobile/src/components/LoadingOverlay/LoadingOverlay.tsx b/apps/wallet-mobile/src/components/LoadingOverlay/LoadingOverlay.tsx
index 08298da5d4..679a5a32c3 100644
--- a/apps/wallet-mobile/src/components/LoadingOverlay/LoadingOverlay.tsx
+++ b/apps/wallet-mobile/src/components/LoadingOverlay/LoadingOverlay.tsx
@@ -1,14 +1,15 @@
import React from 'react'
-import {ActivityIndicator, StyleSheet, View} from 'react-native'
+import {ActivityIndicator, StyleSheet, View, ViewProps} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
-export const LoadingOverlay = ({loading}: {loading: boolean}) => {
+export const LoadingOverlay = ({loading, style, ...props}: ViewProps & {loading: boolean}) => {
return loading ? (
void
closeModal: () => void
+ startLoading: () => void
+ stopLoading: () => void
}
const ModalContext = React.createContext(undefined)
@@ -40,6 +43,8 @@ export const ModalProvider = ({
dispatch({type: 'open', title, content, height})
navigation.navigate('modal')
},
+ startLoading: () => dispatch({type: 'startLoading'}),
+ stopLoading: () => dispatch({type: 'stopLoading'}),
}).current
const context = React.useMemo(() => ({...state, ...actions}), [state, actions])
@@ -50,6 +55,8 @@ export const ModalProvider = ({
type ModalAction =
| {type: 'open'; height: ModalState['height'] | undefined; content: ModalState['content']; title: ModalState['title']}
| {type: 'close'}
+ | {type: 'startLoading'}
+ | {type: 'stopLoading'}
const modalReducer = (state: ModalState, action: ModalAction) => {
switch (action.type) {
@@ -60,14 +67,27 @@ const modalReducer = (state: ModalState, action: ModalAction) => {
height: action.height ?? defaultState.height,
title: action.title,
isOpen: true,
+ isLoading: false,
}
case 'close':
return {...defaultState}
+ case 'stopLoading':
+ return {...state, isLoading: false}
+
+ case 'startLoading':
+ return {...state, isLoading: true}
+
default:
throw new Error(`modalReducer invalid action`)
}
}
-const defaultState: ModalState = Object.freeze({content: undefined, height: 350, title: '', isOpen: false})
+const defaultState: ModalState = Object.freeze({
+ content: undefined,
+ height: 350,
+ title: '',
+ isOpen: false,
+ isLoading: false,
+})
diff --git a/apps/wallet-mobile/src/components/Modal/ModalScreen.tsx b/apps/wallet-mobile/src/components/Modal/ModalScreen.tsx
index 8c4a7df543..0c69e74523 100644
--- a/apps/wallet-mobile/src/components/Modal/ModalScreen.tsx
+++ b/apps/wallet-mobile/src/components/Modal/ModalScreen.tsx
@@ -12,11 +12,12 @@ import {
} from 'react-native'
import {Spacer} from '..'
+import {LoadingOverlay} from '../LoadingOverlay/LoadingOverlay'
import {useModal} from './ModalContext'
export const ModalScreen = () => {
const {current} = useCardAnimation()
- const {height, closeModal, content, isOpen} = useModal()
+ const {height, closeModal, content, isOpen, isLoading} = useModal()
const [swipeLocationY, setSwipeLocationY] = React.useState(height)
const onResponderMove = ({nativeEvent}: GestureResponderEvent) => {
@@ -55,7 +56,9 @@ export const ModalScreen = () => {
styles.animatedView,
]}
>
-
+
+
+
true} />
{content}
@@ -103,11 +106,13 @@ const styles = StyleSheet.create({
animatedView: {
alignSelf: 'stretch',
},
+ rounded: {
+ borderTopRightRadius: 20,
+ borderTopLeftRadius: 20,
+ },
sheet: {
flex: 1,
backgroundColor: 'white',
- borderTopRightRadius: 20,
- borderTopLeftRadius: 20,
alignSelf: 'stretch',
paddingHorizontal: 16,
paddingBottom: 16,
diff --git a/apps/wallet-mobile/src/components/ModalScreenWrapper/ModalScreenWrapper.stories.tsx b/apps/wallet-mobile/src/components/ModalScreenWrapper/ModalScreenWrapper.stories.tsx
new file mode 100644
index 0000000000..ee1d14e986
--- /dev/null
+++ b/apps/wallet-mobile/src/components/ModalScreenWrapper/ModalScreenWrapper.stories.tsx
@@ -0,0 +1,23 @@
+import {action} from '@storybook/addon-actions'
+import {storiesOf} from '@storybook/react-native'
+import React from 'react'
+import {Alert, Text, View} from 'react-native'
+
+import {ModalScreenWrapper} from './ModalScreenWrapper'
+
+storiesOf('ModalScreenWrapper', module).add('initial', () => {
+ return (
+ {
+ action('onClose')
+ Alert.alert('onClose', 'Will it close?')
+ }}
+ >
+
+ Body text
+
+
+ )
+})
diff --git a/apps/wallet-mobile/src/components/ModalScreenWrapper/ModalScreenWrapper.tsx b/apps/wallet-mobile/src/components/ModalScreenWrapper/ModalScreenWrapper.tsx
new file mode 100644
index 0000000000..b1ddd2c750
--- /dev/null
+++ b/apps/wallet-mobile/src/components/ModalScreenWrapper/ModalScreenWrapper.tsx
@@ -0,0 +1,141 @@
+import {useCardAnimation} from '@react-navigation/stack'
+import React from 'react'
+import {
+ Animated,
+ GestureResponderEvent,
+ KeyboardAvoidingView,
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from 'react-native'
+
+import {Spacer} from '..'
+
+// This is another option for modals that will keep the context tree, it requires navigation and if you need to wrap it a fragment is a must
+// Tested and working but it will require a big refactor on the navigator to work with it
+type ModalScreenProps = {
+ height: number
+ title: string
+ onClose: () => void
+ children?: Exclude
+}
+export const ModalScreenWrapper = ({height, onClose, children, title}: ModalScreenProps) => {
+ const {current} = useCardAnimation()
+ const [swipeLocationY, setSwipeLocationY] = React.useState(height)
+
+ const onResponderMove = ({nativeEvent}: GestureResponderEvent) => {
+ if (swipeLocationY < nativeEvent.locationY) {
+ setSwipeLocationY(height)
+ onClose()
+ return
+ }
+
+ setSwipeLocationY(nativeEvent.locationY)
+ }
+
+ return (
+
+
+
+
+
+
+ true} />
+
+ {children}
+
+
+
+
+ )
+}
+
+const Header = ({
+ title,
+ ...props
+}: {
+ onResponderMove?: (event: GestureResponderEvent) => void
+ onStartShouldSetResponder?: () => boolean
+ title: string
+}) => {
+ return (
+
+
+
+
+
+
+
+ {title}
+
+ )
+}
+
+const SliderIndicator = () =>
+
+const styles = StyleSheet.create({
+ root: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+ alignSelf: 'stretch',
+ },
+ cancellableArea: {
+ flexGrow: 1,
+ },
+ backdrop: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ animatedView: {
+ alignSelf: 'stretch',
+ },
+ sheet: {
+ flex: 1,
+ backgroundColor: 'white',
+ borderTopRightRadius: 20,
+ borderTopLeftRadius: 20,
+ alignSelf: 'stretch',
+ paddingHorizontal: 16,
+ paddingBottom: 16,
+ },
+ title: {
+ fontWeight: '500',
+ fontFamily: 'Rubik-Medium',
+ fontSize: 20,
+ padding: 16,
+ color: '#000',
+ },
+ header: {
+ alignItems: 'center',
+ alignSelf: 'stretch',
+ },
+ slider: {
+ height: 4,
+ backgroundColor: 'black',
+ width: 32,
+ borderRadius: 10,
+ },
+})
diff --git a/apps/wallet-mobile/src/components/PressableIcon/PressableIcon.stories.tsx b/apps/wallet-mobile/src/components/PressableIcon/PressableIcon.stories.tsx
new file mode 100644
index 0000000000..8a971478b5
--- /dev/null
+++ b/apps/wallet-mobile/src/components/PressableIcon/PressableIcon.stories.tsx
@@ -0,0 +1,10 @@
+import {action} from '@storybook/addon-actions'
+import {storiesOf} from '@storybook/react-native'
+import * as React from 'react'
+
+import {Icon} from '../Icon'
+import {PressableIcon} from './PressableIcon'
+
+storiesOf('PressableIcon', module).add('Icon.ExternalLink', () => (
+
+))
diff --git a/apps/wallet-mobile/src/components/PressableIcon/PressableIcon.tsx b/apps/wallet-mobile/src/components/PressableIcon/PressableIcon.tsx
new file mode 100644
index 0000000000..9e901b145e
--- /dev/null
+++ b/apps/wallet-mobile/src/components/PressableIcon/PressableIcon.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react'
+import {TouchableOpacity, TouchableOpacityProps} from 'react-native'
+
+type PressableIconProps = {
+ size?: number
+ color?: string
+ icon: (props: {size?: number; color?: string}) => React.ReactNode
+} & TouchableOpacityProps
+export const PressableIcon = ({icon, size, color, ...props}: PressableIconProps) => (
+ {icon({size, color})}
+)
diff --git a/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx b/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx
new file mode 100644
index 0000000000..e73528c13d
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/common/useClaimErrorResolver.tsx
@@ -0,0 +1,29 @@
+import {useApiErrorResolver} from '../../../api/useApiErrorResolver'
+import {
+ ClaimApiErrorsAlreadyClaimed,
+ ClaimApiErrorsExpired,
+ ClaimApiErrorsInvalidRequest,
+ ClaimApiErrorsNotFound,
+ ClaimApiErrorsRateLimited,
+ ClaimApiErrorsTooEarly,
+} from '../module/errors'
+import {useDialogs} from './useDialogs'
+
+export const useClaimErrorResolver = () => {
+ const dialogs = useDialogs()
+ const apiResolver = useApiErrorResolver()
+
+ const resolver = (error: unknown) => {
+ if (error instanceof ClaimApiErrorsAlreadyClaimed) return dialogs.errorAlreadyClaimed
+ if (error instanceof ClaimApiErrorsExpired) return dialogs.errorExpired
+ if (error instanceof ClaimApiErrorsInvalidRequest) return dialogs.errorInvalidRequest
+ if (error instanceof ClaimApiErrorsNotFound) return dialogs.errorNotFound
+ if (error instanceof ClaimApiErrorsRateLimited) return dialogs.errorRateLimited
+ if (error instanceof ClaimApiErrorsTooEarly) return dialogs.errorTooEarly
+
+ // falback to api errors
+ return apiResolver(error)
+ }
+
+ return resolver
+}
diff --git a/apps/wallet-mobile/src/features/Claim/common/useDialogs.tsx b/apps/wallet-mobile/src/features/Claim/common/useDialogs.tsx
new file mode 100644
index 0000000000..e58ba66572
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/common/useDialogs.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react'
+
+import {useStrings} from './useStrings'
+
+export const useDialogs = () => {
+ const strings = useStrings()
+
+ return React.useRef({
+ errorInvalidRequest: {
+ title: strings.apiErrorTitle,
+ message: strings.apiErrorInvalidRequest,
+ },
+ errorNotFound: {
+ title: strings.apiErrorTitle,
+ message: strings.apiErrorNotFound,
+ },
+ errorAlreadyClaimed: {
+ title: strings.apiErrorTitle,
+ message: strings.apiErrorAlreadyClaimed,
+ },
+ errorExpired: {
+ title: strings.apiErrorTitle,
+ message: strings.apiErrorExpired,
+ },
+ errorTooEarly: {
+ title: strings.apiErrorTitle,
+ message: strings.apiErrorTooEarly,
+ },
+ errorRateLimited: {
+ title: strings.apiErrorTitle,
+ message: strings.apiErrorRateLimited,
+ },
+
+ accepted: {
+ title: strings.acceptedTitle,
+ message: strings.acceptedMesage,
+ },
+ processing: {
+ title: strings.processingTitle,
+ message: strings.processingMessage,
+ },
+ done: {
+ title: strings.doneTitle,
+ message: strings.doneMessage,
+ },
+ } as const).current
+}
diff --git a/apps/wallet-mobile/src/features/Claim/common/useNavigateTo.tsx b/apps/wallet-mobile/src/features/Claim/common/useNavigateTo.tsx
new file mode 100644
index 0000000000..ab77056f02
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/common/useNavigateTo.tsx
@@ -0,0 +1,14 @@
+import {useNavigation} from '@react-navigation/native'
+import {useRef} from 'react'
+
+import {TxHistoryRouteNavigation} from '../../../navigation'
+
+export const useNavigateTo = () => {
+ const navigation = useNavigation()
+
+ return useRef({
+ showSuccess: () => navigation.navigate('claim-show-success'),
+
+ back: () => navigation.goBack(),
+ } as const).current
+}
diff --git a/apps/wallet-mobile/src/features/Claim/common/useStrings.tsx b/apps/wallet-mobile/src/features/Claim/common/useStrings.tsx
new file mode 100644
index 0000000000..acfd6985d4
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/common/useStrings.tsx
@@ -0,0 +1,122 @@
+import * as React from 'react'
+import {defineMessages, useIntl} from 'react-intl'
+
+import globalMessages, {txLabels} from '../../../i18n/global-messages'
+
+export const useStrings = () => {
+ const intl = useIntl()
+
+ return React.useRef({
+ askConfirmationTitle: intl.formatMessage(messages.askConfirmationTitle),
+ showSuccessTitle: intl.formatMessage(messages.showSuccessTitle),
+
+ acceptedTitle: intl.formatMessage(messages.acceptedTitle),
+ acceptedMesage: intl.formatMessage(messages.acceptedMessage),
+ processingTitle: intl.formatMessage(messages.processingTitle),
+ processingMessage: intl.formatMessage(messages.processingMessage),
+ doneTitle: intl.formatMessage(messages.doneTitle),
+ doneMessage: intl.formatMessage(messages.doneMessage),
+ transactionId: intl.formatMessage(txLabels.txId),
+
+ addressSharingWarning: intl.formatMessage(messages.addressSharingWarning),
+ domain: intl.formatMessage(messages.domain),
+ code: intl.formatMessage(messages.code),
+
+ apiErrorTitle: intl.formatMessage(messages.apiErrorTitle),
+ apiErrorInvalidRequest: intl.formatMessage(messages.apiErrorInvalidRequest),
+ apiErrorNotFound: intl.formatMessage(messages.apiErrorNotFound),
+ apiErrorAlreadyClaimed: intl.formatMessage(messages.apiErrorAlreadyClaimed),
+ apiErrorExpired: intl.formatMessage(messages.apiErrorExpired),
+ apiErrorTooEarly: intl.formatMessage(messages.apiErrorTooEarly),
+ apiErrorRateLimited: intl.formatMessage(messages.apiErrorRateLimited),
+
+ ok: intl.formatMessage(globalMessages.ok),
+ cancel: intl.formatMessage(globalMessages.cancel),
+ continue: intl.formatMessage(messages.continue),
+ } as const).current
+}
+
+export const messages = Object.freeze(
+ defineMessages({
+ askConfirmationTitle: {
+ id: 'claim.askConfirmation.title',
+ defaultMessage: '!!!Confirm Claim',
+ },
+ showSuccessTitle: {
+ id: 'claim.showSuccess.title',
+ defaultMessage: '!!!Success',
+ },
+
+ doneTitle: {
+ id: 'claim.done.title',
+ defaultMessage: '!!!Done',
+ },
+ doneMessage: {
+ id: 'claim.done.message',
+ defaultMessage: '!!!Done',
+ },
+ acceptedTitle: {
+ id: 'claim.accepted.title',
+ defaultMessage: '!!!Accepted',
+ },
+ acceptedMessage: {
+ id: 'claim.accepted.message',
+ defaultMessage: '!!!Accepted',
+ },
+ processingTitle: {
+ id: 'claim.processing.title',
+ defaultMessage: '!!!Processing',
+ },
+ processingMessage: {
+ id: 'claim.processing.message',
+ defaultMessage: '!!!Processing',
+ },
+
+ addressSharingWarning: {
+ id: 'claim.addressSharingWarning',
+ defaultMessage: '!!!Address sharing warning',
+ },
+ domain: {
+ id: 'claim.domain',
+ defaultMessage: '!!!Domain',
+ },
+ code: {
+ id: 'claim.code',
+ defaultMessage: '!!!Code',
+ },
+
+ apiErrorTitle: {
+ id: 'claim.apiError.title',
+ defaultMessage: '!!!Error title',
+ },
+ apiErrorInvalidRequest: {
+ id: 'claim.apiError.invalidRequest',
+ defaultMessage: '!!!Invalid request',
+ },
+ apiErrorNotFound: {
+ id: 'claim.apiError.notFound',
+ defaultMessage: '!!!Not found',
+ },
+ apiErrorAlreadyClaimed: {
+ id: 'claim.apiError.alreadyClaimed',
+ defaultMessage: '!!!Already claimed',
+ },
+ apiErrorExpired: {
+ id: 'claim.apiError.expired',
+ defaultMessage: '!!!Expired',
+ },
+ apiErrorTooEarly: {
+ id: 'claim.apiError.tooEarly',
+ defaultMessage: '!!!Too early',
+ },
+ apiErrorRateLimited: {
+ id: 'claim.apiError.rateLimited',
+ defaultMessage: '!!!Rate limited',
+ },
+
+ continue: {
+ id: 'global.actions.dialogs.commonbuttons.continueButton',
+ defaultMessage: '!!!Continue',
+ },
+ }),
+)
diff --git a/apps/wallet-mobile/src/features/Claim/illustrations/ClaimSuccessIllustration.tsx b/apps/wallet-mobile/src/features/Claim/illustrations/ClaimSuccessIllustration.tsx
new file mode 100644
index 0000000000..5492d78896
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/illustrations/ClaimSuccessIllustration.tsx
@@ -0,0 +1,79 @@
+import * as React from 'react'
+import Svg, {Defs, G, LinearGradient, Path, Stop, SvgProps} from 'react-native-svg'
+
+// TODO: Get from Product the oficial illustration
+export const ClaimSuccessIllustration = ({zoom = 1, ...props}: SvgProps & {zoom?: number}) => {
+ return (
+
+ )
+}
diff --git a/apps/wallet-mobile/src/features/Claim/illustrations/Ilustrations.stories.tsx b/apps/wallet-mobile/src/features/Claim/illustrations/Ilustrations.stories.tsx
new file mode 100644
index 0000000000..704a81d3c0
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/illustrations/Ilustrations.stories.tsx
@@ -0,0 +1,8 @@
+import {storiesOf} from '@storybook/react-native'
+import React from 'react'
+
+import {ClaimSuccessIllustration} from './ClaimSuccessIllustration'
+
+storiesOf('Claim Illustrations Gallery', module).add('Claim request success', () => {
+ return
+})
diff --git a/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx b/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx
new file mode 100644
index 0000000000..7b32481321
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/ClaimProvider.tsx
@@ -0,0 +1,52 @@
+import {invalid} from '@yoroi/common'
+import * as React from 'react'
+
+import {ScanActionClaim} from '../../Scan/common/types'
+import {claimApiMockInstances} from './api.mocks'
+import {ClaimActions, ClaimActionType, claimReducer, defaultClaimActions, defaultClaimState} from './state'
+import {ClaimApi, ClaimState, ClaimToken} from './types'
+
+export type ClaimProviderContext = React.PropsWithChildren
+
+const initialClaimProvider: ClaimProviderContext = {
+ ...defaultClaimState,
+ ...defaultClaimActions,
+ ...claimApiMockInstances.error,
+}
+const ClaimContext = React.createContext(initialClaimProvider)
+
+type ClaimProviderProps = React.PropsWithChildren<{
+ claimApi: ClaimApi
+ initialState?: ClaimState
+}>
+export const ClaimProvider = ({children, claimApi, initialState}: ClaimProviderProps) => {
+ const [state, dispatch] = React.useReducer(claimReducer, {
+ ...defaultClaimState,
+ ...initialState,
+ })
+
+ const actions = React.useRef({
+ claimTokenChanged: (claimToken: ClaimToken) => {
+ dispatch({type: ClaimActionType.ClaimTokenChanged, claimToken})
+ },
+ scanActionClaimChanged: (scanActionClaim: ScanActionClaim) => {
+ dispatch({type: ClaimActionType.ScanActionClaimChanged, scanActionClaim})
+ },
+ reset: () => {
+ dispatch({type: ClaimActionType.Reset})
+ },
+ }).current
+
+ const context = React.useMemo(
+ () => ({
+ ...state,
+ ...claimApi,
+ ...actions,
+ }),
+ [state, claimApi, actions],
+ )
+
+ return {children}
+}
+export const useClaim = () =>
+ React.useContext(ClaimContext) ?? invalid('useClaim: needs to be wrapped in a ClaimManagerProvider')
diff --git a/apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts b/apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts
new file mode 100644
index 0000000000..64dd6a1758
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/api-faucet.mocks.ts
@@ -0,0 +1,34 @@
+import {ClaimApiClaimTokensResponse} from './types'
+
+const claimTokens: Record = {
+ success: {
+ accepted: {
+ status: 'accepted',
+ lovelaces: '2000000',
+ tokens: {
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '1000000',
+ },
+ queue_position: 100,
+ },
+ queued: {
+ lovelaces: '2000000',
+ tokens: {
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '1000000',
+ },
+ status: 'queued',
+ queue_position: 1,
+ },
+ claimed: {
+ lovelaces: '2000000',
+ tokens: {
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '1000000',
+ },
+ status: 'claimed',
+ tx_hash: 'tx_hash',
+ },
+ },
+} as const
+
+export const claimFaucetResponses = {
+ claimTokens,
+} as const
diff --git a/apps/wallet-mobile/src/features/Claim/module/api.mocks.ts b/apps/wallet-mobile/src/features/Claim/module/api.mocks.ts
new file mode 100644
index 0000000000..1f9958605d
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/api.mocks.ts
@@ -0,0 +1,78 @@
+import {ClaimApi, ClaimToken} from './types'
+
+const claimTokensResponse: {[key: string]: ClaimToken} = {
+ accepted: {
+ status: 'accepted',
+ amounts: {
+ '': '2000000',
+ '698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9d.7444524950': '44',
+ '29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c6.4d494e': '410',
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '5',
+ '1ca1fc0c880d25850cb00303788dfb51bdf2f902f6dce47d1ad09d5b.44': '2463889379',
+ '08d91ec4e6c743a92de97d2fde5ca0d81493555c535894a3097061f7.c8b0': '148',
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c53': '100008',
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c54': '10000000012',
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c55': '100000000000000020',
+ },
+ },
+ processing: {
+ status: 'processing',
+ amounts: {
+ '': '2000000',
+ '698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9d.7444524950': '44',
+ },
+ },
+ done: {
+ status: 'done',
+ amounts: {
+ '': '2000000',
+ '698a6ea0ca99f315034072af31eaac6ec11fe8558d3f48e9775aab9d.7444524950': '44',
+ '29d222ce763455e3d7a09a665ce554f00ac89d2e99a1a83d267170c6.4d494e': '410',
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65f.74484f444c52': '5',
+ '1ca1fc0c880d25850cb00303788dfb51bdf2f902f6dce47d1ad09d5b.44': '2463889379',
+ '08d91ec4e6c743a92de97d2fde5ca0d81493555c535894a3097061f7.c8b0': '148',
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c53': '100008',
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c54': '10000000012',
+ '1d129dc9c03f95a863489883914f05a52e13135994a32f0cbeacc65e.74484f444c55': '100000000000000020',
+ },
+ txHash: '3a27ac29f4218a4503ed241a19e59291835b38ccdb1f1f71ae4dc889d7dbfeb4',
+ },
+} as const
+
+export const claimApiMockResponses = {
+ claimTokens: claimTokensResponse,
+} as const
+
+const claimTokensApi = {
+ success: {
+ accepted: () => {
+ return Promise.resolve(claimTokensResponse.accepted)
+ },
+ processing: () => {
+ return Promise.resolve(claimTokensResponse.processing)
+ },
+ done: () => {
+ return Promise.resolve(claimTokensResponse.done)
+ },
+ },
+ error: () => {
+ return Promise.reject(new Error('Something went wrong'))
+ },
+ loading: () => {
+ return new Promise(() => null) as unknown as ClaimToken
+ },
+} as const
+
+export const claimApiMockFetchers = {
+ claimTokens: claimTokensApi,
+} as const
+
+const claimApiError: ClaimApi = {
+ claimTokens: claimTokensApi.error,
+ address: 'address',
+ primaryTokenId: '',
+} as const
+
+export const claimApiMockInstances = {
+ error: claimApiError,
+} as const
diff --git a/apps/wallet-mobile/src/features/Claim/module/api.tests.ts b/apps/wallet-mobile/src/features/Claim/module/api.tests.ts
new file mode 100644
index 0000000000..d4d13d04ed
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/api.tests.ts
@@ -0,0 +1,16 @@
+import {fetchData} from '@yoroi/common'
+
+import {claimApiMaker} from './api'
+
+describe('claimApiMaker', () => {
+ it('success', () => {
+ const appApi = claimApiMaker({address: 'addr_test', primaryTokenId: 'primaryTokenId'})
+ expect(appApi).toBeDefined()
+
+ const appApiWithFetcher = claimApiMaker(
+ {address: 'addr_test', primaryTokenId: 'primaryTokenId'},
+ {request: fetchData},
+ )
+ expect(appApiWithFetcher).toBeDefined()
+ })
+})
diff --git a/apps/wallet-mobile/src/features/Claim/module/api.ts b/apps/wallet-mobile/src/features/Claim/module/api.ts
new file mode 100644
index 0000000000..81a1deeccd
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/api.ts
@@ -0,0 +1,48 @@
+import {FetchData, fetchData, isLeft} from '@yoroi/common'
+import {Api, Balance} from '@yoroi/types'
+
+import {ScanActionClaim} from '../../Scan/common/types'
+import {asClaimApiError, asClaimToken} from './transformers'
+import {ClaimApi, ClaimApiClaimTokensResponse} from './types'
+import {ClaimTokensApiResponseSchema} from './validators'
+
+type ClaimApiMaker = Readonly<{
+ address: string
+ primaryTokenId: Balance.TokenInfo['id']
+}>
+
+export const claimApiMaker = (
+ {address, primaryTokenId}: ClaimApiMaker,
+ deps: Readonly<{request: FetchData}> = {request: fetchData} as const,
+): Readonly => {
+ const claimTokens = postClaimTokens({address, primaryTokenId}, deps)
+
+ return {
+ claimTokens,
+ address,
+ primaryTokenId,
+ } as const
+}
+
+const postClaimTokens =
+ ({address, primaryTokenId}: ClaimApiMaker, {request} = {request: fetchData}) =>
+ async (claimAction: ScanActionClaim) => {
+ // builds the request from the action, overides address and code
+ const {code, params, url} = claimAction
+ const payload = {...params, address, code}
+
+ const response = await request({
+ url,
+ method: 'post',
+ data: payload,
+ })
+
+ if (isLeft(response)) {
+ return asClaimApiError(response.error)
+ } else {
+ const claimToken = response.value.data
+ if (!ClaimTokensApiResponseSchema.safeParse(claimToken).success) throw new Api.Errors.ResponseMalformed()
+
+ return asClaimToken(claimToken, primaryTokenId)
+ }
+ }
diff --git a/apps/wallet-mobile/src/features/Claim/module/errors.ts b/apps/wallet-mobile/src/features/Claim/module/errors.ts
new file mode 100644
index 0000000000..1b1fe489fb
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/errors.ts
@@ -0,0 +1,28 @@
+export class ClaimApiErrorsInvalidRequest extends Error {
+ static readonly statusCode = 400
+}
+export class ClaimApiErrorsNotFound extends Error {
+ static readonly statusCode = 404
+}
+export class ClaimApiErrorsAlreadyClaimed extends Error {
+ static readonly statusCode = 409
+}
+export class ClaimApiErrorsExpired extends Error {
+ static readonly statusCode = 410
+}
+export class ClaimApiErrorsTooEarly extends Error {
+ static readonly statusCode = 425
+}
+export class ClaimApiErrorsRateLimited extends Error {
+ static readonly statusCode = 429
+}
+
+// API errors that when returned from a faucet have a more specific error message
+export const claimApiErrors = [
+ ClaimApiErrorsInvalidRequest,
+ ClaimApiErrorsNotFound,
+ ClaimApiErrorsAlreadyClaimed,
+ ClaimApiErrorsExpired,
+ ClaimApiErrorsTooEarly,
+ ClaimApiErrorsRateLimited,
+] as const
diff --git a/apps/wallet-mobile/src/features/Claim/module/state.mocks.ts b/apps/wallet-mobile/src/features/Claim/module/state.mocks.ts
new file mode 100644
index 0000000000..b2e0673fc2
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/state.mocks.ts
@@ -0,0 +1,37 @@
+import {claimApiMockResponses} from './api.mocks'
+import {defaultClaimState} from './state'
+import {ClaimState} from './types'
+
+const empty: ClaimState = {...defaultClaimState} as const
+const withScanActionClaim: ClaimState = {
+ ...defaultClaimState,
+ scanActionClaim: {
+ action: 'claim',
+ code: 'code',
+ params: {},
+ url: 'https://example.com',
+ },
+} as const
+
+const withClaimTokenAccepted: ClaimState = {
+ ...withScanActionClaim,
+ claimToken: claimApiMockResponses.claimTokens['accepted'],
+} as const
+
+const withClaimTokenProcessing: ClaimState = {
+ ...withScanActionClaim,
+ claimToken: claimApiMockResponses.claimTokens['processing'],
+} as const
+
+const withClaimTokenDone: ClaimState = {
+ ...withScanActionClaim,
+ claimToken: claimApiMockResponses.claimTokens['done'],
+} as const
+
+export const mocks = {
+ empty,
+ withScanActionClaim,
+ withClaimTokenAccepted,
+ withClaimTokenDone,
+ withClaimTokenProcessing,
+} as const
diff --git a/apps/wallet-mobile/src/features/Claim/module/state.ts b/apps/wallet-mobile/src/features/Claim/module/state.ts
new file mode 100644
index 0000000000..cb7c0defb4
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/state.ts
@@ -0,0 +1,57 @@
+import {invalid} from '@yoroi/common'
+import {produce} from 'immer'
+
+import {ScanActionClaim} from '../../Scan/common/types'
+import {ClaimState, ClaimToken} from './types'
+export type ClaimActions = Readonly<{
+ claimTokenChanged: (claimToken: ClaimToken) => void
+ scanActionClaimChanged: (scanActionClaim: ScanActionClaim) => void
+ reset: () => void
+}>
+
+export enum ClaimActionType {
+ ClaimTokenChanged = 'claimTokenChanged',
+ ScanActionClaimChanged = 'scanActionClaimChanged',
+ Reset = 'reset',
+}
+
+export type ClaimAction =
+ | {
+ type: ClaimActionType.ClaimTokenChanged
+ claimToken: ClaimToken
+ }
+ | {
+ type: ClaimActionType.ScanActionClaimChanged
+ scanActionClaim: ScanActionClaim
+ }
+ | {
+ type: ClaimActionType.Reset
+ }
+
+export const defaultClaimState: ClaimState = {
+ claimToken: undefined,
+ scanActionClaim: undefined,
+} as const
+
+export const defaultClaimActions: ClaimActions = {
+ claimTokenChanged: () => invalid('missing init'),
+ scanActionClaimChanged: () => invalid('missing init'),
+ reset: () => invalid('missing init'),
+} as const
+
+export const claimReducer = (state: ClaimState, action: ClaimAction): ClaimState => {
+ return produce(state, (draft) => {
+ switch (action.type) {
+ case ClaimActionType.ClaimTokenChanged:
+ draft.claimToken = action.claimToken
+ break
+ case ClaimActionType.ScanActionClaimChanged:
+ draft.scanActionClaim = action.scanActionClaim
+ break
+ case ClaimActionType.Reset:
+ draft.claimToken = undefined
+ draft.scanActionClaim = undefined
+ break
+ }
+ })
+}
diff --git a/apps/wallet-mobile/src/features/Claim/module/transformers.ts b/apps/wallet-mobile/src/features/Claim/module/transformers.ts
new file mode 100644
index 0000000000..5f136fdd51
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/transformers.ts
@@ -0,0 +1,47 @@
+import {handleApiError} from '@yoroi/common'
+import {Api, Balance} from '@yoroi/types'
+
+import {Amounts, asQuantity} from '../../../yoroi-wallets/utils/utils'
+import {claimApiErrors} from './errors'
+import {ClaimApiClaimTokensResponse, ClaimToken} from './types'
+
+// if the error is a known claim api error, throw it with a more specific error message otherwise throw the api error
+export const asClaimApiError = (error: Api.ResponseError) => {
+ const ClaimApiError = claimApiErrors.find(({statusCode}) => statusCode === error.status)
+ if (ClaimApiError) throw new ClaimApiError()
+ handleApiError(error)
+}
+
+export const asClaimToken = (
+ claimItemResponse: ClaimApiClaimTokensResponse,
+ primaryTokenId: Balance.TokenInfo['id'],
+) => {
+ const {lovelaces, tokens, status} = claimItemResponse
+ const ptQuantity = asQuantity(lovelaces)
+ const amounts = Amounts.fromArray(
+ Object.entries(tokens)
+ .concat([primaryTokenId, ptQuantity])
+ .map(([tokenId, quantity]): Balance.Amount => ({tokenId, quantity: asQuantity(quantity)})),
+ )
+
+ if (status === 'claimed') {
+ const claimed: Readonly = {
+ status: 'done',
+ amounts,
+ txHash: claimItemResponse.tx_hash,
+ }
+ return claimed
+ } else if (status === 'queued') {
+ const queued: Readonly = {
+ status: 'processing',
+ amounts,
+ }
+ return queued
+ } else {
+ const accepted: Readonly = {
+ status: 'accepted',
+ amounts,
+ }
+ return accepted
+ }
+}
diff --git a/apps/wallet-mobile/src/features/Claim/module/types.ts b/apps/wallet-mobile/src/features/Claim/module/types.ts
new file mode 100644
index 0000000000..724fa8f4ba
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/types.ts
@@ -0,0 +1,52 @@
+import {Balance} from '@yoroi/types'
+
+import {ScanActionClaim} from '../../Scan/common/types'
+
+export type ClaimApiClaimTokensRequestPayload = {
+ address: string
+ code: string
+ [key: string]: unknown
+}
+
+export type ClaimApiClaimTokensResponse = {
+ lovelaces: string
+ tokens: {
+ [tokenId: string]: string
+ }
+} & (
+ | {
+ // code: 200
+ status: 'accepted'
+ queue_position: number
+ }
+ | {
+ // code: 201
+ status: 'queued'
+ queue_position: number
+ }
+ | {
+ // code: 202
+ status: 'claimed'
+ tx_hash: string
+ }
+)
+
+export type ClaimStatus = 'accepted' | 'processing' | 'done'
+
+export type ClaimToken = Readonly<{
+ // api
+ status: ClaimStatus
+ amounts: Balance.Amounts
+ txHash?: string
+}>
+
+export type ClaimApi = Readonly<{
+ claimTokens: (action: ScanActionClaim) => Promise
+ address: string
+ primaryTokenId: Balance.TokenInfo['id']
+}>
+
+export type ClaimState = Readonly<{
+ claimToken: ClaimToken | undefined
+ scanActionClaim: ScanActionClaim | undefined
+}>
diff --git a/apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx b/apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx
new file mode 100644
index 0000000000..50110cfec8
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/useClaimTokens.tsx
@@ -0,0 +1,21 @@
+import {UseMutationOptions} from 'react-query'
+
+import {useMutationWithInvalidations} from '../../../yoroi-wallets/hooks'
+import {ScanActionClaim} from '../../Scan/common/types'
+import {useClaim} from './ClaimProvider'
+import {ClaimToken} from './types'
+
+export const useClaimTokens = (options: UseMutationOptions = {}) => {
+ const {claimTokens, address} = useClaim()
+ const mutation = useMutationWithInvalidations({
+ ...options,
+ mutationFn: claimTokens,
+ invalidateQueries: [['useClaimTokens', address]],
+ })
+
+ return {
+ claimTokens: mutation.mutate,
+
+ ...mutation,
+ } as const
+}
diff --git a/apps/wallet-mobile/src/features/Claim/module/validators.ts b/apps/wallet-mobile/src/features/Claim/module/validators.ts
new file mode 100644
index 0000000000..a792f6f6ca
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/module/validators.ts
@@ -0,0 +1,33 @@
+import {z} from 'zod'
+
+const QuantitySchema = z.string().regex(/^\d+$/, {
+ message: 'Expected a string containing only numeric characters (0-9)',
+})
+
+const AmountsSchema = z.record(QuantitySchema)
+
+const BaseClaimTokensSchema = z.object({
+ lovelaces: QuantitySchema,
+ tokens: AmountsSchema,
+})
+
+const ClaimTokensAcceptedSchema = z.object({
+ status: z.literal('accepted'),
+ queue_position: z.number(),
+})
+
+const ClaimTokensQueuedSchema = z.object({
+ status: z.literal('queued'),
+ queue_position: z.number(),
+})
+
+const ClaimTokensClaimedSchema = z.object({
+ status: z.literal('claimed'),
+ tx_hash: z.string(),
+})
+
+export const ClaimTokensApiResponseSchema = z.union([
+ BaseClaimTokensSchema.merge(ClaimTokensAcceptedSchema),
+ BaseClaimTokensSchema.merge(ClaimTokensClaimedSchema),
+ BaseClaimTokensSchema.merge(ClaimTokensQueuedSchema),
+])
diff --git a/apps/wallet-mobile/src/features/Claim/useCases/AskConfirmation.stories.tsx b/apps/wallet-mobile/src/features/Claim/useCases/AskConfirmation.stories.tsx
new file mode 100644
index 0000000000..e7b9023f3e
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/useCases/AskConfirmation.stories.tsx
@@ -0,0 +1,9 @@
+import {action} from '@storybook/addon-actions'
+import {storiesOf} from '@storybook/react-native'
+import React from 'react'
+
+import {AskConfirmation} from './AskConfirmation'
+
+storiesOf('AskConfirmation', module).add('initial', () => {
+ return
+})
diff --git a/apps/wallet-mobile/src/features/Claim/useCases/AskConfirmation.tsx b/apps/wallet-mobile/src/features/Claim/useCases/AskConfirmation.tsx
new file mode 100644
index 0000000000..c5a3898b42
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/useCases/AskConfirmation.tsx
@@ -0,0 +1,122 @@
+import * as React from 'react'
+import {Platform, ScrollView, StyleSheet, Text, View, ViewProps} from 'react-native'
+
+import {Button} from '../../../components/Button/Button'
+import {useModal} from '../../../components/Modal/ModalContext'
+import {Spacer} from '../../../components/Spacer/Spacer'
+import {useStrings} from '../common/useStrings'
+
+type Props = {
+ address: string
+ url: string
+ code: string
+ onContinue: () => void
+}
+export const AskConfirmation = ({address, url, code, onContinue}: Props) => {
+ const strings = useStrings()
+ const {closeModal, isLoading} = useModal()
+ const domain = getDomain(url)
+
+ return (
+
+
+ {strings.addressSharingWarning}
+
+
+
+ {address}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const Actions = ({style, ...props}: ViewProps) =>
+const Item = ({label, value}: {label: string; value: string}) => {
+ return (
+
+ {label}
+
+
+ {value}
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ actions: {
+ flexDirection: 'row',
+ alignItems: 'flex-end',
+ minHeight: 48,
+ maxHeight: 54,
+ },
+ item: {
+ alignSelf: 'stretch',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ warning: {
+ color: '#242838',
+ fontFamily: 'Rubik',
+ fontSize: 16,
+ fontWeight: '400',
+ lineHeight: 24,
+ textAlign: 'center',
+ },
+ rowLabel: {
+ color: '#6B7384',
+ fontFamily: 'Rubik',
+ fontSize: 16,
+ fontWeight: '400',
+ lineHeight: 24,
+ paddingRight: 8,
+ },
+ rowValue: {
+ color: '#000',
+ fontFamily: 'Rubik',
+ fontSize: 16,
+ fontWeight: '400',
+ lineHeight: 24,
+ maxWidth: 240,
+ },
+ monospace: {
+ ...Platform.select({
+ ios: {fontFamily: 'Menlo'},
+ android: {fontFamily: 'monospace'},
+ }),
+ },
+})
+
+function getDomain(url) {
+ try {
+ const domain = new URL(url).hostname
+ return domain
+ } catch (error) {
+ return ''
+ }
+}
diff --git a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx
new file mode 100644
index 0000000000..0e56644fe8
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.stories.tsx
@@ -0,0 +1,44 @@
+import {DecoratorFunction} from '@storybook/addons'
+import {storiesOf} from '@storybook/react-native'
+import React from 'react'
+import {QueryClientProvider} from 'react-query'
+
+import {SelectedWalletProvider} from '../../../SelectedWallet/Context/SelectedWalletContext'
+import {queryClientFixture} from '../../../utils/fixtures'
+import {mocks as walletMocks} from '../../../yoroi-wallets/mocks/wallet'
+import {claimApiMockInstances} from '../module/api.mocks'
+import {ClaimProvider} from '../module/ClaimProvider'
+import {mocks as claimMocks} from '../module/state.mocks'
+import {ShowSuccessScreen} from './ShowSuccessScreen'
+
+const AppDecorator: DecoratorFunction = (story) => {
+ return (
+
+ {story()}
+
+ )
+}
+
+storiesOf('Claim ShowSuccessScreen', module)
+ .addDecorator(AppDecorator)
+ .add('processing', () => {
+ return (
+
+
+
+ )
+ })
+ .add('accepted', () => {
+ return (
+
+
+
+ )
+ })
+ .add('done', () => {
+ return (
+
+
+
+ )
+ })
diff --git a/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx
new file mode 100644
index 0000000000..9d22dbaf9a
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Claim/useCases/ShowSuccessScreen.tsx
@@ -0,0 +1,187 @@
+import {invalid} from '@yoroi/common'
+import {Balance} from '@yoroi/types'
+import React from 'react'
+import {FlatList, Linking, Platform, StyleSheet, Text, TextProps, View, ViewProps} from 'react-native'
+import {SafeAreaView} from 'react-native-safe-area-context'
+
+import {CopyButton, Icon} from '../../../components'
+import {AmountItem} from '../../../components/AmountItem/AmountItem'
+import {Button} from '../../../components/Button/Button'
+import {PressableIcon} from '../../../components/PressableIcon/PressableIcon'
+import {Spacer} from '../../../components/Spacer/Spacer'
+import {useBlockGoBack} from '../../../navigation'
+import {useSelectedWallet} from '../../../SelectedWallet/Context/SelectedWalletContext'
+import {COLORS} from '../../../theme/config'
+import {sortTokenInfos} from '../../../utils/sorting'
+import {isEmptyString} from '../../../utils/utils'
+import {getNetworkConfigById} from '../../../yoroi-wallets/cardano/networks'
+import {useTokenInfos} from '../../../yoroi-wallets/hooks'
+import {Amounts} from '../../../yoroi-wallets/utils/utils'
+import {useDialogs} from '../common/useDialogs'
+import {useNavigateTo} from '../common/useNavigateTo'
+import {useStrings} from '../common/useStrings'
+import {ClaimSuccessIllustration} from '../illustrations/ClaimSuccessIllustration'
+import {useClaim} from '../module/ClaimProvider'
+import {ClaimStatus} from '../module/types'
+
+export const ShowSuccessScreen = () => {
+ useBlockGoBack()
+ const strings = useStrings()
+ const navigateTo = useNavigateTo()
+ const {claimToken} = useClaim()
+
+ if (!claimToken) invalid('Should never happen')
+
+ const {status, txHash, amounts} = claimToken
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {!isEmptyString(txHash) && (
+ <>
+
+
+
+ >
+ )}
+
+
+
+
+
+
+ )
+}
+
+const Actions = ({style, ...props}: ViewProps) =>
+const Header = ({style, ...props}: ViewProps) =>
+const Status = ({status, style, ...props}: TextProps & {status: ClaimStatus}) => {
+ const dialogs = useDialogs()
+ const dialog: Record = {
+ ['processing']: dialogs.processing,
+ ['accepted']: dialogs.accepted,
+ ['done']: dialogs.done,
+ }
+ return (
+ <>
+
+ {dialog[status].title}
+
+
+
+
+ {dialog[status].message}
+ >
+ )
+}
+
+const TxHash = ({txHash}: {txHash: string}) => {
+ const strings = useStrings()
+ const wallet = useSelectedWallet()
+ const config = getNetworkConfigById(wallet.networkId)
+
+ return (
+ <>
+
+ {strings.transactionId}
+
+
+
+
+
+
+
+
+ {txHash}
+
+
+ Linking.openURL(config.EXPLORER_URL_FOR_TX(txHash))}
+ color={COLORS.GRAY}
+ size={16}
+ />
+
+ >
+ )
+}
+
+export const AmountList = ({amounts}: {amounts: Balance.Amounts}) => {
+ const wallet = useSelectedWallet()
+
+ const tokenInfos = useTokenInfos({
+ wallet,
+ tokenIds: Amounts.toArray(amounts).map(({tokenId}) => tokenId),
+ })
+
+ return (
+ (
+
+ )}
+ ItemSeparatorComponent={() => }
+ style={{paddingHorizontal: 16}}
+ keyExtractor={({id}) => id}
+ />
+ )
+}
+
+const styles = StyleSheet.create({
+ header: {
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ },
+ title: {
+ color: COLORS.BLACK,
+ fontWeight: '600',
+ fontSize: 20,
+ padding: 4,
+ textAlign: 'center',
+ lineHeight: 30,
+ fontFamily: 'Rubik-Medium',
+ },
+ message: {
+ color: COLORS.TEXT_INPUT,
+ fontSize: 14,
+ lineHeight: 22,
+ textAlign: 'center',
+ maxWidth: 300,
+ },
+ txLabel: {
+ fontSize: 16,
+ lineHeight: 18,
+ paddingRight: 8,
+ },
+ monospace: {
+ fontSize: 16,
+ lineHeight: 18,
+ color: COLORS.TEXT_INPUT,
+ ...Platform.select({
+ ios: {fontFamily: 'Menlo'},
+ android: {fontFamily: 'monospace'},
+ }),
+ paddingRight: 8,
+ flex: 1,
+ },
+ txRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+})
diff --git a/apps/wallet-mobile/src/features/Scan/common/CodeScannerButton.tsx b/apps/wallet-mobile/src/features/Scan/common/CodeScannerButton.tsx
index f921e69f37..cba13f47d5 100644
--- a/apps/wallet-mobile/src/features/Scan/common/CodeScannerButton.tsx
+++ b/apps/wallet-mobile/src/features/Scan/common/CodeScannerButton.tsx
@@ -1,7 +1,8 @@
import * as React from 'react'
-import {Pressable, PressableProps} from 'react-native'
+import {TouchableOpacityProps} from 'react-native'
import {Icon} from '../../../components'
+import {PressableIcon} from '../../../components/PressableIcon/PressableIcon'
import {COLORS} from '../../../theme'
const fallbackSize = 30
@@ -10,16 +11,7 @@ const fallbackColor = COLORS.BLACK // TODO: use theme
type CodeScannerButtonProps = {
size?: number
color?: string
-} & PressableProps
+} & TouchableOpacityProps
export const CodeScannerButton = ({size = fallbackSize, color = fallbackColor, ...props}: CodeScannerButtonProps) => (
)
-
-type PressableIconProps = {
- size: number
- color: string
- icon: (props: {size: number; color: string}) => React.ReactNode
-} & PressableProps
-const PressableIcon = ({icon, size, color, ...props}: PressableIconProps) => (
- {icon({size, color})}
-)
diff --git a/apps/wallet-mobile/src/features/Scan/common/types.ts b/apps/wallet-mobile/src/features/Scan/common/types.ts
index 82f5f4178d..907f245da0 100644
--- a/apps/wallet-mobile/src/features/Scan/common/types.ts
+++ b/apps/wallet-mobile/src/features/Scan/common/types.ts
@@ -1,28 +1,29 @@
-// TODO: migrate to yoroi types after full implemented
+// TODO: migrate to yoroi types after fully implemented
export class ScanErrorUnknownContent extends Error {}
export class ScanErrorUnknown extends Error {}
-export type ScanAction = Readonly<
- | {
- action: 'send-only-receiver'
- receiver: string
- }
- | {
- action: 'send-single-pt'
- receiver: string
- params:
- | {
- amount: number | undefined
- memo: string | undefined
- message: string | undefined
- }
- | undefined
- }
- | {
- action: 'claim'
- url: string
- code: string
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- params: Record | undefined
- }
->
+
+export type ScanActionSendOnlyReceiver = Readonly<{
+ action: 'send-only-receiver'
+ receiver: string
+}>
+export type ScanActionSendSinglePt = Readonly<{
+ action: 'send-single-pt'
+ receiver: string
+ params:
+ | {
+ amount: number | undefined
+ memo: string | undefined
+ message: string | undefined
+ }
+ | undefined
+}>
+export type ScanActionClaim = Readonly<{
+ action: 'claim'
+ url: string
+ code: string
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ params: Record | undefined
+}>
+export type ScanAction = ScanActionSendOnlyReceiver | ScanActionSendSinglePt | ScanActionClaim
+
export type ScanFeature = 'send' | 'scan'
diff --git a/apps/wallet-mobile/src/features/Scan/common/useNavigateTo.tsx b/apps/wallet-mobile/src/features/Scan/common/useNavigateTo.tsx
index c9ff966042..a623388a6d 100644
--- a/apps/wallet-mobile/src/features/Scan/common/useNavigateTo.tsx
+++ b/apps/wallet-mobile/src/features/Scan/common/useNavigateTo.tsx
@@ -8,6 +8,7 @@ export const useNavigateTo = () => {
return useRef({
showCameraPermissionDenied: () => navigation.navigate('scan-show-camera-permission-denied'),
+ claimShowSuccess: () => navigation.navigate('claim-show-success'),
send: () => navigation.navigate('send-start-tx'),
back: () => navigation.goBack(),
}).current
diff --git a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx
index 1c84d80fc5..b68e4c084d 100644
--- a/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx
+++ b/apps/wallet-mobile/src/features/Scan/common/useTriggerScanAction.tsx
@@ -1,3 +1,12 @@
+import * as React from 'react'
+import {Alert} from 'react-native'
+
+import {useModal} from '../../../components/Modal/ModalContext'
+import {useClaimErrorResolver} from '../../../features/Claim/common/useClaimErrorResolver'
+import {useStrings as useStringsClaim} from '../../../features/Claim/common/useStrings'
+import {useClaim} from '../../../features/Claim/module/ClaimProvider'
+import {useClaimTokens} from '../../../features/Claim/module/useClaimTokens'
+import {AskConfirmation} from '../../../features/Claim/useCases/AskConfirmation'
import {useSend} from '../../../features/Send/common/SendContext'
import {useSelectedWallet} from '../../../SelectedWallet/Context/SelectedWalletContext'
import {pastedFormatter} from '../../../yoroi-wallets/utils/amountUtils'
@@ -6,10 +15,28 @@ import {ScanAction, ScanFeature} from './types'
import {useNavigateTo} from './useNavigateTo'
export const useTriggerScanAction = ({insideFeature}: {insideFeature: ScanFeature}) => {
- const {receiverChanged, amountChanged, tokenSelectedChanged, resetForm, memoChanged} = useSend()
const {primaryTokenInfo} = useSelectedWallet()
+ const {openModal, closeModal, startLoading, stopLoading} = useModal()
const navigateTo = useNavigateTo()
+ const {receiverChanged, amountChanged, tokenSelectedChanged, resetForm, memoChanged} = useSend()
+
+ const {reset, scanActionClaimChanged, address, claimTokenChanged} = useClaim()
+ const claimErrorResolver = useClaimErrorResolver()
+ const {claimTokens} = useClaimTokens({
+ onSuccess: (claimToken) => {
+ claimTokenChanged(claimToken)
+ closeModal()
+ navigateTo.claimShowSuccess()
+ },
+ onError: (error) => {
+ stopLoading()
+ const claimErrorDialog = claimErrorResolver(error)
+ Alert.alert(claimErrorDialog.title, claimErrorDialog.message)
+ },
+ })
+ const stringsClaim = useStringsClaim()
+
const trigger = (scanAction: ScanAction) => {
switch (scanAction.action) {
case 'send-single-pt': {
@@ -46,7 +73,25 @@ export const useTriggerScanAction = ({insideFeature}: {insideFeature: ScanFeatur
}
case 'claim': {
- console.log('TODO: implement')
+ navigateTo.back()
+ reset()
+ scanActionClaimChanged(scanAction)
+
+ const handleOnContinue = () => {
+ startLoading()
+ claimTokens(scanAction)
+ }
+ const claimContent = (
+
+ )
+
+ openModal(stringsClaim.askConfirmationTitle, claimContent, 400)
+
break
}
}
diff --git a/apps/wallet-mobile/src/features/Scan/common/illustrations/CameraPermissionDeniedIlustration.tsx b/apps/wallet-mobile/src/features/Scan/illustrations/CameraPermissionDeniedIlustration.tsx
similarity index 100%
rename from apps/wallet-mobile/src/features/Scan/common/illustrations/CameraPermissionDeniedIlustration.tsx
rename to apps/wallet-mobile/src/features/Scan/illustrations/CameraPermissionDeniedIlustration.tsx
diff --git a/apps/wallet-mobile/src/features/Scan/illustrations/Ilustrations.stories.tsx b/apps/wallet-mobile/src/features/Scan/illustrations/Ilustrations.stories.tsx
new file mode 100644
index 0000000000..9bfd862ed2
--- /dev/null
+++ b/apps/wallet-mobile/src/features/Scan/illustrations/Ilustrations.stories.tsx
@@ -0,0 +1,8 @@
+import {storiesOf} from '@storybook/react-native'
+import React from 'react'
+
+import {CameraPermissionDeniedIllustration} from './CameraPermissionDeniedIlustration'
+
+storiesOf('Scan Illustrations Gallery', module).add('Camera permission denied', () => {
+ return
+})
diff --git a/apps/wallet-mobile/src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.tsx b/apps/wallet-mobile/src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.tsx
index a3a57dc695..baba6dd560 100644
--- a/apps/wallet-mobile/src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.tsx
+++ b/apps/wallet-mobile/src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.tsx
@@ -5,8 +5,8 @@ import {SafeAreaView} from 'react-native-safe-area-context'
import {Button, Spacer, Text} from '../../../../components'
import {useBlockGoBack, useWalletNavigation} from '../../../../navigation'
import {COLORS} from '../../../../theme'
-import {CameraPermissionDeniedIllustration} from '../../common/illustrations/CameraPermissionDeniedIlustration'
import {useStrings} from '../../common/useStrings'
+import {CameraPermissionDeniedIllustration} from '../../illustrations/CameraPermissionDeniedIlustration'
import {OpenDeviceAppSettingsButton} from './OpenDeviceAppSettingsButton'
export const ShowCameraPermissionDeniedScreen = () => {
diff --git a/apps/wallet-mobile/src/i18n/global-messages.ts b/apps/wallet-mobile/src/i18n/global-messages.ts
index 470e5b1992..da5fe6abb6 100644
--- a/apps/wallet-mobile/src/i18n/global-messages.ts
+++ b/apps/wallet-mobile/src/i18n/global-messages.ts
@@ -474,6 +474,65 @@ export const assetMessages = defineMessages({
},
})
+export const apiErrors = defineMessages({
+ title: {
+ id: 'api.error.title',
+ defaultMessage: '!!!API error title',
+ },
+ badRequest: {
+ id: 'api.error.badRequest',
+ defaultMessage: '!!!Bad request',
+ },
+ unauthorized: {
+ id: 'api.error.unauthorized',
+ defaultMessage: '!!!Unauthorized',
+ },
+ forbidden: {
+ id: 'api.error.forbidden',
+ defaultMessage: '!!!Forbidden',
+ },
+ notFound: {
+ id: 'api.error.notFound',
+ defaultMessage: '!!!Not found',
+ },
+ conflict: {
+ id: 'api.error.conflict',
+ defaultMessage: '!!!Conflict',
+ },
+ gone: {
+ id: 'api.error.gone',
+ defaultMessage: '!!!Gone',
+ },
+ tooEarly: {
+ id: 'api.error.tooEarly',
+ defaultMessage: '!!!Too early',
+ },
+ tooManyRequests: {
+ id: 'api.error.tooManyRequests',
+ defaultMessage: '!!!Too many requests',
+ },
+ serverSide: {
+ id: 'api.error.serverSide',
+ defaultMessage: '!!!Server side',
+ },
+ unknown: {
+ id: 'api.error.unknown',
+ defaultMessage: '!!!Unknown',
+ },
+ network: {
+ id: 'api.error.network',
+ defaultMessage: '!!!Network',
+ },
+ invalidState: {
+ id: 'api.error.invalidState',
+ defaultMessage: '!!!Invalid state',
+ },
+ responseMalformed: {
+ id: 'api.error.responseMalformed',
+ defaultMessage: '!!!Response malformed',
+ },
+})
+
export const actionMessages = defineMessages({
send: {
id: 'global.send',
diff --git a/apps/wallet-mobile/src/i18n/locales/en-US.json b/apps/wallet-mobile/src/i18n/locales/en-US.json
index fb72c40272..da11080505 100644
--- a/apps/wallet-mobile/src/i18n/locales/en-US.json
+++ b/apps/wallet-mobile/src/i18n/locales/en-US.json
@@ -1,4 +1,18 @@
{
+ "api.error.title": "Oops! API error",
+ "api.error.badRequest": "Server could not understand the request",
+ "api.error.unauthorized": "You are not authorized to access this resource",
+ "api.error.forbidden": "You are not allowed to access this resource",
+ "api.error.notFound": "The requested resource could not be found",
+ "api.error.conflict": "The request could not be completed due to a conflict",
+ "api.error.gone": "The requested resource is no longer available",
+ "api.error.tooEarly": "The requested resource is not yet available, try again later",
+ "api.error.tooManyRequests": "Too many requests, wait a bit and try again",
+ "api.error.serverSide": "The server encountered an unexpected condition",
+ "api.error.unknown": "An error that is unknown for the app happened, please try again",
+ "api.error.network": "Something went wrong with the network",
+ "api.error.invalidState": "Invalid state",
+ "api.error.responseMalformed": "The server response is malformed we can't interpret it",
"menu": "Menu",
"menu.allWallets": "All Wallets",
"menu.catalystVoting": "Catalyst Voting",
@@ -22,6 +36,24 @@
"analytics.selectLanguage": "Select Language",
"analytics.tosIAgreeWith": "I agree with",
"analytics.tosAgreement": "Terms Of Service Agreement",
+ "claim.askConfirmation.title": "Confirm claim",
+ "claim.showSuccess.title": "Claim success",
+ "claim.domain": "Domain",
+ "claim.code": "Code",
+ "claim.addressSharingWarning": "You will be sharing with the domain listed here your address",
+ "claim.accepted.title": "Claim accepted đź‘Ť",
+ "claim.accepted.message": "Claim has been accepted, you will receive your asset(s) soon, please scan the code again to check the status",
+ "claim.processing.title": "Processing claim ⏳️",
+ "claim.processing.message": "Claim is being processed, you will receive your asset(s) soon, please scan the code again to check the status",
+ "claim.done.title": "Claim completed ✔️",
+ "claim.done.message": "Claim was completed, you should have received your asset(s), you can verify the transaction on the chain explorer",
+ "claim.apiError.title": "The claim failed",
+ "claim.apiError.invalidRequest": "The server rejecte the request, check the address provided by the wallet is on the wrong network for this claim, and if the code is still avaible",
+ "claim.apiError.notFound": "The claim could not be found",
+ "claim.apiError.alreadyClaimed": "This claim has already been done",
+ "claim.apiError.expired": "This claim has ended, it is no longer availble",
+ "claim.apiError.tooEarly": "This claim hasn't started yet, please try again later",
+ "claim.apiError.rateLimited": "Too many claims happening, wait a bit and try again",
"components.common.errormodal.hideError": "Hide error message",
"components.common.errormodal.showError": "Show error message",
"components.common.fingerprintscreenbase.welcomeMessage": "Welcome Back",
diff --git a/apps/wallet-mobile/src/navigation.tsx b/apps/wallet-mobile/src/navigation.tsx
index 10d9cc232a..5af38e484d 100644
--- a/apps/wallet-mobile/src/navigation.tsx
+++ b/apps/wallet-mobile/src/navigation.tsx
@@ -198,7 +198,8 @@ export type TxHistoryRoutes = {
'send-edit-amount': undefined
'send-select-token-from-list': undefined
} & SwapTokenRoutes &
- ScanRoutes
+ ScanRoutes &
+ ClaimRoutes
export type TxHistoryRouteNavigation = StackNavigationProp
type ScanStartParams = Readonly<{
@@ -209,6 +210,9 @@ export type ScanRoutes = {
'scan-claim-confirm-summary': undefined
'scan-show-camera-permission-denied': undefined
}
+export type ClaimRoutes = {
+ 'claim-show-success': undefined
+}
export type SwapTokenRoutes = {
'swap-start-swap': undefined
diff --git a/apps/wallet-mobile/src/utils/fixtures.ts b/apps/wallet-mobile/src/utils/fixtures.ts
new file mode 100644
index 0000000000..d3cb0695fb
--- /dev/null
+++ b/apps/wallet-mobile/src/utils/fixtures.ts
@@ -0,0 +1,14 @@
+import {QueryClient} from 'react-query'
+
+export const queryClientFixture = () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ cacheTime: 0,
+ },
+ mutations: {
+ retry: false,
+ },
+ },
+ })
diff --git a/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json b/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json
index 09d65df355..edec2d8c91 100644
--- a/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json
+++ b/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json
@@ -4,14 +4,14 @@
"defaultMessage": "!!!Receive",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 299,
+ "line": 318,
"column": 16,
- "index": 10378
+ "index": 11471
},
"end": {
- "line": 302,
+ "line": 321,
"column": 3,
- "index": 10467
+ "index": 11560
}
},
{
@@ -19,14 +19,14 @@
"defaultMessage": "!!!Swap",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 303,
+ "line": 322,
"column": 13,
- "index": 10482
+ "index": 11575
},
"end": {
- "line": 306,
+ "line": 325,
"column": 3,
- "index": 10555
+ "index": 11648
}
},
{
@@ -34,14 +34,14 @@
"defaultMessage": "!!!Swap from",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 307,
+ "line": 326,
"column": 17,
- "index": 10574
+ "index": 11667
},
"end": {
- "line": 310,
+ "line": 329,
"column": 3,
- "index": 10651
+ "index": 11744
}
},
{
@@ -49,14 +49,14 @@
"defaultMessage": "!!!Swap to",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 311,
+ "line": 330,
"column": 15,
- "index": 10668
+ "index": 11761
},
"end": {
- "line": 314,
+ "line": 333,
"column": 3,
- "index": 10741
+ "index": 11834
}
},
{
@@ -64,14 +64,14 @@
"defaultMessage": "!!!Slippage Tolerance",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 315,
+ "line": 334,
"column": 21,
- "index": 10764
+ "index": 11857
},
"end": {
- "line": 318,
+ "line": 337,
"column": 3,
- "index": 10859
+ "index": 11952
}
},
{
@@ -79,14 +79,14 @@
"defaultMessage": "!!!Select pool",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 319,
+ "line": 338,
"column": 14,
- "index": 10875
+ "index": 11968
},
"end": {
- "line": 322,
+ "line": 341,
"column": 3,
- "index": 10956
+ "index": 12049
}
},
{
@@ -94,14 +94,14 @@
"defaultMessage": "!!!Send",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 323,
+ "line": 342,
"column": 13,
- "index": 10971
+ "index": 12064
},
"end": {
- "line": 326,
+ "line": 345,
"column": 3,
- "index": 11051
+ "index": 12144
}
},
{
@@ -109,14 +109,14 @@
"defaultMessage": "!!!Scan QR code address",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 327,
+ "line": 346,
"column": 18,
- "index": 11071
+ "index": 12164
},
"end": {
- "line": 330,
+ "line": 349,
"column": 3,
- "index": 11172
+ "index": 12265
}
},
{
@@ -124,14 +124,14 @@
"defaultMessage": "!!!Select asset",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 331,
+ "line": 350,
"column": 20,
- "index": 11194
+ "index": 12287
},
"end": {
- "line": 334,
+ "line": 353,
"column": 3,
- "index": 11283
+ "index": 12376
}
},
{
@@ -139,14 +139,14 @@
"defaultMessage": "!!!Selected tokens",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 335,
+ "line": 354,
"column": 26,
- "index": 11311
+ "index": 12404
},
"end": {
- "line": 338,
+ "line": 357,
"column": 3,
- "index": 11415
+ "index": 12508
}
},
{
@@ -154,14 +154,14 @@
"defaultMessage": "!!!Edit amount",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 339,
+ "line": 358,
"column": 19,
- "index": 11436
+ "index": 12529
},
"end": {
- "line": 342,
+ "line": 361,
"column": 3,
- "index": 11529
+ "index": 12622
}
},
{
@@ -169,14 +169,14 @@
"defaultMessage": "!!!Confirm",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 343,
+ "line": 362,
"column": 16,
- "index": 11547
+ "index": 12640
},
"end": {
- "line": 346,
+ "line": 365,
"column": 3,
- "index": 11633
+ "index": 12726
}
},
{
@@ -184,14 +184,14 @@
"defaultMessage": "!!!Share this address to receive payments. To protect your privacy, new addresses are generated automatically once you use them.",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 347,
+ "line": 366,
"column": 19,
- "index": 11654
+ "index": 12747
},
"end": {
- "line": 353,
+ "line": 372,
"column": 3,
- "index": 11892
+ "index": 12985
}
},
{
@@ -199,14 +199,14 @@
"defaultMessage": "!!!Confirm transaction",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 354,
+ "line": 373,
"column": 27,
- "index": 11921
+ "index": 13014
},
"end": {
- "line": 357,
+ "line": 376,
"column": 3,
- "index": 12014
+ "index": 13107
}
},
{
@@ -214,14 +214,29 @@
"defaultMessage": "!!!Please scan a QR code",
"file": "src/TxHistory/TxHistoryNavigator.tsx",
"start": {
- "line": 358,
+ "line": 377,
"column": 13,
- "index": 12029
+ "index": 13122
},
"end": {
- "line": 361,
+ "line": 380,
+ "column": 3,
+ "index": 13197
+ }
+ },
+ {
+ "id": "claim.showSuccess.title",
+ "defaultMessage": "!!!Success",
+ "file": "src/TxHistory/TxHistoryNavigator.tsx",
+ "start": {
+ "line": 381,
+ "column": 25,
+ "index": 13224
+ },
+ "end": {
+ "line": 384,
"column": 3,
- "index": 12104
+ "index": 13298
}
}
]
\ No newline at end of file
diff --git a/apps/wallet-mobile/translations/messages/src/features/Claim/common/useStrings.json b/apps/wallet-mobile/translations/messages/src/features/Claim/common/useStrings.json
new file mode 100644
index 0000000000..c8398744db
--- /dev/null
+++ b/apps/wallet-mobile/translations/messages/src/features/Claim/common/useStrings.json
@@ -0,0 +1,287 @@
+[
+ {
+ "id": "claim.askConfirmation.title",
+ "defaultMessage": "!!!Confirm Claim",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 41,
+ "column": 26,
+ "index": 1766
+ },
+ "end": {
+ "line": 44,
+ "column": 5,
+ "index": 1856
+ }
+ },
+ {
+ "id": "claim.showSuccess.title",
+ "defaultMessage": "!!!Success",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 45,
+ "column": 22,
+ "index": 1880
+ },
+ "end": {
+ "line": 48,
+ "column": 5,
+ "index": 1960
+ }
+ },
+ {
+ "id": "claim.done.title",
+ "defaultMessage": "!!!Done",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 50,
+ "column": 15,
+ "index": 1978
+ },
+ "end": {
+ "line": 53,
+ "column": 5,
+ "index": 2048
+ }
+ },
+ {
+ "id": "claim.done.message",
+ "defaultMessage": "!!!Done",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 54,
+ "column": 17,
+ "index": 2067
+ },
+ "end": {
+ "line": 57,
+ "column": 5,
+ "index": 2139
+ }
+ },
+ {
+ "id": "claim.accepted.title",
+ "defaultMessage": "!!!Accepted",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 58,
+ "column": 19,
+ "index": 2160
+ },
+ "end": {
+ "line": 61,
+ "column": 5,
+ "index": 2238
+ }
+ },
+ {
+ "id": "claim.accepted.message",
+ "defaultMessage": "!!!Accepted",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 62,
+ "column": 21,
+ "index": 2261
+ },
+ "end": {
+ "line": 65,
+ "column": 5,
+ "index": 2341
+ }
+ },
+ {
+ "id": "claim.processing.title",
+ "defaultMessage": "!!!Processing",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 66,
+ "column": 21,
+ "index": 2364
+ },
+ "end": {
+ "line": 69,
+ "column": 5,
+ "index": 2446
+ }
+ },
+ {
+ "id": "claim.processing.message",
+ "defaultMessage": "!!!Processing",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 70,
+ "column": 23,
+ "index": 2471
+ },
+ "end": {
+ "line": 73,
+ "column": 5,
+ "index": 2555
+ }
+ },
+ {
+ "id": "claim.addressSharingWarning",
+ "defaultMessage": "!!!Address sharing warning",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 75,
+ "column": 27,
+ "index": 2585
+ },
+ "end": {
+ "line": 78,
+ "column": 5,
+ "index": 2685
+ }
+ },
+ {
+ "id": "claim.domain",
+ "defaultMessage": "!!!Domain",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 79,
+ "column": 12,
+ "index": 2699
+ },
+ "end": {
+ "line": 82,
+ "column": 5,
+ "index": 2767
+ }
+ },
+ {
+ "id": "claim.code",
+ "defaultMessage": "!!!Code",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 83,
+ "column": 10,
+ "index": 2779
+ },
+ "end": {
+ "line": 86,
+ "column": 5,
+ "index": 2843
+ }
+ },
+ {
+ "id": "claim.apiError.title",
+ "defaultMessage": "!!!Error title",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 88,
+ "column": 19,
+ "index": 2865
+ },
+ "end": {
+ "line": 91,
+ "column": 5,
+ "index": 2946
+ }
+ },
+ {
+ "id": "claim.apiError.invalidRequest",
+ "defaultMessage": "!!!Invalid request",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 92,
+ "column": 28,
+ "index": 2976
+ },
+ "end": {
+ "line": 95,
+ "column": 5,
+ "index": 3070
+ }
+ },
+ {
+ "id": "claim.apiError.notFound",
+ "defaultMessage": "!!!Not found",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 96,
+ "column": 22,
+ "index": 3094
+ },
+ "end": {
+ "line": 99,
+ "column": 5,
+ "index": 3176
+ }
+ },
+ {
+ "id": "claim.apiError.alreadyClaimed",
+ "defaultMessage": "!!!Already claimed",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 100,
+ "column": 28,
+ "index": 3206
+ },
+ "end": {
+ "line": 103,
+ "column": 5,
+ "index": 3300
+ }
+ },
+ {
+ "id": "claim.apiError.expired",
+ "defaultMessage": "!!!Expired",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 104,
+ "column": 21,
+ "index": 3323
+ },
+ "end": {
+ "line": 107,
+ "column": 5,
+ "index": 3402
+ }
+ },
+ {
+ "id": "claim.apiError.tooEarly",
+ "defaultMessage": "!!!Too early",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 108,
+ "column": 22,
+ "index": 3426
+ },
+ "end": {
+ "line": 111,
+ "column": 5,
+ "index": 3508
+ }
+ },
+ {
+ "id": "claim.apiError.rateLimited",
+ "defaultMessage": "!!!Rate limited",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 112,
+ "column": 25,
+ "index": 3535
+ },
+ "end": {
+ "line": 115,
+ "column": 5,
+ "index": 3623
+ }
+ },
+ {
+ "id": "global.actions.dialogs.commonbuttons.continueButton",
+ "defaultMessage": "!!!Continue",
+ "file": "src/features/Claim/common/useStrings.tsx",
+ "start": {
+ "line": 117,
+ "column": 14,
+ "index": 3640
+ },
+ "end": {
+ "line": 120,
+ "column": 5,
+ "index": 3749
+ }
+ }
+]
\ No newline at end of file
diff --git a/apps/wallet-mobile/translations/messages/src/i18n/global-messages.json b/apps/wallet-mobile/translations/messages/src/i18n/global-messages.json
index 869e73994e..ea6379de46 100644
--- a/apps/wallet-mobile/translations/messages/src/i18n/global-messages.json
+++ b/apps/wallet-mobile/translations/messages/src/i18n/global-messages.json
@@ -49,14 +49,14 @@
"defaultMessage": "!!!Cancel",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 717,
+ "line": 776,
"column": 10,
- "index": 21532
+ "index": 22832
},
"end": {
- "line": 720,
+ "line": 779,
"column": 3,
- "index": 21595
+ "index": 22895
}
},
{
@@ -1129,14 +1129,14 @@
"defaultMessage": "!!!Feature not supported",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 626,
+ "line": 685,
"column": 16,
- "index": 19299
+ "index": 20599
},
"end": {
- "line": 629,
+ "line": 688,
"column": 3,
- "index": 19383
+ "index": 20683
}
},
{
@@ -1309,14 +1309,224 @@
"defaultMessage": "!!!Assets",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 654,
+ "line": 713,
"column": 15,
- "index": 20124
+ "index": 21424
},
"end": {
- "line": 657,
+ "line": 716,
+ "column": 3,
+ "index": 21499
+ }
+ },
+ {
+ "id": "api.error.title",
+ "defaultMessage": "!!!API error title",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 478,
+ "column": 9,
+ "index": 15695
+ },
+ "end": {
+ "line": 481,
+ "column": 3,
+ "index": 15769
+ }
+ },
+ {
+ "id": "api.error.badRequest",
+ "defaultMessage": "!!!Bad request",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 482,
+ "column": 14,
+ "index": 15785
+ },
+ "end": {
+ "line": 485,
+ "column": 3,
+ "index": 15860
+ }
+ },
+ {
+ "id": "api.error.unauthorized",
+ "defaultMessage": "!!!Unauthorized",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 486,
+ "column": 16,
+ "index": 15878
+ },
+ "end": {
+ "line": 489,
+ "column": 3,
+ "index": 15956
+ }
+ },
+ {
+ "id": "api.error.forbidden",
+ "defaultMessage": "!!!Forbidden",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 490,
+ "column": 13,
+ "index": 15971
+ },
+ "end": {
+ "line": 493,
+ "column": 3,
+ "index": 16043
+ }
+ },
+ {
+ "id": "api.error.notFound",
+ "defaultMessage": "!!!Not found",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 494,
+ "column": 12,
+ "index": 16057
+ },
+ "end": {
+ "line": 497,
+ "column": 3,
+ "index": 16128
+ }
+ },
+ {
+ "id": "api.error.conflict",
+ "defaultMessage": "!!!Conflict",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 498,
+ "column": 12,
+ "index": 16142
+ },
+ "end": {
+ "line": 501,
+ "column": 3,
+ "index": 16212
+ }
+ },
+ {
+ "id": "api.error.gone",
+ "defaultMessage": "!!!Gone",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 502,
+ "column": 8,
+ "index": 16222
+ },
+ "end": {
+ "line": 505,
+ "column": 3,
+ "index": 16284
+ }
+ },
+ {
+ "id": "api.error.tooEarly",
+ "defaultMessage": "!!!Too early",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 506,
+ "column": 12,
+ "index": 16298
+ },
+ "end": {
+ "line": 509,
+ "column": 3,
+ "index": 16369
+ }
+ },
+ {
+ "id": "api.error.tooManyRequests",
+ "defaultMessage": "!!!Too many requests",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 510,
+ "column": 19,
+ "index": 16390
+ },
+ "end": {
+ "line": 513,
+ "column": 3,
+ "index": 16476
+ }
+ },
+ {
+ "id": "api.error.serverSide",
+ "defaultMessage": "!!!Server side",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 514,
+ "column": 14,
+ "index": 16492
+ },
+ "end": {
+ "line": 517,
+ "column": 3,
+ "index": 16567
+ }
+ },
+ {
+ "id": "api.error.unknown",
+ "defaultMessage": "!!!Unknown",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 518,
+ "column": 11,
+ "index": 16580
+ },
+ "end": {
+ "line": 521,
+ "column": 3,
+ "index": 16648
+ }
+ },
+ {
+ "id": "api.error.network",
+ "defaultMessage": "!!!Network",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 522,
+ "column": 11,
+ "index": 16661
+ },
+ "end": {
+ "line": 525,
+ "column": 3,
+ "index": 16729
+ }
+ },
+ {
+ "id": "api.error.invalidState",
+ "defaultMessage": "!!!Invalid state",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 526,
+ "column": 16,
+ "index": 16747
+ },
+ "end": {
+ "line": 529,
+ "column": 3,
+ "index": 16826
+ }
+ },
+ {
+ "id": "api.error.responseMalformed",
+ "defaultMessage": "!!!Response malformed",
+ "file": "src/i18n/global-messages.ts",
+ "start": {
+ "line": 530,
+ "column": 21,
+ "index": 16849
+ },
+ "end": {
+ "line": 533,
"column": 3,
- "index": 20199
+ "index": 16938
}
},
{
@@ -1324,14 +1534,14 @@
"defaultMessage": "!!!Send",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 478,
+ "line": 537,
"column": 8,
- "index": 15699
+ "index": 16999
},
"end": {
- "line": 481,
+ "line": 540,
"column": 3,
- "index": 15758
+ "index": 17058
}
},
{
@@ -1339,14 +1549,14 @@
"defaultMessage": "!!!Receive",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 482,
+ "line": 541,
"column": 11,
- "index": 15771
+ "index": 17071
},
"end": {
- "line": 485,
+ "line": 544,
"column": 3,
- "index": 15836
+ "index": 17136
}
},
{
@@ -1354,14 +1564,14 @@
"defaultMessage": "!!!Buy",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 486,
+ "line": 545,
"column": 7,
- "index": 15845
+ "index": 17145
},
"end": {
- "line": 489,
+ "line": 548,
"column": 3,
- "index": 15902
+ "index": 17202
}
},
{
@@ -1369,14 +1579,14 @@
"defaultMessage": "!!!Exchange ADA",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 490,
+ "line": 549,
"column": 12,
- "index": 15916
+ "index": 17216
},
"end": {
- "line": 493,
+ "line": 552,
"column": 3,
- "index": 15987
+ "index": 17287
}
},
{
@@ -1384,14 +1594,14 @@
"defaultMessage": "!!!Yoroi uses Banxa to provide direct Fiat-ADA exchange. By clicking “Proceed,” you also acknowledge that you will be redirected to our partner’s website, where you may be asked to accept their terms and conditions.",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 494,
+ "line": 553,
"column": 11,
- "index": 16000
+ "index": 17300
},
"end": {
- "line": 498,
+ "line": 557,
"column": 3,
- "index": 16283
+ "index": 17583
}
},
{
@@ -1399,14 +1609,14 @@
"defaultMessage": "!!!Proceed",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 499,
+ "line": 558,
"column": 11,
- "index": 16296
+ "index": 17596
},
"end": {
- "line": 502,
+ "line": 561,
"column": 3,
- "index": 16361
+ "index": 17661
}
},
{
@@ -1414,14 +1624,14 @@
"defaultMessage": "!!!Swap",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 503,
+ "line": 562,
"column": 8,
- "index": 16371
+ "index": 17671
},
"end": {
- "line": 506,
+ "line": 565,
"column": 3,
- "index": 16430
+ "index": 17730
}
},
{
@@ -1429,14 +1639,14 @@
"defaultMessage": "!!!Coming soon",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 610,
+ "line": 669,
"column": 14,
- "index": 18947
+ "index": 20247
},
"end": {
- "line": 613,
+ "line": 672,
"column": 3,
- "index": 19019
+ "index": 20319
}
},
{
@@ -1444,14 +1654,14 @@
"defaultMessage": "!!!ADA",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 514,
+ "line": 573,
"column": 29,
- "index": 16593
+ "index": 17893
},
"end": {
- "line": 517,
+ "line": 576,
"column": 3,
- "index": 16659
+ "index": 17959
}
},
{
@@ -1459,14 +1669,14 @@
"defaultMessage": "!!!BRL",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 518,
+ "line": 577,
"column": 29,
- "index": 16690
+ "index": 17990
},
"end": {
- "line": 521,
+ "line": 580,
"column": 3,
- "index": 16756
+ "index": 18056
}
},
{
@@ -1474,14 +1684,14 @@
"defaultMessage": "!!!BTC",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 522,
+ "line": 581,
"column": 29,
- "index": 16787
+ "index": 18087
},
"end": {
- "line": 525,
+ "line": 584,
"column": 3,
- "index": 16853
+ "index": 18153
}
},
{
@@ -1489,14 +1699,14 @@
"defaultMessage": "!!!CNY",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 526,
+ "line": 585,
"column": 29,
- "index": 16884
+ "index": 18184
},
"end": {
- "line": 529,
+ "line": 588,
"column": 3,
- "index": 16950
+ "index": 18250
}
},
{
@@ -1504,14 +1714,14 @@
"defaultMessage": "!!!ETH",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 530,
+ "line": 589,
"column": 29,
- "index": 16981
+ "index": 18281
},
"end": {
- "line": 533,
+ "line": 592,
"column": 3,
- "index": 17047
+ "index": 18347
}
},
{
@@ -1519,14 +1729,14 @@
"defaultMessage": "!!!EUR",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 534,
+ "line": 593,
"column": 29,
- "index": 17078
+ "index": 18378
},
"end": {
- "line": 537,
+ "line": 596,
"column": 3,
- "index": 17144
+ "index": 18444
}
},
{
@@ -1534,14 +1744,14 @@
"defaultMessage": "!!!JPY",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 538,
+ "line": 597,
"column": 29,
- "index": 17175
+ "index": 18475
},
"end": {
- "line": 541,
+ "line": 600,
"column": 3,
- "index": 17241
+ "index": 18541
}
},
{
@@ -1549,14 +1759,14 @@
"defaultMessage": "!!!KRW",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 542,
+ "line": 601,
"column": 29,
- "index": 17272
+ "index": 18572
},
"end": {
- "line": 545,
+ "line": 604,
"column": 3,
- "index": 17338
+ "index": 18638
}
},
{
@@ -1564,14 +1774,14 @@
"defaultMessage": "!!!USD",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 546,
+ "line": 605,
"column": 29,
- "index": 17369
+ "index": 18669
},
"end": {
- "line": 549,
+ "line": 608,
"column": 3,
- "index": 17435
+ "index": 18735
}
},
{
@@ -1579,14 +1789,14 @@
"defaultMessage": "!!!Apply",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 553,
+ "line": 612,
"column": 9,
- "index": 17482
+ "index": 18782
},
"end": {
- "line": 556,
+ "line": 615,
"column": 3,
- "index": 17543
+ "index": 18843
}
},
{
@@ -1594,14 +1804,14 @@
"defaultMessage": "!!!Max",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 557,
+ "line": 616,
"column": 7,
- "index": 17552
+ "index": 18852
},
"end": {
- "line": 560,
+ "line": 619,
"column": 3,
- "index": 17609
+ "index": 18909
}
},
{
@@ -1609,14 +1819,14 @@
"defaultMessage": "!!!All done!",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 561,
+ "line": 620,
"column": 11,
- "index": 17622
+ "index": 18922
},
"end": {
- "line": 564,
+ "line": 623,
"column": 3,
- "index": 17745
+ "index": 19045
}
},
{
@@ -1624,14 +1834,14 @@
"defaultMessage": "!!!Attention",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 565,
+ "line": 624,
"column": 13,
- "index": 17760
+ "index": 19060
},
"end": {
- "line": 568,
+ "line": 627,
"column": 3,
- "index": 17860
+ "index": 19160
}
},
{
@@ -1639,14 +1849,14 @@
"defaultMessage": "!!!Try again",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 569,
+ "line": 628,
"column": 12,
- "index": 17874
+ "index": 19174
},
"end": {
- "line": 572,
+ "line": 631,
"column": 3,
- "index": 17942
+ "index": 19242
}
},
{
@@ -1654,14 +1864,14 @@
"defaultMessage": "!!!Wallet name cannot exceed 40 letters",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 573,
+ "line": 632,
"column": 26,
- "index": 17970
+ "index": 19270
},
"end": {
- "line": 576,
+ "line": 635,
"column": 3,
- "index": 18080
+ "index": 19380
}
},
{
@@ -1669,14 +1879,14 @@
"defaultMessage": "!!!You already have a wallet with this name",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 577,
+ "line": 636,
"column": 35,
- "index": 18117
+ "index": 19417
},
"end": {
- "line": 580,
+ "line": 639,
"column": 3,
- "index": 18236
+ "index": 19536
}
},
{
@@ -1684,14 +1894,14 @@
"defaultMessage": "!!!Must be filled",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 581,
+ "line": 640,
"column": 31,
- "index": 18269
+ "index": 19569
},
"end": {
- "line": 584,
+ "line": 643,
"column": 3,
- "index": 18362
+ "index": 19662
}
},
{
@@ -1700,14 +1910,14 @@
"defaultMessage": "!!!please wait ...",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 585,
+ "line": 644,
"column": 14,
- "index": 18378
+ "index": 19678
},
"end": {
- "line": 589,
+ "line": 648,
"column": 3,
- "index": 18517
+ "index": 19817
}
},
{
@@ -1715,14 +1925,14 @@
"defaultMessage": "!!!Please confirm",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 590,
+ "line": 649,
"column": 17,
- "index": 18536
+ "index": 19836
},
"end": {
- "line": 593,
+ "line": 652,
"column": 3,
- "index": 18614
+ "index": 19914
}
},
{
@@ -1730,14 +1940,14 @@
"defaultMessage": "!!!Terms of use",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 594,
+ "line": 653,
"column": 7,
- "index": 18623
+ "index": 19923
},
"end": {
- "line": 597,
+ "line": 656,
"column": 3,
- "index": 18696
+ "index": 19996
}
},
{
@@ -1745,14 +1955,14 @@
"defaultMessage": "!!!OK",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 598,
+ "line": 657,
"column": 6,
- "index": 18704
+ "index": 20004
},
"end": {
- "line": 601,
+ "line": 660,
"column": 3,
- "index": 18759
+ "index": 20059
}
},
{
@@ -1760,14 +1970,14 @@
"defaultMessage": "!!!close",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 602,
+ "line": 661,
"column": 9,
- "index": 18770
+ "index": 20070
},
"end": {
- "line": 605,
+ "line": 664,
"column": 3,
- "index": 18831
+ "index": 20131
}
},
{
@@ -1775,14 +1985,14 @@
"defaultMessage": "!!!Available funds",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 606,
+ "line": 665,
"column": 18,
- "index": 18851
+ "index": 20151
},
"end": {
- "line": 609,
+ "line": 668,
"column": 3,
- "index": 18931
+ "index": 20231
}
},
{
@@ -1790,14 +2000,14 @@
"defaultMessage": "!!!Deprecated",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 614,
+ "line": 673,
"column": 14,
- "index": 19035
+ "index": 20335
},
"end": {
- "line": 617,
+ "line": 676,
"column": 3,
- "index": 19106
+ "index": 20406
}
},
{
@@ -1805,14 +2015,14 @@
"defaultMessage": "!!!Epoch",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 618,
+ "line": 677,
"column": 14,
- "index": 19122
+ "index": 20422
},
"end": {
- "line": 621,
+ "line": 680,
"column": 3,
- "index": 19196
+ "index": 20496
}
},
{
@@ -1820,14 +2030,14 @@
"defaultMessage": "!!!Learn more",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 622,
+ "line": 681,
"column": 13,
- "index": 19211
+ "index": 20511
},
"end": {
- "line": 625,
+ "line": 684,
"column": 3,
- "index": 19281
+ "index": 20581
}
},
{
@@ -1835,14 +2045,14 @@
"defaultMessage": "!!!Total",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 630,
+ "line": 689,
"column": 9,
- "index": 19394
+ "index": 20694
},
"end": {
- "line": 633,
+ "line": 692,
"column": 3,
- "index": 19455
+ "index": 20755
}
},
{
@@ -1850,14 +2060,14 @@
"defaultMessage": "!!!Total ADA",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 634,
+ "line": 693,
"column": 12,
- "index": 19469
+ "index": 20769
},
"end": {
- "line": 637,
+ "line": 696,
"column": 3,
- "index": 19537
+ "index": 20837
}
},
{
@@ -1865,14 +2075,14 @@
"defaultMessage": "!!!Stake pool name",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 638,
+ "line": 697,
"column": 17,
- "index": 19556
+ "index": 20856
},
"end": {
- "line": 641,
+ "line": 700,
"column": 3,
- "index": 19643
+ "index": 20943
}
},
{
@@ -1880,14 +2090,14 @@
"defaultMessage": "!!!Stake pool hash",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 642,
+ "line": 701,
"column": 17,
- "index": 19662
+ "index": 20962
},
"end": {
- "line": 645,
+ "line": 704,
"column": 3,
- "index": 19749
+ "index": 21049
}
},
{
@@ -1895,14 +2105,14 @@
"defaultMessage": "!!!We are experiencing synchronization issues.",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 646,
+ "line": 705,
"column": 37,
- "index": 19788
+ "index": 21088
},
"end": {
- "line": 649,
+ "line": 708,
"column": 3,
- "index": 19923
+ "index": 21223
}
},
{
@@ -1910,14 +2120,14 @@
"defaultMessage": "!!!We are experiencing synchronization issues. Pull to refresh",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 650,
+ "line": 709,
"column": 34,
- "index": 19959
+ "index": 21259
},
"end": {
- "line": 653,
+ "line": 712,
"column": 3,
- "index": 20107
+ "index": 21407
}
},
{
@@ -1925,14 +2135,14 @@
"defaultMessage": "!!!Register to vote",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 658,
+ "line": 717,
"column": 15,
- "index": 20216
+ "index": 21516
},
"end": {
- "line": 661,
+ "line": 720,
"column": 3,
- "index": 20301
+ "index": 21601
}
},
{
@@ -1940,14 +2150,14 @@
"defaultMessage": "!!!Participating requires at least {requiredBalance},but you only have {currentBalance}. Unwithdrawn rewards are not included in this amount",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 662,
+ "line": 721,
"column": 23,
- "index": 20326
+ "index": 21626
},
"end": {
- "line": 668,
+ "line": 727,
"column": 3,
- "index": 20574
+ "index": 21874
}
},
{
@@ -1955,14 +2165,14 @@
"defaultMessage": "!!! NFTs",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 669,
+ "line": 728,
"column": 8,
- "index": 20584
+ "index": 21884
},
"end": {
- "line": 672,
+ "line": 731,
"column": 3,
- "index": 20644
+ "index": 21944
}
},
{
@@ -1970,14 +2180,14 @@
"defaultMessage": "!!! Tokens",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 673,
+ "line": 732,
"column": 10,
- "index": 20656
+ "index": 21956
},
"end": {
- "line": 676,
+ "line": 735,
"column": 3,
- "index": 20720
+ "index": 22020
}
},
{
@@ -1985,14 +2195,14 @@
"defaultMessage": "!!! Assets",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 677,
+ "line": 736,
"column": 10,
- "index": 20732
+ "index": 22032
},
"end": {
- "line": 680,
+ "line": 739,
"column": 3,
- "index": 20796
+ "index": 22096
}
},
{
@@ -2000,14 +2210,14 @@
"defaultMessage": "!!! available",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 681,
+ "line": 740,
"column": 13,
- "index": 20811
+ "index": 22111
},
"end": {
- "line": 684,
+ "line": 743,
"column": 3,
- "index": 20881
+ "index": 22181
}
},
{
@@ -2015,14 +2225,14 @@
"defaultMessage": "!!! Dex",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 685,
+ "line": 744,
"column": 9,
- "index": 20892
+ "index": 22192
},
"end": {
- "line": 688,
+ "line": 747,
"column": 3,
- "index": 20952
+ "index": 22252
}
},
{
@@ -2030,14 +2240,14 @@
"defaultMessage": "!!! Price",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 689,
+ "line": 748,
"column": 9,
- "index": 20963
+ "index": 22263
},
"end": {
- "line": 692,
+ "line": 751,
"column": 3,
- "index": 21025
+ "index": 22325
}
},
{
@@ -2045,14 +2255,14 @@
"defaultMessage": "!!!All",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 693,
+ "line": 752,
"column": 7,
- "index": 21034
+ "index": 22334
},
"end": {
- "line": 696,
+ "line": 755,
"column": 3,
- "index": 21091
+ "index": 22391
}
},
{
@@ -2060,14 +2270,14 @@
"defaultMessage": "!!!Locked deposit",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 697,
+ "line": 756,
"column": 17,
- "index": 21110
+ "index": 22410
},
"end": {
- "line": 700,
+ "line": 759,
"column": 3,
- "index": 21188
+ "index": 22488
}
},
{
@@ -2075,14 +2285,14 @@
"defaultMessage": "!!!Locked deposit hint",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 701,
+ "line": 760,
"column": 21,
- "index": 21211
+ "index": 22511
},
"end": {
- "line": 704,
+ "line": 763,
"column": 3,
- "index": 21298
+ "index": 22598
}
},
{
@@ -2090,14 +2300,14 @@
"defaultMessage": "!!!Currency",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 705,
+ "line": 764,
"column": 12,
- "index": 21312
+ "index": 22612
},
"end": {
- "line": 708,
+ "line": 767,
"column": 3,
- "index": 21379
+ "index": 22679
}
},
{
@@ -2105,14 +2315,14 @@
"defaultMessage": "!!!Next",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 709,
+ "line": 768,
"column": 8,
- "index": 21389
+ "index": 22689
},
"end": {
- "line": 712,
+ "line": 771,
"column": 3,
- "index": 21448
+ "index": 22748
}
},
{
@@ -2120,14 +2330,14 @@
"defaultMessage": "!!!Error",
"file": "src/i18n/global-messages.ts",
"start": {
- "line": 713,
+ "line": 772,
"column": 9,
- "index": 21459
+ "index": 22759
},
"end": {
- "line": 716,
+ "line": 775,
"column": 3,
- "index": 21520
+ "index": 22820
}
}
]
\ No newline at end of file
diff --git a/metro.config.js b/metro.config.js
index e81e7ade8c..5b405ba0d4 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -6,6 +6,7 @@ module.exports = {
path.resolve(__dirname, "node_modules"),
path.resolve(__dirname, "packages/types"),
path.resolve(__dirname, "packages/banxa"),
+ path.resolve(__dirname, "packages/links"),
path.resolve(__dirname, "packages/common"),
path.resolve(__dirname, "packages/links"),
path.resolve(__dirname, "packages/api"),
diff --git a/packages/common/src/api/fetchData.test.ts b/packages/common/src/api/fetchData.test.ts
new file mode 100644
index 0000000000..f688668803
--- /dev/null
+++ b/packages/common/src/api/fetchData.test.ts
@@ -0,0 +1,104 @@
+import axios from 'axios'
+import MockAdapter from 'axios-mock-adapter'
+import {fetchData} from './fetchData' // Update with the actual path
+
+const mock = new MockAdapter(axios)
+
+describe('fetchData', () => {
+ afterEach(() => {
+ mock.reset()
+ })
+
+ it('should handle a GET request successfully (without method)', async () => {
+ const mockData = {id: 1, name: 'Test User'}
+ mock.onGet('https://example.com/data').reply(200, mockData)
+
+ const response = await fetchData<{id: number; name: string}>({
+ url: 'https://example.com/data',
+ })
+
+ if (response.tag === 'left') fail('Response should be right')
+ expect(response.tag).toBe('right')
+ expect(response.value.data).toEqual(mockData)
+ expect(response.value.status).toBe(200)
+ })
+
+ it('should handle a GET request successfully (providing method)', async () => {
+ const mockData = {id: 1, name: 'Test User'}
+ mock.onGet('https://example.com/data').reply(200, mockData)
+
+ const response = await fetchData<{id: number; name: string}>({
+ url: 'https://example.com/data',
+ method: 'get',
+ })
+
+ if (response.tag === 'left') fail('Response should be right')
+ expect(response.tag).toBe('right')
+ expect(response.value.data).toEqual(mockData)
+ expect(response.value.status).toBe(200)
+ })
+
+ it('should handle a POST request successfully', async () => {
+ const postData = {title: 'New Post'}
+ const mockResponse = {id: 1, title: 'New Post'}
+
+ // Simulate a successful POST request
+ mock.onPost('https://example.com/posts', postData).reply(200, mockResponse)
+
+ const response = await fetchData<
+ {id: number; title: string},
+ typeof postData
+ >({
+ url: 'https://example.com/posts',
+ method: 'post',
+ data: postData,
+ })
+
+ if (response.tag === 'left') fail('Response should be right')
+ expect(response.tag).toBe('right')
+ expect(response.value.data).toEqual(mockResponse)
+ expect(response.value.status).toBe(200)
+ })
+
+ it('should handle an error response', async () => {
+ mock.onGet('https://example.com/error').reply(500)
+
+ const response = await fetchData<{id: number; name: string}>({
+ url: 'https://example.com/error',
+ })
+
+ if (response.tag === 'right') fail('Response should be left')
+ expect(response.tag).toBe('left')
+ expect(response.error.status).toBe(500)
+ })
+
+ it('should handle a timeout', async () => {
+ mock.onGet('https://example.com/network-error').reply(() => {
+ return Promise.reject({
+ request: {},
+ })
+ })
+
+ const response = await fetchData<{id: number; name: string}>({
+ url: 'https://example.com/network-error',
+ })
+
+ if (response.tag === 'right') fail('Response should be left')
+ expect(response.tag).toBe('left')
+ expect(response.error.status).toBe(-1)
+ })
+
+ it('should handle an unknown error', async () => {
+ mock.onGet('https://example.com/unknown-error').reply(() => {
+ throw new Error('Some error')
+ })
+
+ const response = await fetchData<{id: number; name: string}>({
+ url: 'https://example.com/unknown-error',
+ })
+
+ if (response.tag === 'right') fail('Response should be left')
+ expect(response.tag).toBe('left')
+ expect(response.error.status).toBe(-2)
+ })
+})
diff --git a/packages/common/src/api/fetchData.ts b/packages/common/src/api/fetchData.ts
new file mode 100644
index 0000000000..614b63bb29
--- /dev/null
+++ b/packages/common/src/api/fetchData.ts
@@ -0,0 +1,106 @@
+import axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from 'axios'
+import {Api} from '@yoroi/types'
+
+type GetRequestConfig = {
+ url: string
+ method?: 'get'
+ headers?: Record
+}
+
+type OtherRequestConfig = {
+ url: string
+ method: 'post' | 'put' | 'delete'
+ data?: D
+ headers?: Record
+}
+
+type RequestConfig = GetRequestConfig | OtherRequestConfig
+export type FetchData = (
+ config: RequestConfig,
+) => Promise>
+
+/**
+ * Performs an HTTP request using Axios based on the specified configuration.
+ * This function simplifies making HTTP requests by handling different
+ * request methods and their respective data and headers.
+ *
+ * @param config - The configuration object for the request.
+ * This includes the URL, HTTP method, optional data, and headers.
+ * The type of `config` varies based on the HTTP method:
+ * - For `GET` requests, `data` should not be provided.
+ * - For `POST`, `PUT`, and `DELETE` requests, `data` is optional.
+ *
+ * @returns A `Promise` that resolves to the response data on a successful request
+ * or an error object on failure. The error object includes the HTTP status
+ * code and error message.
+ *
+ * @template T - The expected type of the response data.
+ * @template D - The type of the data to be sent with the request (for `POST`, `PUT`, `DELETE`).
+ *
+ * @example
+ * ```typescript
+ * // Example of a GET request
+ * fetchData<{ someDataType }>({
+ * url: 'https://example.com/data',
+ * }).then(response => {
+ * // Handle response
+ * }).catch(error => {
+ * // Handle error
+ * })
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // Example of a POST request with data
+ * fetchData<{ someDataType }, { somePayloadType }>({
+ * url: 'https://example.com/data',
+ * method: 'post',
+ * data: { /* some data *\/ }
+ * }).then(response => {
+ * // Handle response
+ * }).catch(error => {
+ * // Handle error
+ * })
+ * ```
+ */
+export const fetchData: FetchData = (
+ config: RequestConfig,
+): Promise> => {
+ const method = config.method ?? 'get'
+ const isNotGet = method !== 'get'
+
+ const axiosConfig: AxiosRequestConfig = {
+ url: config.url,
+ method: method,
+ headers: config.headers ?? {'Content-Type': 'application/json'},
+ ...(isNotGet && 'data' in config && {data: config.data}),
+ }
+
+ return axios(axiosConfig)
+ .then((response: AxiosResponse) => {
+ return {
+ tag: 'right',
+ value: {status: response.status, data: response.data},
+ } as const
+ })
+ .catch((error: AxiosError) => {
+ if (error.response) {
+ const status = error.response.status
+ const message = error.response.statusText
+ return {
+ tag: 'left',
+ error: {status, message},
+ } as const
+ } else if (error.request) {
+ return {
+ tag: 'left',
+ error: {status: -1, message: 'Network (no response)'},
+ } as const
+ } else {
+ return {
+ tag: 'left',
+ error: {status: -2, message: `Invalid state: ${error.message}`},
+ } as const
+ }
+ })
+}
diff --git a/packages/common/src/api/fetcher.ts b/packages/common/src/api/fetcher.ts
index 66c0101d5e..5ee968750b 100644
--- a/packages/common/src/api/fetcher.ts
+++ b/packages/common/src/api/fetcher.ts
@@ -1,6 +1,10 @@
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios'
+
import {ApiError, NetworkError} from '../errors/errors'
+/**
+ * @deprecated This function is deprecated and will be removed in a future release. Use `fetchData` instead.
+ */
export const fetcher: Fetcher = async (
config: AxiosRequestConfig,
): Promise => {
diff --git a/packages/common/src/api/handleApiError.test.ts b/packages/common/src/api/handleApiError.test.ts
new file mode 100644
index 0000000000..65b59b11e4
--- /dev/null
+++ b/packages/common/src/api/handleApiError.test.ts
@@ -0,0 +1,85 @@
+import {handleApiError} from './handleApiError'
+import {Api} from '@yoroi/types'
+
+describe('handleApiError', () => {
+ it('should throw NetworkError for -1 status', () => {
+ expect(() =>
+ handleApiError({status: -1, message: 'Network error'}),
+ ).toThrow(Api.Errors.Network)
+ })
+
+ it('should throw InvalidStateError for -2 status', () => {
+ expect(() =>
+ handleApiError({status: -2, message: 'Invalid state'}),
+ ).toThrow(Api.Errors.InvalidState)
+ })
+
+ it('should throw BadRequestError for 400 status', () => {
+ expect(() => handleApiError({status: 400, message: 'Bad request'})).toThrow(
+ Api.Errors.BadRequest,
+ )
+ })
+
+ it('should throw UnauthorizedError for 401 status', () => {
+ expect(() =>
+ handleApiError({status: 401, message: 'Unauthorized'}),
+ ).toThrow(Api.Errors.Unauthorized)
+ })
+
+ it('should throw ForbiddenError for 403 status', () => {
+ expect(() => handleApiError({status: 403, message: 'Forbidden'})).toThrow(
+ Api.Errors.Forbidden,
+ )
+ })
+
+ it('should throw NotFoundError for 404 status', () => {
+ expect(() => handleApiError({status: 404, message: 'Not found'})).toThrow(
+ Api.Errors.NotFound,
+ )
+ })
+
+ it('should throw ConflictError for 409 status', () => {
+ expect(() => handleApiError({status: 409, message: 'Conflict'})).toThrow(
+ Api.Errors.Conflict,
+ )
+ })
+
+ it('should throw GoneError for 410 status', () => {
+ expect(() => handleApiError({status: 410, message: 'Gone'})).toThrow(
+ Api.Errors.Gone,
+ )
+ })
+
+ it('should throw TooEarlyError for 425 status', () => {
+ expect(() => handleApiError({status: 425, message: 'Too early'})).toThrow(
+ Api.Errors.TooEarly,
+ )
+ })
+
+ it('should throw TooManyRequestsError for 429 status', () => {
+ expect(() =>
+ handleApiError({status: 429, message: 'Too many requests'}),
+ ).toThrow(Api.Errors.TooManyRequests)
+ })
+
+ it('should throw ServerSideError for 500 status', () => {
+ expect(() =>
+ handleApiError({status: 500, message: 'Server error'}),
+ ).toThrow(Api.Errors.ServerSide)
+ })
+
+ it('should throw ServerSideError for other 5xx status codes', () => {
+ expect(() =>
+ handleApiError({status: 503, message: 'Service unavailable'}),
+ ).toThrow(Api.Errors.ServerSide)
+ expect(() =>
+ handleApiError({status: 504, message: 'Gateway timeout'}),
+ ).toThrow(Api.Errors.ServerSide)
+ })
+
+ it('should throw UnknownError for unhandled status codes', () => {
+ expect(() =>
+ handleApiError({status: 999, message: 'Unknown error'}),
+ ).toThrow(Api.Errors.Unknown)
+ })
+})
diff --git a/packages/common/src/api/handleApiError.ts b/packages/common/src/api/handleApiError.ts
new file mode 100644
index 0000000000..2170863aa7
--- /dev/null
+++ b/packages/common/src/api/handleApiError.ts
@@ -0,0 +1,32 @@
+import {Api} from '@yoroi/types'
+
+export const handleApiError = (error: Api.ResponseError): never => {
+ if (error.status >= 500 && error.status < 600) {
+ throw new Api.Errors.ServerSide(error.message)
+ }
+
+ switch (error.status) {
+ case -1:
+ throw new Api.Errors.Network(error.message)
+ case -2:
+ throw new Api.Errors.InvalidState(error.message)
+ case 400:
+ throw new Api.Errors.BadRequest(error.message)
+ case 401:
+ throw new Api.Errors.Unauthorized(error.message)
+ case 403:
+ throw new Api.Errors.Forbidden(error.message)
+ case 404:
+ throw new Api.Errors.NotFound(error.message)
+ case 409:
+ throw new Api.Errors.Conflict(error.message)
+ case 410:
+ throw new Api.Errors.Gone(error.message)
+ case 425:
+ throw new Api.Errors.TooEarly(error.message)
+ case 429:
+ throw new Api.Errors.TooManyRequests(error.message)
+ default:
+ throw new Api.Errors.Unknown(error.message)
+ }
+}
diff --git a/packages/common/src/helpers/monads.test.ts b/packages/common/src/helpers/monads.test.ts
new file mode 100644
index 0000000000..a2f75df156
--- /dev/null
+++ b/packages/common/src/helpers/monads.test.ts
@@ -0,0 +1,27 @@
+import {isLeft, isRight} from './monads'
+
+describe('Either helper functions', () => {
+ describe('isLeft', () => {
+ it('returns true if Either is Left', () => {
+ const left = {tag: 'left', error: new Error('Error')} as const
+ expect(isLeft(left)).toBe(true)
+ })
+
+ it('returns false if Either is Right', () => {
+ const right = {tag: 'right', value: 'Success'} as const
+ expect(isLeft(right)).toBe(false)
+ })
+ })
+
+ describe('isRight', () => {
+ it('returns true if Either is Right', () => {
+ const right = {tag: 'right', value: 'Success'} as const
+ expect(isRight(right)).toBe(true)
+ })
+
+ it('returns false if Either is Left', () => {
+ const left = {tag: 'left', error: new Error('Error')} as const
+ expect(isRight(left)).toBe(false)
+ })
+ })
+})
diff --git a/packages/common/src/helpers/monads.ts b/packages/common/src/helpers/monads.ts
new file mode 100644
index 0000000000..6ad9d7f655
--- /dev/null
+++ b/packages/common/src/helpers/monads.ts
@@ -0,0 +1,13 @@
+import {Either} from '@yoroi/types'
+
+export function isLeft(
+ either: Either,
+): either is {tag: 'left'; error: E} {
+ return either.tag === 'left'
+}
+
+export function isRight(
+ either: Either,
+): either is {tag: 'right'; value: T} {
+ return either.tag === 'right'
+}
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 3376ca625e..0414cae385 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -6,4 +6,7 @@ export * from './storage/adapters/async-storage'
export * from './storage/translators/storage-reactjs'
export * from './storage/adapters/rootStorage'
export * from './api/fetcher'
+export * from './api/fetchData'
+export * from './api/handleApiError'
export * from './observer/observer'
+export * from './helpers/monads'
diff --git a/packages/types/src/api/errors.ts b/packages/types/src/api/errors.ts
new file mode 100644
index 0000000000..6a911c3071
--- /dev/null
+++ b/packages/types/src/api/errors.ts
@@ -0,0 +1,13 @@
+export class ApiErrorBadRequest extends Error {}
+export class ApiErrorUnauthorized extends Error {}
+export class ApiErrorForbidden extends Error {}
+export class ApiErrorNotFound extends Error {}
+export class ApiErrorConflict extends Error {}
+export class ApiErrorGone extends Error {}
+export class ApiErrorTooEarly extends Error {}
+export class ApiErrorTooManyRequests extends Error {}
+export class ApiErrorServerSide extends Error {}
+export class ApiErrorUnknown extends Error {}
+export class ApiErrorNetwork extends Error {}
+export class ApiErrorInvalidState extends Error {}
+export class ApiErrorResponseMalformed extends Error {}
diff --git a/packages/types/src/api/response.ts b/packages/types/src/api/response.ts
new file mode 100644
index 0000000000..d30bf228e7
--- /dev/null
+++ b/packages/types/src/api/response.ts
@@ -0,0 +1,13 @@
+import {Either} from '../helpers/types'
+
+export type ApiResponseError = {
+ status: number
+ message: string
+}
+
+export type ApiResponseSuccess = {
+ status: number
+ data: T
+}
+
+export type ApiResponse = Either>
diff --git a/packages/types/src/helpers/types.ts b/packages/types/src/helpers/types.ts
index 66f80ff456..d38225cbc3 100644
--- a/packages/types/src/helpers/types.ts
+++ b/packages/types/src/helpers/types.ts
@@ -5,3 +5,15 @@ export type Writable = {
export type RemoveUndefined = {
[K in keyof T]-?: Exclude
}
+
+export type Left = {
+ tag: 'left'
+ error: E
+}
+
+export type Right = {
+ tag: 'right'
+ value: T
+}
+
+export type Either = Left | Right
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index b00682f483..5d3a75753a 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -34,6 +34,22 @@ import {
LinksErrorUnsupportedAuthority,
LinksErrorUnsupportedVersion,
} from './links/errors'
+import {ApiResponse, ApiResponseError, ApiResponseSuccess} from './api/response'
+import {
+ ApiErrorBadRequest,
+ ApiErrorNotFound,
+ ApiErrorConflict,
+ ApiErrorForbidden,
+ ApiErrorGone,
+ ApiErrorTooEarly,
+ ApiErrorTooManyRequests,
+ ApiErrorUnauthorized,
+ ApiErrorNetwork,
+ ApiErrorUnknown,
+ ApiErrorServerSide,
+ ApiErrorInvalidState,
+ ApiErrorResponseMalformed,
+} from './api/errors'
export namespace App {
export interface Storage extends AppStorage {}
@@ -108,6 +124,29 @@ export namespace Links {
}
}
+export namespace Api {
+ export type ResponseError = ApiResponseError
+ export type ResponseSuccess = ApiResponseSuccess
+ export type Response = ApiResponse
+
+ export namespace Errors {
+ export class BadRequest extends ApiErrorBadRequest {}
+ export class NotFound extends ApiErrorNotFound {}
+ export class Conflict extends ApiErrorConflict {}
+ export class Forbidden extends ApiErrorForbidden {}
+ export class Gone extends ApiErrorGone {}
+ export class TooEarly extends ApiErrorTooEarly {}
+ export class TooManyRequests extends ApiErrorTooManyRequests {}
+ export class Unauthorized extends ApiErrorUnauthorized {}
+ export class ServerSide extends ApiErrorServerSide {}
+ export class Network extends ApiErrorNetwork {}
+ export class Unknown extends ApiErrorUnknown {}
+ export class InvalidState extends ApiErrorInvalidState {}
+
+ export class ResponseMalformed extends ApiErrorResponseMalformed {}
+ }
+}
+
export namespace Numbers {
export type Locale = NumberLocale
}