diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 7bab43acd..2b96ff0fb 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -696,7 +696,7 @@ SPEC CHECKSUMS:
EXSecureStore: 1b571851e6068b30b8ec097be848a04603c03bae
EXSplashScreen: ab5984afcca91e0af6b3138f01a8c47dc4955c51
FBLazyVector: 49cbe4b43e445b06bf29199b6ad2057649e4c8f5
- FBReactNativeSpec: 3560256311b67bbddc2ce503625e9edde9a6ee1d
+ FBReactNativeSpec: ef7076047ecfe23933320b0fb2b844474516e2e8
glog: 7a8788d88d2a384c05df45e95c5e763e4d81f3ad
lottie-ios: 48fac6be217c76937e36e340e2d09cf7b10b7f5f
lottie-react-native: 4dff8fe8d10ddef9e7880e770080f4a56121397e
diff --git a/package.json b/package.json
index 8fbc12e38..858e6feec 100644
--- a/package.json
+++ b/package.json
@@ -122,6 +122,7 @@
"react-qr-code": "^1.0.3",
"react-redux": "^7.2.2",
"redux-thunk": "^2.3.0",
+ "rn-swipe-button": "^1.3.6",
"tinycolor2": "^1.4.2",
"use-debounce": "^5.2.0",
"use-state-with-callback": "^2.0.3"
diff --git a/src/assets/images/deployModeIcon.svg b/src/assets/images/deployModeIcon.svg
new file mode 100644
index 000000000..50e7c4992
--- /dev/null
+++ b/src/assets/images/deployModeIcon.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/images/lockIconRed.svg b/src/assets/images/lockIconRed.svg
new file mode 100644
index 000000000..697ce934a
--- /dev/null
+++ b/src/assets/images/lockIconRed.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/Bullet.tsx b/src/components/Bullet.tsx
index 2ba5124f9..fecf04a3b 100644
--- a/src/components/Bullet.tsx
+++ b/src/components/Bullet.tsx
@@ -1,10 +1,20 @@
import React, { ReactNode } from 'react'
+import { ViewStyle } from 'react-native'
import Box from './Box'
import Text from './Text'
-type Props = { children?: ReactNode; color?: string }
-const Bullet = ({ children, color = 'black' }: Props) => (
-
+type Props = {
+ children?: ReactNode
+ color?: string
+ style?: ViewStyle
+}
+const Bullet = ({ children, color = 'black', style = {} }: Props) => (
+
•
diff --git a/src/components/HeliumActionSheet.tsx b/src/components/HeliumActionSheet.tsx
index ec92e9093..48f388188 100644
--- a/src/components/HeliumActionSheet.tsx
+++ b/src/components/HeliumActionSheet.tsx
@@ -1,17 +1,11 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { BoxProps } from '@shopify/restyle'
-import Close from '@assets/images/close.svg'
import CarotDown from '@assets/images/carot-down.svg'
import Kabob from '@assets/images/kabob.svg'
import { useTranslation } from 'react-i18next'
-import { Modal, StyleSheet } from 'react-native'
+import { StyleSheet } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import {
- useAnimatedStyle,
- useSharedValue,
- withSpring,
-} from 'react-native-reanimated'
import { FlatList } from 'react-native-gesture-handler'
import { Colors, Theme } from '../theme/theme'
import HeliumActionSheetItem, {
@@ -22,9 +16,7 @@ import { useColors } from '../theme/themeHooks'
import Text, { TextProps } from './Text'
import Box from './Box'
import TouchableOpacityBox from './TouchableOpacityBox'
-import BlurBox from './BlurBox'
-import { ReAnimatedBox } from './AnimatedBox'
-import useVisible from '../utils/useVisible'
+import HeliumBottomSheet from './HeliumBottomSheet'
type Props = BoxProps & {
data: Array
@@ -66,31 +58,11 @@ const HeliumActionSheet = ({
const [data, setData] = useState>([])
const { t } = useTranslation()
const colors = useColors()
- const offset = useSharedValue(0)
useEffect(() => {
setData(propsData)
}, [propsData])
- const animatedStyles = useAnimatedStyle(() => {
- return {
- transform: [{ translateY: offset.value + sheetHeight }],
- }
- })
-
- const animate = useCallback(
- (val: number) => {
- offset.value = withSpring(val, {
- damping: 80,
- overshootClamping: true,
- restDisplacementThreshold: 0.1,
- restSpeedThreshold: 0.1,
- stiffness: 500,
- })
- },
- [offset],
- )
-
useEffect(() => {
let nextSheetHeight =
data.length * HeliumActionSheetItemHeight + 156 + (insets?.bottom || 0)
@@ -98,8 +70,7 @@ const HeliumActionSheet = ({
nextSheetHeight = maxModalHeight
}
setSheetHeight(nextSheetHeight)
- animate(nextSheetHeight)
- }, [animate, data.length, insets?.bottom, maxModalHeight])
+ }, [data.length, insets?.bottom, maxModalHeight])
const handlePresentModalPress = useCallback(async () => {
setModalVisible(true)
@@ -109,15 +80,6 @@ const HeliumActionSheet = ({
setModalVisible(false)
}, [])
- useVisible({ onDisappear: handleClose })
-
- useEffect(() => {
- if (modalVisible) {
- offset.value = 0
- animate(-sheetHeight)
- }
- }, [animate, modalVisible, offset, sheetHeight])
-
const keyExtractor = useCallback((item) => item.value, [])
const buttonTitle = useMemo(() => {
@@ -247,55 +209,19 @@ const HeliumActionSheet = ({
return (
{displayText}
-
-
-
-
-
-
-
-
- {title}
-
-
-
-
-
-
- {footer}
-
-
-
+
+ {footer}
+
)
}
diff --git a/src/components/HeliumBottomSheet.tsx b/src/components/HeliumBottomSheet.tsx
new file mode 100644
index 000000000..c02353a04
--- /dev/null
+++ b/src/components/HeliumBottomSheet.tsx
@@ -0,0 +1,122 @@
+import React, { memo, useCallback, useEffect } from 'react'
+import { BoxProps } from '@shopify/restyle'
+import Close from '@assets/images/close.svg'
+import { Modal, StyleSheet } from 'react-native'
+import {
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+} from 'react-native-reanimated'
+import { Theme } from '../theme/theme'
+import { useColors } from '../theme/themeHooks'
+import Text from './Text'
+import Box from './Box'
+import TouchableOpacityBox from './TouchableOpacityBox'
+import BlurBox from './BlurBox'
+import { ReAnimatedBox } from './AnimatedBox'
+import useVisible from '../utils/useVisible'
+
+type Props = BoxProps & {
+ children?: React.ReactNode
+ hideHeaderBorder?: boolean
+ isVisible: boolean
+ onClose: () => void
+ sheetHeight?: number
+ title?: string
+}
+
+const HeliumBottomSheet = ({
+ children,
+ hideHeaderBorder = false,
+ isVisible,
+ onClose,
+ sheetHeight = 260,
+ title,
+}: Props) => {
+ const colors = useColors()
+ const offset = useSharedValue(0)
+
+ const animatedStyles = useAnimatedStyle(() => {
+ return {
+ transform: [{ translateY: offset.value + sheetHeight }],
+ }
+ })
+
+ const animate = useCallback(
+ (val: number) => {
+ offset.value = withSpring(val, {
+ damping: 80,
+ overshootClamping: true,
+ restDisplacementThreshold: 0.1,
+ restSpeedThreshold: 0.1,
+ stiffness: 500,
+ })
+ },
+ [offset],
+ )
+
+ const handleClose = useCallback(async () => {
+ onClose()
+ }, [onClose])
+
+ useVisible({ onDisappear: handleClose })
+
+ useEffect(() => {
+ if (isVisible) {
+ offset.value = 0
+ animate(-sheetHeight)
+ }
+ }, [animate, isVisible, offset, sheetHeight])
+
+ return (
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ divider: { borderBottomColor: '#F0F0F5' },
+})
+
+export default memo(HeliumBottomSheet)
diff --git a/src/components/InputField.tsx b/src/components/InputField.tsx
index 2abd3ea8a..ea79a27b4 100644
--- a/src/components/InputField.tsx
+++ b/src/components/InputField.tsx
@@ -12,7 +12,7 @@ import InputLock from '../assets/images/input-lock.svg'
// import TextInput from '../../../components/TextInput'
type Props = {
- label: string
+ label?: string
placeholder?: string
extra?: ReactElement
footer?: ReactElement
@@ -26,6 +26,7 @@ type Props = {
isLast?: boolean
isFirst?: boolean
testID?: string
+ optional?: boolean
}
const InputField = ({
@@ -42,6 +43,7 @@ const InputField = ({
numberOfLines,
isLast = false,
isFirst = false,
+ optional = false,
testID,
}: Props) => {
const inputRef = useRef(null)
@@ -50,61 +52,88 @@ const InputField = ({
inputRef.current?.focus()
}
- return (
-
+ let headerContent
+ if (label || extra) {
+ headerContent = (
+
+ {label ? label.toUpperCase() : null}
+ {locked && }
+
+ {extra !== undefined && extra}
+
+ )
+ }
+
+ let optionalLabel
+ if (optional) {
+ optionalLabel = (
+
+ Optional
+
+ )
+ }
+
+ return (
+ <>
+ {optionalLabel}
+
-
- {label.toUpperCase()}
- {locked && }
-
- {extra !== undefined && extra}
+ {headerContent}
+
+
+
+ {footer !== undefined && footer}
-
-
-
- {footer !== undefined && footer}
-
-
+
+ >
)
}
diff --git a/src/components/LockedHeader.tsx b/src/components/LockedHeader.tsx
index 82ba54334..450ff107a 100644
--- a/src/components/LockedHeader.tsx
+++ b/src/components/LockedHeader.tsx
@@ -1,21 +1,26 @@
import React from 'react'
-import { useTranslation } from 'react-i18next'
import Box from './Box'
+import { Colors } from '../theme/theme'
import Close from '../assets/images/close.svg'
import TouchableOpacityBox from './TouchableOpacityBox'
import Text from './Text'
type Props = {
+ backgroundColor: Colors
onClosePress: () => void
allowClose?: boolean
+ text: string
}
-const SendLockedHeader = ({ onClosePress, allowClose = true }: Props) => {
- const { t } = useTranslation()
-
+const SendLockedHeader = ({
+ backgroundColor,
+ onClosePress,
+ allowClose = true,
+ text,
+}: Props) => {
return (
{
fontSize={14}
letterSpacing={0.85}
>
- {t('send.qrInfo')}
+ {text}
{allowClose && (
void
+ onSwipeSuccessDelay?: number
+ title?: string
+}
+
+function SwipeButton({
+ disabled = false,
+ disabledColor = '#F59CA2',
+ enabledColor = '#FF4949',
+ onSwipeSuccess = () => {},
+ onSwipeSuccessDelay,
+ thumbColor = '#FFFFFF',
+ textColor = '#FFFFFF',
+ title,
+}: SwipeButtonProps) {
+ const { t } = useTranslation()
+ const [isLoading, setIsLoading] = useState(false)
+ const containerStyle = {
+ ...styles.container,
+ backgroundColor: disabled ? disabledColor : enabledColor,
+ }
+ const textStyle = { ...styles.text, color: textColor }
+
+ const onSuccess = () => {
+ if (onSwipeSuccessDelay) {
+ setIsLoading(true)
+ setTimeout(onSwipeSuccess, onSwipeSuccessDelay)
+ } else {
+ onSwipeSuccess()
+ }
+ }
+ return (
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 5,
+ borderRadius: 10,
+ },
+ text: {
+ fontFamily: Font.main.regular,
+ fontWeight: '500',
+ fontSize: 16,
+ },
+})
+
+export default SwipeButton
diff --git a/src/features/hotspots/root/HotspotsList.tsx b/src/features/hotspots/root/HotspotsList.tsx
index 42b0d6c60..3e8060dc1 100644
--- a/src/features/hotspots/root/HotspotsList.tsx
+++ b/src/features/hotspots/root/HotspotsList.tsx
@@ -5,6 +5,7 @@ import { useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import Search from '@assets/images/search.svg'
import Add from '@assets/images/add.svg'
+import LockIcon from '@assets/images/lockIconRed.svg'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { orderBy, sortBy, uniq } from 'lodash'
import { useAsync } from 'react-async-hook'
@@ -89,6 +90,10 @@ const HotspotsList = ({
)
const prevOrder = usePrevious(gatewaySortOrder)
+ const isDeployModeEnabled = useSelector(
+ (state: RootState) => state.app.isDeployModeEnabled,
+ )
+
const locationDeniedHandler = useCallback(() => {
setGatewaySortOrder(GatewaySort.New)
}, [])
@@ -369,6 +374,11 @@ const HotspotsList = ({
+ {isDeployModeEnabled && (
+
+
+
+ )}
diff --git a/src/features/hotspots/settings/HotspotSettings.tsx b/src/features/hotspots/settings/HotspotSettings.tsx
index fdb9ad15f..4074ecf58 100644
--- a/src/features/hotspots/settings/HotspotSettings.tsx
+++ b/src/features/hotspots/settings/HotspotSettings.tsx
@@ -93,7 +93,10 @@ const HotspotSettings = ({ hotspot }: Props) => {
const { permissionResponse, locationBlocked } = useSelector(
(state: RootState) => state.location,
)
- const { showOKCancelAlert } = useAlert()
+ const isDeployModeEnabled = useSelector(
+ (state: RootState) => state.app.isDeployModeEnabled,
+ )
+ const { showOKAlert, showOKCancelAlert } = useAlert()
const dataOnly = useMemo(() => isDataOnly(hotspot), [hotspot])
@@ -218,15 +221,22 @@ const HotspotSettings = ({ hotspot }: Props) => {
},
],
)
+ } else if (isDeployModeEnabled) {
+ showOKAlert({
+ titleKey: 'transfer.deployModeTransferDisableTitle',
+ messageKey: 'transfer.deployModeTransferDisabled',
+ })
} else {
setNextState('transfer')
}
}, [
+ isDeployModeEnabled,
activeTransfer?.buyer,
activeTransfer?.gateway,
cancelTransfer,
hasActiveTransfer,
setNextState,
+ showOKAlert,
t,
])
diff --git a/src/features/moreTab/more/DeployModeModal.tsx b/src/features/moreTab/more/DeployModeModal.tsx
new file mode 100644
index 000000000..62107c9b9
--- /dev/null
+++ b/src/features/moreTab/more/DeployModeModal.tsx
@@ -0,0 +1,129 @@
+import React, { useCallback, useState, useEffect } from 'react'
+import { BoxProps } from '@shopify/restyle'
+import { Address } from '@helium/crypto-react-native'
+import { KeyboardAvoidingView, Platform, StyleSheet } from 'react-native'
+import { useTranslation } from 'react-i18next'
+import { useSafeAreaInsets } from 'react-native-safe-area-context'
+import DeployModeIcon from '@assets/images/deployModeIcon.svg'
+import { Theme } from '../../../theme/theme'
+import appSlice from '../../../store/user/appSlice'
+import { useAppDispatch } from '../../../store/store'
+import Text from '../../../components/Text'
+import Box from '../../../components/Box'
+import Bullet from '../../../components/Bullet'
+import HeliumBottomSheet from '../../../components/HeliumBottomSheet'
+import InputField from '../../../components/InputField'
+import { useColors } from '../../../theme/themeHooks'
+import SwipeButton from '../../../components/SwipeButton'
+
+type Props = BoxProps & {
+ isVisible: boolean
+ onClose?: () => void
+}
+
+const DeployModeModal = ({ isVisible, onClose = () => {} }: Props) => {
+ const { t } = useTranslation()
+ const dispatch = useAppDispatch()
+ const insets = useSafeAreaInsets()
+ const colors = useColors()
+ const [sendAddress, setSendAddress] = useState('')
+
+ const sheetHeight = 700 + (insets?.bottom || 0)
+ const enableDeployMode = useCallback(() => {
+ dispatch(appSlice.actions.enableDeployMode(true))
+ if (sendAddress) {
+ dispatch(appSlice.actions.setPermanentPaymentAddress(sendAddress))
+ }
+ onClose()
+ }, [dispatch, sendAddress, onClose])
+
+ useEffect(() => {
+ if (!isVisible) setSendAddress('')
+ }, [isVisible])
+
+ // Only disable "Submit" if an address is provided but is invalid
+ const isValid = sendAddress ? Address.isValid(sendAddress) : true
+ return (
+
+
+
+
+ {t('more.sections.security.deployMode.title')}
+
+
+ {t('more.sections.security.deployMode.subtitle')}
+
+
+ {t('more.sections.security.deployMode.inDeployMode')}
+
+
+
+
+ {t('more.sections.security.deployMode.cantViewWords')}
+
+
+
+
+ {t('more.sections.security.deployMode.cantTransferHotspots')}
+
+
+
+
+ {t('more.sections.security.deployMode.canOnlySendFunds')}{' '}
+
+ {t('generic.one')}
+ {' '}
+ {t('more.sections.security.deployMode.otherAccount')}
+
+
+
+
+ {t('more.sections.security.deployMode.disableInstructions')}
+
+
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ bullet: { marginBottom: 0 },
+ cancelContainer: { backgroundColor: '#F0F0F5' },
+ cancelText: { color: '#B3B4D6' },
+ confirmText: { color: '#FFFFFF' },
+})
+
+export default DeployModeModal
diff --git a/src/features/moreTab/more/MoreListItem.tsx b/src/features/moreTab/more/MoreListItem.tsx
index aadbe2ca0..654ee4f3f 100644
--- a/src/features/moreTab/more/MoreListItem.tsx
+++ b/src/features/moreTab/more/MoreListItem.tsx
@@ -7,6 +7,7 @@ import CarotRight from '../../../assets/images/carot-right.svg'
import LinkImg from '../../../assets/images/link.svg'
import HeliumActionSheet from '../../../components/HeliumActionSheet'
import { HeliumActionSheetItemType } from '../../../components/HeliumActionSheetItem'
+import { Colors } from '../../../theme/theme'
export type SelectProps = {
onDonePress?: () => void
@@ -19,13 +20,25 @@ export type MoreListItemType = {
destructive?: boolean
onPress?: () => void
onToggle?: (value: boolean) => void
+ renderModal?: () => void
value?: boolean | string | number
select?: SelectProps
openUrl?: string
+ disabled?: boolean
}
const MoreListItem = ({
- item: { title, value, destructive, onToggle, onPress, select, openUrl },
+ item: {
+ title,
+ value,
+ destructive,
+ onToggle,
+ onPress,
+ renderModal,
+ select,
+ openUrl,
+ disabled,
+ },
isTop = false,
isBottom = false,
}: {
@@ -60,6 +73,11 @@ const MoreListItem = ({
[],
)
+ let textColor: Colors = 'primaryText'
+ if (destructive && !disabled) textColor = 'redMain'
+ if (destructive && disabled) textColor = 'redMedium'
+ if (!destructive && disabled) textColor = 'disabled'
+
return (
-
+ {renderModal && renderModal()}
+
{title}
{!onToggle && !select && onPress && (
@@ -89,6 +108,7 @@ const MoreListItem = ({
onValueChange={onToggle}
trackColor={trackColor}
thumbColor={colors.white}
+ disabled={disabled}
/>
)}
{select && (
diff --git a/src/features/moreTab/more/MoreScreen.tsx b/src/features/moreTab/more/MoreScreen.tsx
index 7a4ca1b93..5499a52b0 100644
--- a/src/features/moreTab/more/MoreScreen.tsx
+++ b/src/features/moreTab/more/MoreScreen.tsx
@@ -1,4 +1,11 @@
-import React, { memo, ReactText, useCallback, useEffect, useMemo } from 'react'
+import React, {
+ memo,
+ ReactText,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, SectionList } from 'react-native'
import { useSelector } from 'react-redux'
@@ -31,6 +38,7 @@ import Account from '../../../assets/images/account.svg'
import Box from '../../../components/Box'
import DiscordItem from './DiscordItem'
import AppInfoItem from './AppInfoItem'
+import DeployModeModal from './DeployModeModal'
import activitySlice from '../../../store/activity/activitySlice'
import hotspotsSlice from '../../../store/hotspots/hotspotsSlice'
import { useLanguageContext } from '../../../providers/LanguageProvider'
@@ -58,6 +66,10 @@ const MoreScreen = () => {
const { changeLanguage, language } = useLanguageContext()
const navigation = useNavigation()
const spacing = useSpacing()
+ const [
+ showingDeployModeConfirmation,
+ setShowingDeployModeConfirmation,
+ ] = useState(false)
useEffect(
() =>
@@ -259,6 +271,21 @@ const MoreScreen = () => {
{
title: t('more.sections.security.revealWords'),
onPress: handleRevealWords,
+ disabled: app.isDeployModeEnabled,
+ },
+ {
+ title: t('more.sections.security.deployMode.enableButton'),
+ value: app.isDeployModeEnabled,
+ onToggle: () => {
+ setShowingDeployModeConfirmation(true)
+ },
+ renderModal: () => (
+ setShowingDeployModeConfirmation(false)}
+ />
+ ),
+ disabled: app.isDeployModeEnabled,
},
]
return [
@@ -338,6 +365,9 @@ const MoreScreen = () => {
app.isHapticDisabled,
app.authInterval,
app.isPinRequiredForPayment,
+ app.isDeployModeEnabled,
+ showingDeployModeConfirmation,
+ setShowingDeployModeConfirmation,
showHiddenHotspots,
handleRevealWords,
language,
diff --git a/src/features/wallet/root/WalletHeader.tsx b/src/features/wallet/root/WalletHeader.tsx
index 9f5b73a72..c6a3d225c 100644
--- a/src/features/wallet/root/WalletHeader.tsx
+++ b/src/features/wallet/root/WalletHeader.tsx
@@ -1,13 +1,16 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { memo } from 'react'
import { useTranslation } from 'react-i18next'
+import { useSelector } from 'react-redux'
import Qr from '@assets/images/qr.svg'
+import LockIcon from '@assets/images/lockIconRed.svg'
import { BoxProps } from '@shopify/restyle'
import { Insets } from 'react-native'
import Box from '../../../components/Box'
import Text from '../../../components/Text'
import TouchableOpacityBox from '../../../components/TouchableOpacityBox'
import { Theme } from '../../../theme/theme'
+import { RootState } from '../../../store/rootReducer'
type Props = BoxProps & {
handleScanPressed: () => void
@@ -17,6 +20,14 @@ type Props = BoxProps & {
const hitSlop = { top: 12, bottom: 12, left: 24, right: 24 } as Insets
const WalletHeader = ({ handleScanPressed, hideTitle, ...boxProps }: Props) => {
const { t } = useTranslation()
+ const isDeployModeEnabled = useSelector(
+ (state: RootState) => state.app.isDeployModeEnabled,
+ )
+ const walletTitleIcon = isDeployModeEnabled ? (
+
+
+
+ ) : null
return (
{
{...boxProps}
>
{!hideTitle && (
-
- {t('wallet.title')}
-
+ <>
+
+ {t('wallet.title')}
+
+ {walletTitleIcon}
+ >
)}
diff --git a/src/features/wallet/send/SendDetailsForm.tsx b/src/features/wallet/send/SendDetailsForm.tsx
index 4a9901f99..4ca9717a2 100644
--- a/src/features/wallet/send/SendDetailsForm.tsx
+++ b/src/features/wallet/send/SendDetailsForm.tsx
@@ -37,6 +37,7 @@ type Props = {
account?: Account
fee: Balance
isLocked: boolean
+ isLockedAddress: boolean
isSeller?: boolean
index: number
lastReportedActivity?: string
@@ -53,6 +54,7 @@ const SendDetailsForm = ({
account,
fee,
isLocked,
+ isLockedAddress,
isSeller,
lastReportedActivity,
onScanPress,
@@ -238,35 +240,39 @@ const SendDetailsForm = ({
const renderPaymentForm = () => (
<>
-
- }
- footer={
-
-
- {isHotspotAddress && (
-
- {t('send.not_valid_address')}
-
- )}
-
- }
- />
+ {isLockedAddress ? (
+
+ ) : (
+
+ }
+ footer={
+
+
+ {isHotspotAddress && (
+
+ {t('send.not_valid_address')}
+
+ )}
+
+ }
+ />
+ )}
(
<>
-
-
-
- ) : (
-
-
-
- )
- }
- />
+ {isLockedAddress ? (
+
+ ) : (
+
+
+
+ ) : (
+
+
+
+ )
+ }
+ />
+ )}
hasSufficientBalance?: boolean
hasValidActivity?: boolean
+ isDisabled: boolean
isLocked: boolean
+ isLockedAddress: boolean
isSeller?: boolean
isValid: boolean
lastReportedActivity?: string
@@ -31,6 +34,7 @@ type Props = {
type: AppLinkCategoryType
unlockForm: () => void
updateSendDetails: (detailsId: string, updates: SendDetailsUpdate) => void
+ warning?: string
}
const SendForm = ({
@@ -38,7 +42,9 @@ const SendForm = ({
fee,
hasSufficientBalance,
hasValidActivity,
+ isDisabled,
isLocked,
+ isLockedAddress,
isSeller,
isValid,
lastReportedActivity,
@@ -50,6 +56,7 @@ const SendForm = ({
type,
unlockForm,
updateSendDetails,
+ warning,
}: Props) => {
const { t } = useTranslation()
const [sendDisabled, setSendDisabled] = useState(false)
@@ -80,8 +87,10 @@ const SendForm = ({
{isLocked && (
)}
{sendDetails.map((details, index) => (
@@ -91,6 +100,7 @@ const SendForm = ({
account={account}
fee={fee}
isLocked={isLocked}
+ isLockedAddress={isLockedAddress}
isSeller={isSeller}
setSendDisabled={setSendDisabled}
lastReportedActivity={lastReportedActivity}
@@ -130,17 +140,31 @@ const SendForm = ({
disabled={!isValid || sendDisabled}
/>
{shouldShowFee && }
+ {warning && }
)
}
const FeeFooter = ({ fee }: { fee: Balance }) => {
const { t } = useTranslation()
+ const feeText = `+${fee.toString(8, {
+ decimalSeparator,
+ groupSeparator,
+ })} ${t('generic.fee').toUpperCase()}`
+ return
+}
+
+const NoticeFooter = ({
+ text,
+ color = 'grayText',
+}: {
+ text: string
+ color?: Colors
+}) => {
return (
-
- +{fee.toString(8, { decimalSeparator, groupSeparator })}{' '}
- {t('generic.fee').toUpperCase()}
+
+ {text}
)
diff --git a/src/features/wallet/send/SendScreen.tsx b/src/features/wallet/send/SendScreen.tsx
index 8aef3c7a4..eb4c4364b 100644
--- a/src/features/wallet/send/SendScreen.tsx
+++ b/src/features/wallet/send/SendScreen.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useMemo } from 'react'
import { RouteProp, useNavigation } from '@react-navigation/native'
import { useSelector } from 'react-redux'
+import { useTranslation } from 'react-i18next'
import Box from '../../../components/Box'
import { SendStackParamList } from './sendTypes'
import SendView from './SendView'
@@ -14,15 +15,26 @@ type Props = {
}
const SendScreen = ({ route }: Props) => {
+ const { t } = useTranslation()
const rootNavigation = useNavigation()
const scanResult = route?.params?.scanResult
- const type = route?.params?.type
const hotspotAddress = route?.params?.hotspotAddress
const isSeller = route?.params?.isSeller
const isPinVerified = route?.params?.pinVerified
const isPinRequiredForPayment = useSelector(
(state: RootState) => state.app.isPinRequiredForPayment,
)
+ const isDeployModeEnabled = useSelector(
+ (state: RootState) => state.app.isDeployModeEnabled,
+ )
+ const permanentPaymentAddress = useSelector(
+ (state: RootState) => state.app.permanentPaymentAddress,
+ )
+ // If "Deploy Mode" is enabled, only allow payment transactions
+ const type = isDeployModeEnabled ? 'payment' : route?.params?.type
+ // If "Deploy Mode" is enabled without a permanent payment address, disable all payments
+ const isDeployModePaymentsDisabled =
+ isDeployModeEnabled && !permanentPaymentAddress
useEffect(() => {
// Check if pin is required, show lock screen if so
@@ -57,8 +69,15 @@ const SendScreen = ({ route }: Props) => {
scanResult={scanResult}
sendType={type}
hotspotAddress={hotspotAddress}
+ isDisabled={isDeployModePaymentsDisabled}
isSeller={isSeller}
canSubmit={canSubmit}
+ lockedPaymentAddress={permanentPaymentAddress}
+ warning={
+ isDeployModePaymentsDisabled
+ ? t('send.deployModePaymentsDisabled')
+ : undefined
+ }
/>
>
diff --git a/src/features/wallet/send/SendView.tsx b/src/features/wallet/send/SendView.tsx
index 5b41c7580..b15ac4113 100644
--- a/src/features/wallet/send/SendView.tsx
+++ b/src/features/wallet/send/SendView.tsx
@@ -69,16 +69,22 @@ type Props = {
scanResult?: AppLink
sendType?: AppLinkCategoryType
hotspotAddress?: string
+ isDisabled: boolean
isSeller?: boolean
canSubmit?: boolean
+ lockedPaymentAddress?: string
+ warning?: string
}
const SendView = ({
scanResult,
sendType,
hotspotAddress,
+ isDisabled,
isSeller,
canSubmit = true,
+ lockedPaymentAddress,
+ warning,
}: Props) => {
const tabNavigation = useNavigation()
const sendNavigation = useNavigation()
@@ -93,7 +99,7 @@ const SendView = ({
(state: RootState) => state.heliumData.currentOraclePrice,
)
const [type, setType] = useState(sendType || 'payment')
- const [isLocked, setIsLocked] = useState(false)
+ const [isLocked, setIsLocked] = useState(isDisabled)
const [isValid, setIsValid] = useState(false)
const [hasSufficientBalance, setHasSufficientBalance] = useState(false)
const [transferData, setTransferData] = useState()
@@ -116,7 +122,7 @@ const SendView = ({
const [sendDetails, setSendDetails] = useState>([
{
id: '0',
- address: '',
+ address: lockedPaymentAddress || '',
addressAlias: '',
addressLoading: false,
amount: '',
@@ -552,7 +558,9 @@ const SendView = ({
fee={fee}
hasSufficientBalance={hasSufficientBalance}
hasValidActivity={hasValidActivity}
+ isDisabled={isDisabled}
isLocked={isLocked}
+ isLockedAddress={!!lockedPaymentAddress}
isSeller={isSeller}
isValid={isValid}
lastReportedActivity={lastReportedActivity}
@@ -564,6 +572,7 @@ const SendView = ({
type={type}
unlockForm={unlockForm}
updateSendDetails={updateSendDetails}
+ warning={warning}
/>
{isSeller && (
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 2d81d23c9..0630185c9 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -94,6 +94,7 @@ export default {
generic: {
clear: 'Clear',
done: 'Done',
+ disabled: 'Disabled',
readMore: 'Read More',
witness: 'Witness',
understand: 'I understand',
@@ -148,6 +149,8 @@ export default {
meters: '{{distance}}m',
kilometers: '{{distance}}km',
owner: 'Owner',
+ one: 'one',
+ swipe_to_confirm: 'Swipe to Confirm',
},
hotspot_setup: {
selection: {
@@ -509,6 +512,7 @@ export default {
},
qrInfo: 'QR INFO',
error: 'There was an error submitting this transaction. Please try again.',
+ deployModePaymentsDisabled: 'Payments are disabled in Deploy Mode',
hotspot_label: 'Hotspot',
last_activity: 'LAST REPORTED ACTIVITY: {{activity}}',
label_error: 'You do not have enough HNT in your account.',
@@ -550,6 +554,20 @@ export default {
after_4_hr: 'After 4 hours',
},
revealWords: 'Reveal Words',
+ deployMode: {
+ title: 'Deploy Mode',
+ subtitle:
+ 'This mode adds extra protection to your wallet, restricting some app features.',
+ inDeployMode: 'In Deploy Mode:',
+ cantViewWords: "Can't view your 12 secure words",
+ cantTransferHotspots: "Can't transfer Hotspots from this account",
+ canOnlySendFunds: 'Can only send funds to',
+ otherAccount: 'other specified account',
+ enableButton: 'Enable Deploy Mode',
+ disableInstructions:
+ 'In order to disable this feature, you will have to log out. Remember to write down all 12 words now.',
+ addressLabel: 'Allowed Account Address...',
+ },
},
learn: {
title: 'Learn',
@@ -994,6 +1012,9 @@ export default {
fine_print:
'Hotspot will transfer once the buyer accepts and completes the transaction.',
notification_button: 'View Transaction',
+ deployModeTransferDisableTitle: 'Transfer Hotspot Disabled',
+ deployModeTransferDisabled:
+ 'Transfer Hotspot is disabled while in Deploy Mode.',
cancel: {
button_title: 'Transfer Pending. Tap to Cancel.',
failed_alert_title: 'Unable to Cancel Transfer',
diff --git a/src/locales/ja.ts b/src/locales/ja.ts
index fde9446db..97bf1b2f5 100644
--- a/src/locales/ja.ts
+++ b/src/locales/ja.ts
@@ -94,6 +94,7 @@ export default {
generic: {
clear: 'クリア',
done: '完了',
+ disabled: '無効',
understand: '理解しました',
blocks: 'ブロック',
active: 'アクティブ',
@@ -140,6 +141,8 @@ export default {
minutes_plural: '{{count}}分',
seconds: '{{count}}秒',
seconds_plural: '{{count}}秒',
+ one: '一',
+ swipe_to_confirm: 'スワイプして確認',
},
hotspot_setup: {
selection: {
@@ -479,6 +482,7 @@ export default {
qrInfo: 'QR情報',
error:
'このトランザクションの申請中にエラーが発生しました。もう一度実行してください。',
+ deployModePaymentsDisabled: 'デプロイモードでは支払いが無効になります',
hotspot_label: 'Hotspot',
last_activity: '最後に報告されたアクティビティ:{{activity}}',
label_error: 'アカウントに十分なHNTがありません。',
@@ -520,6 +524,21 @@ export default {
after_4_hr: '4時間後',
},
revealWords: '単語を表示',
+ deployMode: {
+ title: 'デプロイモード',
+ subtitle:
+ 'このモードはウォレットに追加の保護を追加し、一部のアプリ機能を制限します。',
+ inDeployMode: 'デプロイモードの場合:',
+ cantViewWords: 'あなたの12の安全な言葉を見ることができません',
+ cantTransferHotspots:
+ 'このアカウントからホットスポットを転送できません',
+ canOnlySendFunds: 'にのみ資金を送ることができます',
+ otherAccount: '他の指定されたアカウント',
+ enableButton: 'デプロイモードを有効にする',
+ disableInstructions:
+ 'この機能を無効にするには、ログアウトする必要があります。 今すぐ12語すべてを書き留めることを忘れないでください。',
+ addressLabel: '許可されたアカウントアドレス...',
+ },
},
learn: {
title: '詳細',
@@ -820,6 +839,9 @@ export default {
fine_print:
'購入者がトランザクションを承諾して完了すると、Hotspotでデータが転送されます。',
notification_button: 'トランザクションを表示',
+ deployModeTransferDisableTitle: 'ホットスポットの転送が無効',
+ deployModeTransferDisabled:
+ '展開モードでは、転送ホットスポットは無効になります。',
cancel: {
button_title: '転送を保留しています。タップしてキャンセルします。',
failed_alert_title: '転送をキャンセルできません',
diff --git a/src/locales/ko.ts b/src/locales/ko.ts
index b55bad902..5b2372b05 100644
--- a/src/locales/ko.ts
+++ b/src/locales/ko.ts
@@ -94,6 +94,7 @@ export default {
generic: {
clear: '지우기',
done: '완료',
+ disabled: '장애가있는',
understand: '내용을 이해함',
blocks: '블록',
active: '활성화',
@@ -140,6 +141,8 @@ export default {
minutes_plural: '{{count}} 분',
seconds: '{{count}} 초',
seconds_plural: '{{count}} 초',
+ one: '하나',
+ swipe_to_confirm: '스와이프하여 확인',
},
hotspot_setup: {
selection: {
@@ -463,6 +466,7 @@ export default {
},
qrInfo: 'QR 정보',
error: '이 트랜잭션을 제출하는 중에 오류가 발생했습니다. 다시 시도하세요.',
+ deployModePaymentsDisabled: '배포 모드에서는 결제가 비활성화됩니다.',
hotspot_label: 'Hotspot',
last_activity: '마지막으로 보고된 활동: {{activity}}',
label_error: '계정에 충분한 HNT가 없습니다.',
@@ -504,6 +508,20 @@ export default {
after_4_hr: '4시간 후',
},
revealWords: '단어 공개',
+ deployMode: {
+ title: '배포 모드',
+ subtitle:
+ '이 모드는 지갑에 추가 보호 기능을 추가하여 일부 앱 기능을 제한합니다.',
+ inDeployMode: '배포 모드에서:',
+ cantViewWords: '12개의 보안 단어를 볼 수 없습니다',
+ cantTransferHotspots: '이 계정에서 핫스팟을 전송할 수 없습니다',
+ canOnlySendFunds: '송금만 가능',
+ otherAccount: '기타 지정된 계정',
+ enableButton: '배포 모드 활성화',
+ disableInstructions:
+ '이 기능을 비활성화하려면 로그아웃해야 합니다. 지금 12개의 단어를 모두 적어두는 것을 잊지 마십시오.',
+ addressLabel: '허용된 계정 주소...',
+ },
},
learn: {
title: '알아보기',
@@ -801,6 +819,8 @@ export default {
'이 전송은 더 이상 활성화되지 않습니다. 판매자에게 자세한 내용을 문의하세요.',
fine_print: '구매자가 트랜잭션을 수락하고 완료하면 Hotspot이 전송됩니다.',
notification_button: '트랜잭션 보기',
+ deployModeTransferDisableTitle: '핫스팟 전송 비활성화됨',
+ deployModeTransferDisabled: '배포 모드에서는 핫스팟 전송이 비활성화됩니다.',
cancel: {
button_title: '전송 보류. 취소하려면 탭하세요.',
failed_alert_title: '전송을 취소할 수 없음',
diff --git a/src/locales/zh.ts b/src/locales/zh.ts
index 87ac6f680..4e2030010 100644
--- a/src/locales/zh.ts
+++ b/src/locales/zh.ts
@@ -92,6 +92,7 @@ export default {
generic: {
clear: '清除',
done: '已完成',
+ disabled: '已禁用',
understand: '我知道了',
blocks: '区块',
active: '活跃',
@@ -137,6 +138,8 @@ export default {
minutes_plural: '{{count}} 分钟',
seconds: '{{count}} 秒',
seconds_plural: '{{count}} 秒',
+ one: '一',
+ swipe_to_confirm: '滑动确认',
},
hotspot_setup: {
selection: {
@@ -435,6 +438,7 @@ export default {
},
qrInfo: 'QR 信息',
error: '提交此交易时出错。请重试。',
+ deployModePaymentsDisabled: '在部署模式下付款被禁用',
hotspot_label: 'Hotspot',
last_activity: '上次报告的活动: {{activity}}',
label_error: '您的帐户 HNT 余额不足。',
@@ -474,6 +478,19 @@ export default {
after_4_hr: '4 小时后',
},
revealWords: '显示助记词',
+ deployMode: {
+ title: '部署模式',
+ subtitle: '此模式为您的钱包增加了额外保护,限制了某些应用程序功能。',
+ inDeployMode: '在部署模式下:',
+ cantViewWords: '无法查看您的 12 个安全词',
+ cantTransferHotspots: '无法从此帐户转移热点',
+ canOnlySendFunds: '只能将资金发送至',
+ otherAccount: '其他指定帐户',
+ enableButton: '启用部署模式',
+ disableInstructions:
+ '要禁用此功能,您必须注销。 记住现在写下所有 12 个单词。',
+ addressLabel: '允许的帐户地址...',
+ },
},
learn: {
title: '学习',
@@ -765,6 +782,8 @@ export default {
canceled_alert_body: '此转让不再处于活动状态。请联系卖家了解更多信息。',
fine_print: '一旦买家接受并完成交易,Hotspot 即被转让。',
notification_button: '查看交易',
+ deployModeTransferDisableTitle: '传输热点已禁用',
+ deployModeTransferDisabled: '在部署模式下禁用传输热点。',
cancel: {
button_title: '转让待处理。轻触以取消。',
failed_alert_title: '无法取消转让',
diff --git a/src/store/user/appSlice.ts b/src/store/user/appSlice.ts
index edc9a45a8..81af8e325 100644
--- a/src/store/user/appSlice.ts
+++ b/src/store/user/appSlice.ts
@@ -12,6 +12,8 @@ import { Intervals } from '../../features/moreTab/more/useAuthIntervals'
export type AppState = {
isBackedUp: boolean
isHapticDisabled: boolean
+ isDeployModeEnabled: boolean
+ permanentPaymentAddress: string
isSettingUpHotspot: boolean
isRestored: boolean
isPinRequired: boolean
@@ -24,6 +26,8 @@ export type AppState = {
const initialState: AppState = {
isBackedUp: false,
isHapticDisabled: false,
+ isDeployModeEnabled: false,
+ permanentPaymentAddress: '',
isSettingUpHotspot: false,
isRestored: false,
isPinRequired: false,
@@ -38,6 +42,8 @@ type Restore = {
isBackedUp: boolean
isPinRequired: boolean
isPinRequiredForPayment: boolean
+ isDeployModeEnabled: boolean
+ permanentPaymentAddress: string
authInterval: number
isLocked: boolean
isHapticDisabled: boolean
@@ -53,6 +59,8 @@ export const restoreAppSettings = createAsyncThunk(
authInterval,
isHapticDisabled,
address,
+ isDeployModeEnabled,
+ permanentPaymentAddress,
] = await Promise.all([
getSecureItem('accountBackedUp'),
getSecureItem('requirePin'),
@@ -60,6 +68,8 @@ export const restoreAppSettings = createAsyncThunk(
getSecureItem('authInterval'),
getSecureItem('hapticDisabled'),
getSecureItem('address'),
+ getSecureItem('deployModeEnabled'),
+ getSecureItem('permanentPaymentAddress'),
])
if (isBackedUp && address) {
@@ -75,6 +85,8 @@ export const restoreAppSettings = createAsyncThunk(
: Intervals.IMMEDIATELY,
isLocked: isPinRequired,
isHapticDisabled,
+ isDeployModeEnabled,
+ permanentPaymentAddress,
} as Restore
},
)
@@ -102,6 +114,14 @@ const appSlice = createSlice({
state.isPinRequiredForPayment = action.payload
setSecureItem('requirePinForPayment', action.payload)
},
+ enableDeployMode: (state, action: PayloadAction) => {
+ state.isDeployModeEnabled = action.payload
+ setSecureItem('deployModeEnabled', action.payload)
+ },
+ setPermanentPaymentAddress: (state, action: PayloadAction) => {
+ state.permanentPaymentAddress = action.payload
+ setSecureItem('permanentPaymentAddress', action.payload)
+ },
updateHapticEnabled: (state, action: PayloadAction) => {
state.isHapticDisabled = action.payload
setSecureItem('hapticDisabled', action.payload)
diff --git a/src/utils/secureAccount.ts b/src/utils/secureAccount.ts
index bdfe69b4a..d611e5a1e 100644
--- a/src/utils/secureAccount.ts
+++ b/src/utils/secureAccount.ts
@@ -13,6 +13,7 @@ const stringKeys = [
'authInterval',
'walletApiToken',
'language',
+ 'permanentPaymentAddress',
] as const
type StringKey = typeof stringKeys[number]
@@ -23,6 +24,7 @@ const boolKeys = [
'requirePinForPayment',
'hapticDisabled',
'convertHntToCurrency',
+ 'deployModeEnabled',
'fleetModeEnabled',
'hasFleetModeAutoEnabled',
] as const
diff --git a/yarn.lock b/yarn.lock
index 540afcefc..1593b38c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11810,6 +11810,11 @@ rn-host-detect@1.2.0:
resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.2.0.tgz#8b0396fc05631ec60c1cb8789e5070cdb04d0da0"
integrity sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A==
+rn-swipe-button@^1.3.6:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/rn-swipe-button/-/rn-swipe-button-1.3.6.tgz#02e6ce0664fc94900851871a532fa0e2fbc7fd35"
+ integrity sha512-H4//cnGE7r4S3lTc1DbLR5gIZuibkk+Svy3g3LtRhyfd/vr1qfnQ70kMhCB/iIHD06yYOWgRdyqfyCb7jTosGg==
+
rsvp@^4.8.4:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"