diff --git a/src/app/services/HotspotService/pages/ClaimTokensPage.tsx b/src/app/services/HotspotService/pages/ClaimTokensPage.tsx
index 15a280b3..65812c32 100644
--- a/src/app/services/HotspotService/pages/ClaimTokensPage.tsx
+++ b/src/app/services/HotspotService/pages/ClaimTokensPage.tsx
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import { Image, RefreshControl } from 'react-native'
import MobileIcon from '@assets/svgs/mobileIconNew.svg'
import IotIcon from '@assets/svgs/iotIconNew.svg'
-import HntIcon from '@assets/svgs/hnt.svg'
+import HntIcon from '@assets/svgs/hntIconNew.svg'
import TouchableContainer from '@components/TouchableContainer'
import BalanceText from '@components/BalanceText'
import useHotspots from '@hooks/useHotspots'
@@ -30,6 +30,7 @@ import { ReAnimatedBox } from '@components/AnimatedBox'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { RootState } from '@store/rootReducer'
import { useSelector } from 'react-redux'
+import { useBottomSpacing } from '@hooks/useBottomSpacing'
const ClaimTokensPage = () => {
const { t } = useTranslation()
@@ -39,6 +40,7 @@ const ClaimTokensPage = () => {
const { showModal } = useModal()
const solBalance = useBN(useSolOwnedAmount(wallet).amount)
const colors = useColors()
+ const bottomSpacing = useBottomSpacing()
const hasEnoughSol = useMemo(() => {
return (solBalance || new BN(0)).gt(new BN(MIN_BALANCE_THRESHOLD))
@@ -57,8 +59,9 @@ const ClaimTokensPage = () => {
const contentContainerStyle = useMemo(() => {
return {
padding: spacing['2xl'],
+ paddingBottom: bottomSpacing,
}
- }, [spacing])
+ }, [spacing, bottomSpacing])
const totalPendingIot = useMemo(() => {
if (!pendingIotRewards) return 0
@@ -67,7 +70,7 @@ const ClaimTokensPage = () => {
const totalPendingHnt = useMemo(() => {
if (!pendingHntRewards) return 0
- return toNumber(pendingHntRewards, 6)
+ return toNumber(pendingHntRewards, 8)
}, [pendingHntRewards])
const totalPendingMobile = useMemo(() => {
@@ -164,12 +167,31 @@ const ClaimTokensPage = () => {
{t('ClaimTokensPage.subtitle')}
+
+
+
+
+
+
+ HNT
+
+
+
+
{totalPendingMobile > 0 && (
{
)}
-
-
-
-
-
-
- HNT
-
-
-
-
{
- const { hotspotsWithMeta } = useHotspots()
+ const { hotspotsWithMeta, loading } = useHotspots()
const spacing = useSpacing()
const [userLocation, setUserLocation] = useState()
const [showTotalHotspotPuck, setShowTotalHotspotPuck] = useState(false)
@@ -67,12 +68,21 @@ const ExplorerPage = () => {
)
}, [showTotalHotspotPuck, hotspotsWithMeta, spacing])
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
return (
diff --git a/src/components/SegmentedControl.tsx b/src/components/SegmentedControl.tsx
index 4fdd8bb0..c3cb7b3f 100644
--- a/src/components/SegmentedControl.tsx
+++ b/src/components/SegmentedControl.tsx
@@ -13,6 +13,7 @@ import { GestureResponderEvent, LayoutChangeEvent } from 'react-native'
import { SvgProps } from 'react-native-svg'
import { useColors } from '@config/theme/themeHooks'
import { useAnimatedStyle, withTiming } from 'react-native-reanimated'
+import useHaptic from '@hooks/useHaptic'
import { Theme } from '../config/theme/theme'
import { Box, ReAnimatedBox, Text } from '.'
import TouchableOpacityBox from './TouchableOpacityBox'
@@ -119,6 +120,7 @@ const SegmentedControl = forwardRef(
) => {
const [selectedIndex, setSelectedIndex] = useState(0)
const [hasTouched, setHasTouched] = useState(false)
+ const { triggerImpact } = useHaptic()
useImperativeHandle(ref, () => ({ selectedIndex }))
@@ -133,11 +135,12 @@ const SegmentedControl = forwardRef(
const handleItemSelected = useCallback(
(index: number) => () => {
+ triggerImpact('light')
setHasTouched(true)
setSelectedIndex(index)
onItemSelected(index)
},
- [onItemSelected],
+ [onItemSelected, triggerImpact],
)
const leftPosition = useMemo(() => {
diff --git a/src/components/ServiceNavBar.tsx b/src/components/ServiceNavBar.tsx
index 57d05041..f73caae8 100644
--- a/src/components/ServiceNavBar.tsx
+++ b/src/components/ServiceNavBar.tsx
@@ -14,6 +14,7 @@ import Animated, {
} from 'react-native-reanimated'
import { SvgProps } from 'react-native-svg'
import { useColors, useVerticalHitSlop } from '@config/theme/themeHooks'
+import useHaptic from '@hooks/useHaptic'
import Box from './Box'
import TouchableOpacityBox, {
TouchableOpacityBoxProps,
@@ -118,6 +119,7 @@ const NavServiceNavBar = ({
}: NavServiceBarProps) => {
const hitSlop = useVerticalHitSlop('6')
const [itemRects, setItemRects] = useState>()
+ const { triggerImpact } = useHaptic()
const offset = useSharedValue(null)
@@ -132,9 +134,10 @@ const NavServiceNavBar = ({
const handlePress = useCallback(
(value: string) => () => {
+ triggerImpact('light')
onItemSelected(value)
},
- [onItemSelected],
+ [onItemSelected, triggerImpact],
)
const handleLongPress = useCallback(
diff --git a/src/components/SideDrawer.tsx b/src/components/SideDrawer.tsx
index 7001b817..e92e1430 100644
--- a/src/components/SideDrawer.tsx
+++ b/src/components/SideDrawer.tsx
@@ -6,6 +6,7 @@ import {
withTiming,
} from 'react-native-reanimated'
import { ww } from '@utils/layout'
+import useHaptic from '@hooks/useHaptic'
import TouchableOpacityBox from './TouchableOpacityBox'
import { Box, ReAnimatedBox, SafeAreaBox, Text } from '.'
import MenuButton from './MenuButton'
@@ -18,6 +19,7 @@ type SideDrawerProps = {
const SideDrawer = ({ isExpanded, onRoute, onClose }: SideDrawerProps) => {
const { t } = useTranslation()
+ const { triggerImpact } = useHaptic()
const routes: { title: string; value: string }[] = useMemo(
() =>
@@ -42,9 +44,10 @@ const SideDrawer = ({ isExpanded, onRoute, onClose }: SideDrawerProps) => {
const onRoutePressed = useCallback(
(route: string) => () => {
+ triggerImpact('light')
onRoute(route)
},
- [onRoute],
+ [onRoute, triggerImpact],
)
return (
diff --git a/src/config/locales/en.ts b/src/config/locales/en.ts
index a3f330f3..635ae0f1 100644
--- a/src/config/locales/en.ts
+++ b/src/config/locales/en.ts
@@ -1753,4 +1753,7 @@ export default {
subtitle:
'There was an error with the app attempting to deep link. Please contact the app provider.',
},
+ UnclaimedRewardsBanner: {
+ title: 'You have rewards to claim.',
+ },
}
diff --git a/src/config/storage/AppStorageProvider.tsx b/src/config/storage/AppStorageProvider.tsx
index 01eec216..fb993355 100644
--- a/src/config/storage/AppStorageProvider.tsx
+++ b/src/config/storage/AppStorageProvider.tsx
@@ -33,9 +33,7 @@ const useAppStorageHook = () => {
)
const [currency, setCurrency] = useState('USD')
const [explorer, setExplorer] = useState(undefined)
- const [enableHaptic, setEnableHaptic] = useState(
- undefined,
- )
+ const [enableHaptic, setEnableHaptic] = useState(true)
const [locked, setLocked] = useState()
const [convertToCurrency, setConvertToCurrency] = useState(false)
const [enableTestnet, setEnableTestnet] = useState(false)
diff --git a/src/features/hotspot-onboarding/screens/iot/ConnectViaBluetoothScreen.tsx b/src/features/hotspot-onboarding/screens/iot/ConnectViaBluetoothScreen.tsx
index 93d57a84..74400ea8 100644
--- a/src/features/hotspot-onboarding/screens/iot/ConnectViaBluetoothScreen.tsx
+++ b/src/features/hotspot-onboarding/screens/iot/ConnectViaBluetoothScreen.tsx
@@ -6,7 +6,7 @@ import { useHotspotOnboarding } from '@features/hotspot-onboarding/OnboardingShe
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import RightArrow from '@assets/svgs/rightArrow.svg'
-import BluetoothIcon from '@assets/svgs/bluetooth.svg'
+import BluetoothIcon from '@assets/svgs/bluetoothOnboard.svg'
import { useColors, useSpacing } from '@config/theme/themeHooks'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { ReAnimatedBox } from '@components/AnimatedBox'
@@ -38,7 +38,7 @@ const ConnectViaBluetoothScreen = () => {
}
>
-
+
{
setNetworks(available)
})
+ useFocusEffect(
+ useCallback(() => {
+ handleRefresh()
+ }, [handleRefresh]),
+ )
+
// Refresh on network change or on load
useEffect(() => {
handleRefresh()
@@ -97,10 +104,11 @@ const WifiSettings = () => {
[carouselRef, setOnboardDetails, getOnboardingAddress],
)
- const data = useMemo(
- () => [...(configuredNetworks || []), ...(networks || [])],
- [configuredNetworks, networks],
- )
+ const data = useMemo(() => {
+ const nks = [...(configuredNetworks || []), ...(networks || [])]
+ // remove duplicates
+ return nks.filter((network, index, self) => self.indexOf(network) === index)
+ }, [configuredNetworks, networks])
const renderItem = useCallback(
({
diff --git a/src/features/hotspots/HotspotDetails.tsx b/src/features/hotspots/HotspotDetails.tsx
index b8585175..530b9e70 100644
--- a/src/features/hotspots/HotspotDetails.tsx
+++ b/src/features/hotspots/HotspotDetails.tsx
@@ -68,6 +68,7 @@ const HotspotDetails = () => {
}, [hotspot])
const { result: hotspotAddress } = useAsync(async () => {
+ if (!iotInfoAcc && !mobileInfoAcc) return undefined
let address:
| {
city: string | undefined
@@ -91,7 +92,7 @@ const HotspotDetails = () => {
if (!address) return undefined
return `${address.street}, ${address.city}, ${address.state}`
- }, [hotspot, subDao])
+ }, [iotInfoAcc?.info.location, mobileInfoAcc?.info.location])
const onConfig = useCallback(() => {
if (!hotspotAddress) return
diff --git a/src/features/hotspots/HotspotPage.tsx b/src/features/hotspots/HotspotPage.tsx
index bd8894a9..61ccf6f6 100644
--- a/src/features/hotspots/HotspotPage.tsx
+++ b/src/features/hotspots/HotspotPage.tsx
@@ -16,7 +16,7 @@ import { Location, MarkerView } from '@rnmapbox/maps'
import ImageBox from '@components/ImageBox'
import CarotRight from '@assets/svgs/carot-right.svg'
import { getDistance } from 'geolib'
-import { HNT_MINT, MOBILE_MINT, toNumber as heliumToNumber } from '@helium/spl-utils'
+import { HNT_MINT, toNumber as heliumToNumber } from '@helium/spl-utils'
import { BN } from '@coral-xyz/anchor'
import { toNumber } from 'lodash'
import MiniMap from '@components/MiniMap'
diff --git a/src/features/wallet/TokensScreen.tsx b/src/features/wallet/TokensScreen.tsx
index e47cabc6..676fd951 100644
--- a/src/features/wallet/TokensScreen.tsx
+++ b/src/features/wallet/TokensScreen.tsx
@@ -41,6 +41,7 @@ import { ServiceSheetNavigationProp } from 'src/app/services/serviceSheetTypes'
import { useSolana } from '@features/solana/SolanaProvider'
import { WalletNavigationProp } from '@services/WalletService/pages/WalletPage'
import { useBottomSpacing } from '@hooks/useBottomSpacing'
+import UnclaimedRewardsBanner from './components/UnclaimedRewardsBanner'
const TokensScreen = () => {
const widgetGroup = 'group.com.helium.mobile.wallet.widget'
@@ -203,6 +204,9 @@ const TokensScreen = () => {
+
+
+
)
}, [])
diff --git a/src/features/wallet/components/UnclaimedRewardsBanner.tsx b/src/features/wallet/components/UnclaimedRewardsBanner.tsx
new file mode 100644
index 00000000..3ca7e629
--- /dev/null
+++ b/src/features/wallet/components/UnclaimedRewardsBanner.tsx
@@ -0,0 +1,128 @@
+import Box from '@components/Box'
+import Text from '@components/Text'
+import TouchableContainer from '@components/TouchableContainer'
+import React, { useCallback, useEffect, useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import HntIcon from '@assets/svgs/hntIconNew.svg'
+import IotIcon from '@assets/svgs/iotIconNew.svg'
+import MobileIcon from '@assets/svgs/mobileIconNew.svg'
+import BalanceText from '@components/BalanceText'
+import { toNumber } from '@helium/spl-utils'
+import useHotspots from '@hooks/useHotspots'
+import { useNavigation } from '@react-navigation/native'
+import { ServiceSheetNavigationProp } from '@services/serviceSheetTypes'
+
+const UnclaimedRewardsBanner = () => {
+ const { t } = useTranslation()
+ const navigation = useNavigation()
+ const {
+ pendingIotRewards,
+ pendingHntRewards,
+ pendingMobileRewards,
+ refresh,
+ } = useHotspots()
+
+ useEffect(() => {
+ refresh()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const totalPendingIot = useMemo(() => {
+ if (!pendingIotRewards) return 0
+ return toNumber(pendingIotRewards, 6)
+ }, [pendingIotRewards])
+
+ const totalPendingHnt = useMemo(() => {
+ if (!pendingHntRewards) return 0
+ return toNumber(pendingHntRewards, 8)
+ }, [pendingHntRewards])
+
+ const totalPendingMobile = useMemo(() => {
+ if (!pendingMobileRewards) return 0
+ return toNumber(pendingMobileRewards, 6)
+ }, [pendingMobileRewards])
+
+ const goToClaimRewards = useCallback(() => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ;(navigation as any).navigate('HotspotService', {
+ screen: 'ClaimTokens',
+ })
+ }, [navigation])
+
+ if (
+ totalPendingHnt === 0 &&
+ totalPendingIot === 0 &&
+ totalPendingMobile === 0
+ )
+ return null
+
+ return (
+
+ {t('UnclaimedRewardsBanner.title')}
+
+ {totalPendingHnt > 0 && (
+
+ )}
+ {totalPendingMobile > 0 && (
+
+ )}
+ {totalPendingIot > 0 && (
+
+ )}
+
+
+ )
+}
+
+const RewardPill = ({
+ amount,
+ ticker,
+}: {
+ amount: number
+ ticker: 'HNT' | 'IOT' | 'MOBILE'
+}) => {
+ const pillBackgroundColor = useMemo(() => {
+ if (ticker === 'HNT') return 'purple.100'
+ if (ticker === 'IOT') return 'bg.success-primary'
+ if (ticker === 'MOBILE') return 'bg.brand-secondary'
+ }, [ticker])
+
+ const tickerColor = useMemo(() => {
+ if (ticker === 'HNT') return 'purple.600'
+ if (ticker === 'IOT') return 'green.600'
+ if (ticker === 'MOBILE') return 'blue.600'
+ }, [ticker])
+
+ const TokenIcon = useCallback(() => {
+ if (ticker === 'HNT') return
+ if (ticker === 'IOT') return
+ if (ticker === 'MOBILE') return
+ }, [ticker])
+
+ return (
+
+
+
+
+
+ {ticker}
+
+
+
+ )
+}
+
+export default UnclaimedRewardsBanner
diff --git a/src/hooks/useSwipe.ts b/src/hooks/useSwipe.ts
index 7edfc3f7..da092f11 100644
--- a/src/hooks/useSwipe.ts
+++ b/src/hooks/useSwipe.ts
@@ -1,4 +1,5 @@
import { Dimensions, GestureResponderEvent } from 'react-native'
+import useHaptic from './useHaptic'
const windowWidth = Dimensions.get('window').width
@@ -7,6 +8,7 @@ export function useSwipe(
onSwipeRight?: ((event: GestureResponderEvent) => void) | undefined,
rangeOffset = 4,
) {
+ const { triggerImpact } = useHaptic()
let firstTouch = 0
// set user touch start position
@@ -19,14 +21,21 @@ export function useSwipe(
// get touch position and screen size
const positionX = e.nativeEvent.pageX
const range = windowWidth / rangeOffset
-
// check if position is growing positively and has reached specified range
- if (positionX - firstTouch > range) {
- if (onSwipeRight) onSwipeRight(e)
+ const swipeRightRange = positionX - firstTouch
+ if (swipeRightRange > range) {
+ if (onSwipeRight) {
+ triggerImpact('medium')
+ onSwipeRight(e)
+ }
}
// check if position is growing negatively and has reached specified range
- else if (firstTouch - positionX > range) {
- if (onSwipeLeft) onSwipeLeft(e)
+ const swipeLeftRange = firstTouch - positionX
+ if (swipeLeftRange > range) {
+ if (onSwipeLeft) {
+ triggerImpact('medium')
+ onSwipeLeft(e)
+ }
}
}
diff --git a/src/utils/solanaUtils.ts b/src/utils/solanaUtils.ts
index 2d8733bb..9c82f12e 100644
--- a/src/utils/solanaUtils.ts
+++ b/src/utils/solanaUtils.ts
@@ -127,7 +127,13 @@ import {
} from '../types/solana'
import { WrappedConnection } from './WrappedConnection'
import { solAddressIsValid } from './accountUtils'
-import { DAO_KEY, HNT_LAZY_KEY, IOT_LAZY_KEY, MOBILE_LAZY_KEY, Mints } from './constants'
+import {
+ DAO_KEY,
+ HNT_LAZY_KEY,
+ IOT_LAZY_KEY,
+ MOBILE_LAZY_KEY,
+ Mints,
+} from './constants'
import { decimalSeparator, groupSeparator } from './i18n'
import * as Logger from './logger'
import sleep from './sleep'
@@ -1050,16 +1056,20 @@ export const getHotspotPendingRewards = async (
'b58',
true,
)
- const hntRewards = await getPendingRewards(
- program,
- HNT_LAZY_KEY,
- dao,
- entityKeys,
- 'b58',
- true,
- )
+ let hntRewards: Record = {}
- return hotspots.map((hotspot, index) => {
+ try {
+ hntRewards = await getPendingRewards(
+ program,
+ HNT_LAZY_KEY,
+ dao,
+ entityKeys,
+ 'b58',
+ true,
+ )
+ } catch {}
+
+ const hots = hotspots.map((hotspot, index) => {
const entityKey = entityKeys[index]
return {
@@ -1071,6 +1081,8 @@ export const getHotspotPendingRewards = async (
},
}
})
+
+ return hots
}
export const getHotspotRecipients = async (
@@ -1323,14 +1335,18 @@ export async function annotateWithPendingRewards(
true,
)
- const hntRewards = await getPendingRewards(
- program,
- HNT_LAZY_KEY,
- dao,
- entityKeys,
- 'b58',
- true,
- )
+ let hntRewards: Record = {}
+
+ try {
+ hntRewards = await getPendingRewards(
+ program,
+ HNT_LAZY_KEY,
+ dao,
+ entityKeys,
+ 'b58',
+ true,
+ )
+ } catch {}
const rewardRecipients = await getHotspotRecipients(provider, hotspots)
const rewardRecipientsById: {
@@ -1342,9 +1358,7 @@ export async function annotateWithPendingRewards(
}),
{},
)
- console.log('hntRewards', hntRewards)
-
- return hotspots.map((hotspot, index) => {
+ const hots = hotspots.map((hotspot, index) => {
const entityKey = entityKeys[index]
return {
@@ -1357,6 +1371,8 @@ export async function annotateWithPendingRewards(
rewardRecipients: rewardRecipientsById[hotspot.id] || {},
} as HotspotWithPendingRewards
})
+
+ return hots
}
/**