From 041af415c7f02722423c06869ba93b4a07e5c94b Mon Sep 17 00:00:00 2001 From: Chewing Glass Date: Fri, 8 Sep 2023 12:20:04 -0500 Subject: [PATCH 1/8] -Hotspot onboarding flow complete & working. Needs styling, device picker --- ios/Podfile | 6 + ios/Podfile.lock | 18 +- package.json | 3 +- src/App.tsx | 2 +- .../collectables/AssertLocationScreen.tsx | 30 +- .../collectables/CollectablesNavigator.tsx | 7 + src/features/collectables/HotspotList.tsx | 20 +- .../collectables/collectablesTypes.ts | 1 + .../hotspot-onboarding/AddGatewayBle.tsx | 242 +++++++++++ .../hotspot-onboarding/Diagnostics.tsx | 54 +++ .../hotspot-onboarding/HotspotBLENav.tsx | 49 +++ .../hotspot-onboarding/ScanHotspots.tsx | 227 ++++++++++ .../hotspot-onboarding/WifiSettings.tsx | 176 ++++++++ src/features/hotspot-onboarding/WifiSetup.tsx | 90 ++++ src/features/hotspot-onboarding/navTypes.tsx | 12 + src/hooks/useSubmitTxn.ts | 90 ++-- src/locales/en.ts | 30 ++ src/navigation/RootNavigator.tsx | 2 +- src/solana/SolanaProvider.tsx | 15 +- src/store/slices/solanaSlice.ts | 72 ---- src/utils/solanaUtils.ts | 12 + yarn.lock | 399 +++++++++++++++++- 22 files changed, 1417 insertions(+), 140 deletions(-) create mode 100644 src/features/hotspot-onboarding/AddGatewayBle.tsx create mode 100644 src/features/hotspot-onboarding/Diagnostics.tsx create mode 100644 src/features/hotspot-onboarding/HotspotBLENav.tsx create mode 100644 src/features/hotspot-onboarding/ScanHotspots.tsx create mode 100644 src/features/hotspot-onboarding/WifiSettings.tsx create mode 100644 src/features/hotspot-onboarding/WifiSetup.tsx create mode 100644 src/features/hotspot-onboarding/navTypes.tsx diff --git a/ios/Podfile b/ios/Podfile index fe272ac77..639bb19d3 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,11 +1,13 @@ require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") require_relative '../node_modules/react-native/scripts/react_native_pods' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' +require_relative '../node_modules/react-native-permissions/scripts/setup' platform :ios, '13.0' prepare_react_native_project! $RNMapboxMapsImpl = 'mapbox' + # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded # @@ -76,6 +78,10 @@ target 'HeliumWallet' do $RNMapboxMaps.pre_install(installer) end + + permissions_path = '../node_modules/react-native-permissions/ios' + setup_permissions(['BluetoothPeripheral']) + post_install do |installer| $RNMapboxMaps.post_install(installer) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b761b4cf7..ee80c499d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -59,7 +59,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.71.5) - fmt (6.2.1) - glog (0.3.5) - - helium-react-native-sdk (1.0.0): + - helium-react-native-sdk (2.1.4): - React-Core - hermes-engine (0.71.5): - hermes-engine/Pre-built (= 0.71.5) @@ -499,6 +499,8 @@ PODS: - Turf - RNOS (1.2.6): - React + - RNPermissions (3.9.0): + - React-Core - RNReactNativeSharedGroupPreferences (1.1.23): - React - RNReanimated (2.14.4): @@ -633,6 +635,7 @@ DEPENDENCIES: - RNLocalize (from `../node_modules/react-native-localize`) - "rnmapbox-maps (from `../node_modules/@rnmapbox/maps`)" - RNOS (from `../node_modules/react-native-os`) + - RNPermissions (from `../node_modules/react-native-permissions`) - RNReactNativeSharedGroupPreferences (from `../node_modules/react-native-shared-group-preferences`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) @@ -818,6 +821,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@rnmapbox/maps" RNOS: :path: "../node_modules/react-native-os" + RNPermissions: + :path: "../node_modules/react-native-permissions" RNReactNativeSharedGroupPreferences: :path: "../node_modules/react-native-shared-group-preferences" RNReanimated: @@ -861,9 +866,9 @@ SPEC CHECKSUMS: FBLazyVector: f1897022b53abf1469d6ad692ee2c69f57d967f3 FBReactNativeSpec: 627fd07f1b9d498c9fa572e76d7f1a6b1ee9a444 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 791fe035093b84822da7f0870421a25839ca7870 - helium-react-native-sdk: 32c0a7e3abc733a7f3d291013b2db31475fc6980 - hermes-engine: 7a53ccac09146018a08239c5425625fdb79a6162 + glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + helium-react-native-sdk: 816b3d02192e23ccb4d0be21780ea7c5ed46de4e + hermes-engine: 0784cadad14b011580615c496f77e0ae112eed75 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 MapboxCommon: fdf7fd31c90b7b607cd9c63e37797f023c01d860 MapboxCoreMaps: 24270c7c6b8cb71819fc2f3c549db9620ee4d019 @@ -871,7 +876,7 @@ SPEC CHECKSUMS: MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 MultiplatformBleAdapter: 5a6a897b006764392f9cef785e4360f54fb9477d OneSignalXCFramework: 81ceac017a290f23793443323090cfbe888f74ea - RCT-Folly: 85766c3226c7ec638f05ad7cb3cf6a268d6c4241 + RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: bd6045fbd511da5efe6db89eecb21e4e36bd7cbf RCTTypeSafety: c06d9f906faa69dd1c88223204c3a24767725fd8 React: b9ea33557ef1372af247f95d110fbdea114ed3b2 @@ -925,6 +930,7 @@ SPEC CHECKSUMS: RNLocalize: a64514b46a01375fdfae9349036b4dc7130333b5 rnmapbox-maps: 3553325eec9e1501942bbb28702b3a44457961d2 RNOS: 6f2f9a70895bbbfbdad7196abd952e7b01d45027 + RNPermissions: 1d1a8318f659ffd609dd7f81de8a7ecabfb52f10 RNReactNativeSharedGroupPreferences: de0121a4224c267bc7e9fb16c398f3f087c8da81 RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128 RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f @@ -939,6 +945,6 @@ SPEC CHECKSUMS: Yoga: cd7d7f509dbfac14ee7f31a6c750acb957cd5022 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 315053ef8dad0ea72bc8b3d40f59c1020ede650c +PODFILE CHECKSUM: 5b4d3c6d9c5a303c84f0b5427958ba4a25671e76 COCOAPODS: 1.11.3 diff --git a/package.json b/package.json index 3e5464d4d..7241e1e5b 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@helium/lazy-distributor-sdk": "^0.2.17", "@helium/onboarding": "4.9.0", "@helium/proto-ble": "4.0.0", - "@helium/react-native-sdk": "1.0.0", + "@helium/react-native-sdk": "^2.1.4", "@helium/spl-utils": "^0.2.17", "@helium/transactions": "4.8.1", "@helium/treasury-management-sdk": "0.1.2", @@ -165,6 +165,7 @@ "react-native-onesignal": "4.4.1", "react-native-os": "1.2.6", "react-native-pager-view": "6.1.2", + "react-native-permissions": "^3.9.0", "react-native-qrcode-svg": "6.1.2", "react-native-randombytes": "3.6.1", "react-native-reanimated": "2.14.4", diff --git a/src/App.tsx b/src/App.tsx index d49213172..0afc6512e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' import { PortalHost, PortalProvider } from '@gorhom/portal' import { AccountContext } from '@helium/account-fetch-cache-hooks' +import { OnboardingProvider } from '@helium/react-native-sdk' import { DarkTheme, NavigationContainer } from '@react-navigation/native' import MapboxGL from '@rnmapbox/maps' import { ThemeProvider } from '@shopify/restyle' @@ -21,7 +22,6 @@ import NetworkAwareStatusBar from './components/NetworkAwareStatusBar' import SplashScreen from './components/SplashScreen' import WalletConnectProvider from './features/dappLogin/WalletConnectProvider' import LockScreen from './features/lock/LockScreen' -import OnboardingProvider from './features/onboarding/OnboardingProvider' import SecurityScreen from './features/security/SecurityScreen' import useMount from './hooks/useMount' import { navigationRef } from './navigation/NavigationHelper' diff --git a/src/features/collectables/AssertLocationScreen.tsx b/src/features/collectables/AssertLocationScreen.tsx index a0ab3ce7e..3a0febe22 100644 --- a/src/features/collectables/AssertLocationScreen.tsx +++ b/src/features/collectables/AssertLocationScreen.tsx @@ -13,10 +13,13 @@ import Text from '@components/Text' import TextInput from '@components/TextInput' import { HotspotType } from '@helium/onboarding' import useAlert from '@hooks/useAlert' +import { useEntityKey } from '@hooks/useEntityKey' import { useForwardGeo } from '@hooks/useForwardGeo' +import { useIotInfo } from '@hooks/useIotInfo' +import { useMobileInfo } from '@hooks/useMobileInfo' import { useReverseGeo } from '@hooks/useReverseGeo' import useSubmitTxn from '@hooks/useSubmitTxn' -import { RouteProp, useRoute } from '@react-navigation/native' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' import MapboxGL from '@rnmapbox/maps' import turfBbox from '@turf/bbox' import { points } from '@turf/helpers' @@ -26,27 +29,27 @@ import React, { useCallback, useEffect, useMemo, - useState, useRef, + useState, } from 'react' import { useTranslation } from 'react-i18next' import { Alert, - KeyboardAvoidingView, Keyboard, + KeyboardAvoidingView, TouchableWithoutFeedback, } from 'react-native' import { Config } from 'react-native-config' import { Edge } from 'react-native-safe-area-context' import 'text-encoding-polyfill' -import { useEntityKey } from '@hooks/useEntityKey' -import { useIotInfo } from '@hooks/useIotInfo' -import { useMobileInfo } from '@hooks/useMobileInfo' import { parseH3BNLocation } from '../../utils/h3' import { removeDashAndCapitalize } from '../../utils/hotspotNftsUtils' import * as Logger from '../../utils/logger' import { MAX_MAP_ZOOM, MIN_MAP_ZOOM } from '../../utils/mapbox' -import { CollectableStackParamList } from './collectablesTypes' +import { + CollectableNavigationProp, + CollectableStackParamList, +} from './collectablesTypes' const BUTTON_HEIGHT = 65 type Route = RouteProp @@ -74,6 +77,7 @@ const AssertLocationScreen = () => { const reverseGeo = useReverseGeo(mapCenter) const forwardGeo = useForwardGeo() const { submitUpdateEntityInfo } = useSubmitTxn() + const collectNav = useNavigation() const { content: { metadata }, @@ -251,6 +255,7 @@ const AssertLocationScreen = () => { decimalGain: gain, }) setAsserting(false) + collectNav.navigate('HotspotDetailsScreen', { collectable }) } catch (error) { setAsserting(false) Logger.error(error) @@ -259,15 +264,14 @@ const AssertLocationScreen = () => { } }, [ - entityKey, mapCenter, - elevation, - gain, + entityKey, hideElevGain, - setAsserting, - setTransactionError, submitUpdateEntityInfo, - // nav, + elevation, + gain, + collectNav, + collectable, ], ) diff --git a/src/features/collectables/CollectablesNavigator.tsx b/src/features/collectables/CollectablesNavigator.tsx index 104becc5c..4c92d72f3 100644 --- a/src/features/collectables/CollectablesNavigator.tsx +++ b/src/features/collectables/CollectablesNavigator.tsx @@ -21,6 +21,7 @@ import CollectionScreen from './CollectionScreen' import NftDetailsScreen from './NftDetailsScreen' import AntennaSetupScreen from './AntennaSetupScreen' import SettingUpAntennaScreen from './SettingUpAntennaScreen' +import HotspotBLENav from '../hotspot-onboarding/HotspotBLENav' const CollectablesStack = createStackNavigator() @@ -113,6 +114,12 @@ const CollectablesStackScreen = () => { name="TransferCompleteScreen" component={TransferCompleteScreen} /> + ) } diff --git a/src/features/collectables/HotspotList.tsx b/src/features/collectables/HotspotList.tsx index a08c9c5ce..59566adb5 100644 --- a/src/features/collectables/HotspotList.tsx +++ b/src/features/collectables/HotspotList.tsx @@ -149,6 +149,10 @@ const HotspotList = () => { navigation.navigate('ClaimAllRewardsScreen') }, [navigation]) + const handleNavigateToHotspotOnboard = useCallback(() => { + navigation.navigate('HotspotBLENavigator') + }, [navigation]) + const toggleFiltersOpen = useCallback( (open) => () => { setFiltersOpen(open) @@ -267,7 +271,6 @@ const HotspotList = () => { titleColorDisabled="secondaryText" title={t('collectablesScreen.hotspots.claimAllRewards')} titleColor="black" - marginBottom="m" disabled={ (pendingIotRewards && pendingIotRewards.eq(new BN('0')) && @@ -277,6 +280,20 @@ const HotspotList = () => { } onPress={handleNavigateToClaimRewards} /> + ) }, [ @@ -290,6 +307,7 @@ const HotspotList = () => { toggleFiltersOpen, hotspotsWithMeta, pageAmount, + handleNavigateToHotspotOnboard, ]) const renderCollectable = useCallback( diff --git a/src/features/collectables/collectablesTypes.ts b/src/features/collectables/collectablesTypes.ts index 0b4e65a8b..d542b9147 100644 --- a/src/features/collectables/collectablesTypes.ts +++ b/src/features/collectables/collectablesTypes.ts @@ -47,6 +47,7 @@ export type CollectableStackParamList = { PaymentQrScanner: undefined AddressBookNavigator: undefined ScanAddress: undefined + HotspotBLENavigator: undefined } export type CollectableNavigationProp = diff --git a/src/features/hotspot-onboarding/AddGatewayBle.tsx b/src/features/hotspot-onboarding/AddGatewayBle.tsx new file mode 100644 index 000000000..65b9533c6 --- /dev/null +++ b/src/features/hotspot-onboarding/AddGatewayBle.tsx @@ -0,0 +1,242 @@ +import BackButton from '@components/BackButton' +import ButtonPressable from '@components/ButtonPressable' +import CircleLoader from '@components/CircleLoader' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import { + init, + iotInfoKey, + keyToAssetKey, + rewardableEntityConfigKey, +} from '@helium/helium-entity-manager-sdk' +import { + AddGatewayV1, + useHotspotBle, + useOnboarding, +} from '@helium/react-native-sdk' +import { + bufferToTransaction, + heliumAddressToSolAddress, + sendAndConfirmWithRetry, +} from '@helium/spl-utils' +import { useNavigation } from '@react-navigation/native' +import { LAMPORTS_PER_SOL, PublicKey, Transaction } from '@solana/web3.js' +import { useAccountStorage } from '@storage/AccountStorageProvider' +import { DAO_KEY, IOT_SUB_DAO_KEY } from '@utils/constants' +import { getHotspotWithRewards } from '@utils/solanaUtils' +import { Buffer } from 'buffer' +import React, { useCallback } from 'react' +import { useAsyncCallback } from 'react-async-hook' +import { useTranslation } from 'react-i18next' +import { Alert } from 'react-native' +import { TabBarNavigationProp } from '../../navigation/rootTypes' +import { useSolana } from '../../solana/SolanaProvider' +import { CollectableNavigationProp } from '../collectables/collectablesTypes' +import { HotspotBleNavProp } from './navTypes' + +const AddGatewayBle = () => { + const { getOnboardingRecord, getOnboardTransactions, onboardingClient } = + useOnboarding() + const { createGatewayTxn, getOnboardingAddress } = useHotspotBle() + const { currentAccount } = useAccountStorage() + const { anchorProvider } = useSolana() + const { t } = useTranslation() + const navigation = useNavigation() + const tabNav = useNavigation() + const collectNav = useNavigation() + const onBack = useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } + }, [navigation]) + + const { + execute: handleAddGateway, + loading, + error, + } = useAsyncCallback(async () => { + if (!anchorProvider) { + Alert.alert('Error', 'No anchor provider') + return + } + const accountAddress = currentAccount?.address + if (!accountAddress) { + Alert.alert( + 'Error', + 'You must first add a wallet address from the main menu', + ) + return + } + + const onboardAddress = await getOnboardingAddress() + const onboardRecord = await getOnboardingRecord(onboardAddress) + + if (!onboardRecord) { + throw new Error( + `This hotspot does not exist in the onboarding server. Contact your manufacturer to have them approve hotspot with id ${onboardAddress}`, + ) + } + + if (!onboardRecord?.maker.address) { + throw new Error('Could not get maker address') + } + const makerSolAddr = heliumAddressToSolAddress(onboardRecord?.maker.address) + const makerSolBalance = ( + await anchorProvider.connection.getAccountInfo( + new PublicKey(makerSolAddr), + ) + )?.lamports + if ( + !makerSolBalance || + makerSolBalance / LAMPORTS_PER_SOL < 0.00089088 + 0.00001 + ) { + throw new Error( + `Manufacturer ${onboardRecord?.maker.name} does not have enough SOL to onboard this hotspot. Please contact the manufacturer to resolve this issue.`, + ) + } + + const txnStr = await createGatewayTxn({ + ownerAddress: accountAddress, + payerAddress: onboardRecord?.maker.address, + }) + const tx = AddGatewayV1.fromString(txnStr) + if (!tx?.gateway?.b58) { + throw new Error('Error signing gateway txn') + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function wrapProgramError(e: any) { + if ( + e.toString().includes('Insufficient Balance') || + e.toString().includes('"Custom":1') || + e.InstructionError[1].Custom === 1 + ) { + throw new Error( + `Manufacturer ${onboardRecord?.maker.name} does not have enough SOL or Data Credits to onboard this hotspot. Please contact the manufacturer to resolve this issue.`, + ) + } + if (e.InstructionError) { + throw new Error(`Program Error: ${JSON.stringify(e)}`) + } + throw e + } + + const createTxns = await onboardingClient.createHotspot({ + transaction: txnStr, + }) + + const createHotspotTxns = createTxns.data?.solanaTransactions?.map( + (createHotspotTx) => Buffer.from(createHotspotTx), + ) + try { + // eslint-disable-next-line no-restricted-syntax + for (const txn of createHotspotTxns || []) { + // eslint-disable-next-line no-await-in-loop + await sendAndConfirmWithRetry( + anchorProvider.connection, + txn, + { + skipPreflight: true, + }, + 'confirmed', + ) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + wrapProgramError(e) + } + + const hemProgram = await init(anchorProvider) + const [iotConfigKey] = rewardableEntityConfigKey(IOT_SUB_DAO_KEY, 'IOT') + const iotInfo = await hemProgram.account.iotHotspotInfoV0.fetchNullable( + iotInfoKey(iotConfigKey, tx?.gateway?.b58)[0], + ) + if (!iotInfo) { + const { solanaTransactions } = await getOnboardTransactions({ + hotspotAddress: onboardAddress, + hotspotTypes: ['iot'], + }) + let solanaSignedTransactions: Transaction[] | undefined + + if (solanaTransactions) { + solanaSignedTransactions = + await anchorProvider?.wallet.signAllTransactions( + solanaTransactions.map((txn) => { + return bufferToTransaction(Buffer.from(txn, 'base64')) + }), + ) + } + + if (solanaSignedTransactions) { + try { + // eslint-disable-next-line no-restricted-syntax + for (const txn of solanaSignedTransactions) { + // eslint-disable-next-line no-await-in-loop + await sendAndConfirmWithRetry( + anchorProvider.connection, + txn.serialize(), + { + skipPreflight: true, + }, + 'confirmed', + ) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + wrapProgramError(e) + } + } + } + + const keyToAssetK = keyToAssetKey(DAO_KEY, tx.gateway.b58, 'b58')[0] + const keyToAsset = await hemProgram.account.keyToAssetV0.fetch(keyToAssetK) + const { asset } = keyToAsset + const collectable = await getHotspotWithRewards(asset, anchorProvider) + collectNav.navigate( + iotInfo ? 'HotspotDetailsScreen' : 'AssertLocationScreen', + { collectable }, + ) + }) + + return ( + + + {t('hotspotOnboarding.onboarding.title')} + + {t('hotspotOnboarding.onboarding.subtitle')} + + {loading && } + {error && ( + + {error.message ? error.message.toString() : error.toString()} + + )} + + tabNav.push('Collectables')} + /> + + ) +} + +export default AddGatewayBle diff --git a/src/features/hotspot-onboarding/Diagnostics.tsx b/src/features/hotspot-onboarding/Diagnostics.tsx new file mode 100644 index 000000000..487a21255 --- /dev/null +++ b/src/features/hotspot-onboarding/Diagnostics.tsx @@ -0,0 +1,54 @@ +import BackButton from '@components/BackButton' +import ButtonPressable from '@components/ButtonPressable' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import { DiagnosticInfo, useHotspotBle } from '@helium/react-native-sdk' +import { useNavigation } from '@react-navigation/native' +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import type { HotspotBleNavProp } from './navTypes' + +const Diagnostics = () => { + const navigation = useNavigation() + const [diagnosticInfo, setDiagnosticInfo] = useState() + const { getDiagnosticInfo } = useHotspotBle() + const { t } = useTranslation() + const onBack = useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } + }, [navigation]) + const navNext = useCallback( + () => navigation.push('AddGatewayBle'), + [navigation], + ) + + useEffect(() => { + getDiagnosticInfo() + .then(setDiagnosticInfo) + .catch(() => navNext()) + }, [getDiagnosticInfo, navNext]) + + return ( + + + {t('hotspotOnboarding.diagnostics.title')} + {diagnosticInfo && ( + + {JSON.stringify(diagnosticInfo, null, 2)} + + )} + + + ) +} + +export default Diagnostics diff --git a/src/features/hotspot-onboarding/HotspotBLENav.tsx b/src/features/hotspot-onboarding/HotspotBLENav.tsx new file mode 100644 index 000000000..3519880aa --- /dev/null +++ b/src/features/hotspot-onboarding/HotspotBLENav.tsx @@ -0,0 +1,49 @@ +import { HotspotBleProvider } from '@helium/react-native-sdk' +import { + createNativeStackNavigator, + NativeStackNavigationOptions, +} from '@react-navigation/native-stack' +import * as React from 'react' +import AddGatewayBle from './AddGatewayBle' +import Diagnostics from './Diagnostics' +import ScanHotspots from './ScanHotspots' +import WifiSettings from './WifiSettings' +import WifiSetup from './WifiSetup' + +const Stack = createNativeStackNavigator() + +const screenOptions = { headerShown: false } as NativeStackNavigationOptions + +export default React.memo(function HotspotBLENav() { + return ( + + + + + + + + + + ) +}) diff --git a/src/features/hotspot-onboarding/ScanHotspots.tsx b/src/features/hotspot-onboarding/ScanHotspots.tsx new file mode 100644 index 000000000..d3042af28 --- /dev/null +++ b/src/features/hotspot-onboarding/ScanHotspots.tsx @@ -0,0 +1,227 @@ +import BackButton from '@components/BackButton' +import ButtonPressable from '@components/ButtonPressable' +import CircleLoader from '@components/CircleLoader' +import FabButton from '@components/FabButton' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import { Device, useHotspotBle } from '@helium/react-native-sdk' +import { useNavigation } from '@react-navigation/native' +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList, Platform } from 'react-native' +import { + PERMISSIONS, + PermissionStatus, + RESULTS, + check, + request, +} from 'react-native-permissions' +import * as Logger from '../../utils/logger' +import type { HotspotBleNavProp } from './navTypes' + +const ScanHotspots = () => { + const { startScan, stopScan, connect, scannedDevices } = useHotspotBle() + const [scanning, setScanning] = useState(false) + const [canScan, setCanScan] = useState(undefined) + const navigation = useNavigation() + const { t } = useTranslation() + const onBack = useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } + }, [navigation]) + const [error, setError] = useState(undefined) + + const showError = (e: any) => { + Logger.error(e) + setError(e.toString()) + } + + const updateCanScan = useCallback((result: PermissionStatus) => { + switch (result) { + case RESULTS.UNAVAILABLE: + case RESULTS.BLOCKED: + case RESULTS.DENIED: + case RESULTS.LIMITED: + setCanScan(false) + break + case RESULTS.GRANTED: + setCanScan(true) + break + } + }, []) + + useEffect(() => { + if (Platform.OS === 'ios') { + setCanScan(true) + return + } + + check(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION) + .then(updateCanScan) + .catch(showError) + }, [updateCanScan]) + + useEffect(() => { + if (canScan !== false) return + + request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION) + .then(updateCanScan) + .catch(showError) + }, [canScan, updateCanScan]) + + const handleScanPress = useCallback(() => { + const shouldScan = !scanning + setScanning(shouldScan) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let timeout: any | undefined + if (shouldScan) { + setError(undefined) + timeout = setTimeout(() => { + stopScan() + setScanning(false) + if (scannedDevices.length === 0) { + setError( + 'No hotspots found. Please ensure bluetooth pairing is enabled', + ) + } + }, 30 * 1000) + } + + if (shouldScan) { + startScan((e) => { + if (e) { + showError(e) + } + }) + } else { + stopScan() + } + return () => { + if (timeout) { + clearTimeout(timeout) + } + } + }, [scannedDevices.length, scanning, startScan, stopScan]) + + const navNext = useCallback( + () => navigation.push('WifiSettings'), + [navigation], + ) + + const [connecting, setConnecting] = useState(false) + const connectDevice = useCallback( + (d: Device) => async () => { + try { + setConnecting(true) + await connect(d) + if (scanning) { + stopScan() + setScanning(false) + } + setConnecting(false) + navNext() + } catch (e) { + showError(e) + } finally { + setConnecting(false) + } + }, + [connect, navNext, scanning, stopScan], + ) + + const renderItem = React.useCallback( + // eslint-disable-next-line react/no-unused-prop-types + ({ item }: { item: Device }) => { + return ( + + + + {item.name} + + + ) + }, + [connectDevice, connecting], + ) + + const keyExtractor = React.useCallback(({ id }: Device) => id, []) + + return ( + + + {t('hotspotOnboarding.scan.title')} + + {t('hotspotOnboarding.scan.subtitle')} + + {scannedDevices.length === 0 && scanning && } + {scannedDevices.length === 0 && scanning && ( + + {t('hotspotOnboarding.scan.scanning')} + + )} + + + {error && ( + + {error} + + )} + + + ) +} + +export default ScanHotspots diff --git a/src/features/hotspot-onboarding/WifiSettings.tsx b/src/features/hotspot-onboarding/WifiSettings.tsx new file mode 100644 index 000000000..d2822e4a4 --- /dev/null +++ b/src/features/hotspot-onboarding/WifiSettings.tsx @@ -0,0 +1,176 @@ +import BackButton from '@components/BackButton' +import ButtonPressable from '@components/ButtonPressable' +import FabButton from '@components/FabButton' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import { useHotspotBle } from '@helium/react-native-sdk' +import { useNavigation } from '@react-navigation/native' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert, SectionList } from 'react-native' +import type { HotspotBleNavProp } from './navTypes' + +type Section = { + title: string + data: string[] + type: 'configured' | 'available' +} + +const WifiSettings = () => { + const navigation = useNavigation() + const { t } = useTranslation() + const onBack = useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } + }, [navigation]) + const navNext = useCallback( + () => navigation.push('AddGatewayBle'), + [navigation], + ) + const [networks, setNetworks] = useState() + const [configuredNetworks, setConfiguredNetworks] = useState() + const [connected, setConnected] = useState(false) + + const { isConnected, readWifiNetworks, removeConfiguredWifi } = + useHotspotBle() + + useEffect(() => { + isConnected().then(setConnected) + }, [isConnected]) + + useEffect(() => { + if (!connected) return + + readWifiNetworks(true).then(setConfiguredNetworks) + readWifiNetworks(false).then(setNetworks) + }, [connected, readWifiNetworks]) + + const handleNetworkSelected = useCallback( + ({ + network, + type, + }: { + network: string + type: 'configured' | 'available' + }) => + async () => { + if (type === 'available') { + navigation.push('WifiSetup', { network }) + } else { + Alert.alert( + t('hotspotOnboarding.wifiSettings.title'), + t('hotspotOnboarding.wifiSettings.remove', { network }), + [ + { + text: t('generic.cancel'), + style: 'default', + }, + { + text: t('generic.remove'), + style: 'destructive', + onPress: async () => { + setConfiguredNetworks( + configuredNetworks?.filter((n) => n !== network), + ) + await removeConfiguredWifi(network) + readWifiNetworks(true).then(setConfiguredNetworks) + readWifiNetworks(false).then(setNetworks) + }, + }, + ], + ) + } + }, + [configuredNetworks, navigation, readWifiNetworks, removeConfiguredWifi, t], + ) + + const renderItem = useCallback( + ({ + item: network, + section: { type }, + }: { + // eslint-disable-next-line react/no-unused-prop-types + item: string + // eslint-disable-next-line react/no-unused-prop-types + section: Section + }) => { + return ( + + + + {network} + + + ) + }, + [handleNetworkSelected], + ) + + const keyExtractor = useCallback((name: string) => name, []) + + const renderSectionHeader = ({ + section: { title }, + }: { + section: Section + }) => ( + + {title} + + ) + + const sections = useMemo( + (): Section[] => [ + { + data: configuredNetworks || [], + title: t('hotspotOnboarding.wifiSettings.configured'), + type: 'configured', + }, + { + data: networks || [], + title: t('hotspotOnboarding.wifiSettings.available'), + type: 'available', + }, + ], + [configuredNetworks, networks, t], + ) + + return ( + + + + {t('hotspotOnboarding.wifiSettings.title')} + + + + + ) +} + +export default WifiSettings diff --git a/src/features/hotspot-onboarding/WifiSetup.tsx b/src/features/hotspot-onboarding/WifiSetup.tsx new file mode 100644 index 000000000..25c6f3d24 --- /dev/null +++ b/src/features/hotspot-onboarding/WifiSetup.tsx @@ -0,0 +1,90 @@ +import BackButton from '@components/BackButton' +import Box from '@components/Box' +import ButtonPressable from '@components/ButtonPressable' +import SafeAreaBox from '@components/SafeAreaBox' +import Text from '@components/Text' +import TextInput from '@components/TextInput' +import { BleError, useHotspotBle } from '@helium/react-native-sdk' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import type { HotspotBLEStackParamList, HotspotBleNavProp } from './navTypes' + +type Route = RouteProp +const WifiSetup = () => { + const { + params: { network }, + } = useRoute() + const [secureTextEntry, setSecureTextEntry] = useState(true) + const [loading, setLoading] = useState(false) + const [status, setStatus] = useState('') + const [password, setPassword] = useState('') + const { setWifi } = useHotspotBle() + const { t } = useTranslation() + const navigation = useNavigation() + const onBack = useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } + }, [navigation]) + + const toggleSecureEntry = useCallback(() => { + setSecureTextEntry(!secureTextEntry) + }, [secureTextEntry]) + + const handleSetWifi = useCallback(async () => { + setLoading(true) + try { + const nextStatus = await setWifi(network, password) + setStatus(nextStatus) + } catch (e) { + if (typeof e === 'string') { + setStatus(e) + } else { + setStatus((e as BleError).toString()) + } + } + setLoading(false) + }, [network, password, setWifi]) + + return ( + + + {network} + + + + + + + {loading ? 'loading...' : status} + + ) +} + +export default WifiSetup diff --git a/src/features/hotspot-onboarding/navTypes.tsx b/src/features/hotspot-onboarding/navTypes.tsx new file mode 100644 index 000000000..0ef6ba4c5 --- /dev/null +++ b/src/features/hotspot-onboarding/navTypes.tsx @@ -0,0 +1,12 @@ +import { NativeStackNavigationProp } from '@react-navigation/native-stack' + +export type HotspotBLEStackParamList = { + ScanHotspots: undefined + WifiSettings: undefined + WifiSetup: { network: string } + AddGatewayBle: undefined + Diagnostics: undefined +} + +export type HotspotBleNavProp = + NativeStackNavigationProp diff --git a/src/hooks/useSubmitTxn.ts b/src/hooks/useSubmitTxn.ts index 92b9c1ef1..9f3a14c60 100644 --- a/src/hooks/useSubmitTxn.ts +++ b/src/hooks/useSubmitTxn.ts @@ -1,5 +1,6 @@ import { HotspotType } from '@helium/onboarding' -import { chunks } from '@helium/spl-utils' +import { useOnboarding } from '@helium/react-native-sdk' +import { chunks, sendAndConfirmWithRetry } from '@helium/spl-utils' import { PublicKey, Transaction } from '@solana/web3.js' import { useAccountStorage } from '@storage/AccountStorageProvider' import i18n from '@utils/i18n' @@ -18,8 +19,6 @@ import { sendDelegateDataCredits, sendMintDataCredits, sendTreasurySwap, - sendUpdateIotInfo, - sendUpdateMobileInfo, } from '../store/slices/solanaSlice' import { useAppDispatch } from '../store/store' import { @@ -33,6 +32,7 @@ export default () => { const { cluster, anchorProvider } = useSolana() const { t } = i18n const { walletSignBottomSheetRef } = useWalletSign() + const { getAssertData } = useOnboarding() const dispatch = useAppDispatch() @@ -461,58 +461,76 @@ export default () => { throw new Error(t('errors.account')) } - const updateInfoTxn = await solUtils.updateEntityInfoTxn({ - anchorProvider, - type, - entityKey, + const data = await getAssertData({ + decimalGain: decimalGain ? parseFloat(decimalGain) : undefined, + elevation: elevation ? parseFloat(elevation) : undefined, + gateway: entityKey, lat, lng, - elevation: elevation ? parseFloat(elevation) : undefined, - decimalGain: decimalGain ? parseFloat(decimalGain) : undefined, + owner: currentAccount.address, + hotspotTypes: [type], }) - const serializedTx = updateInfoTxn.serialize({ - requireAllSignatures: false, - }) + const serializedTxs = data.solanaTransactions?.map((txn) => + Buffer.from(txn, 'base64'), + ) const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', additionalMessage: t('transactions.signAssertLocationTxn'), - serializedTxs: [Buffer.from(serializedTx)], + serializedTxs, }) if (!decision) { throw new Error('User rejected transaction') } - - if (type === 'iot') { - await dispatch( - sendUpdateIotInfo({ - account: currentAccount, - anchorProvider, - cluster, - updateTxn: updateInfoTxn, - }), - ) - } - - if (type === 'mobile') { - await dispatch( - sendUpdateMobileInfo({ - account: currentAccount, - anchorProvider, - cluster, - updateTxn: updateInfoTxn, - }), - ) + const signedTxns = + serializedTxs && + (await anchorProvider.wallet.signAllTransactions( + serializedTxs.map((ser) => Transaction.from(ser)), + )) + + try { + // eslint-disable-next-line no-restricted-syntax + for (const txn of signedTxns || []) { + // eslint-disable-next-line no-await-in-loop + await sendAndConfirmWithRetry( + anchorProvider.connection, + txn.serialize(), + { + skipPreflight: true, + }, + 'confirmed', + ) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + if ( + e.toString().includes('Insufficient Balance') || + e.toString().includes('"Custom":1') || + e.InstructionError[1].Custom === 1 + ) { + if (data.isFree) { + throw new Error( + `Manufacturer ${data?.maker?.name} does not have enough SOL or Data Credits to assert location. Please contact the manufacturer of this hotspot to resolve this issue.`, + ) + } else { + throw new Error( + 'Insufficient balance of either HNT, Data Credits, or Sol to assert location', + ) + } + } + if (e.InstructionError) { + throw new Error(`Program Error: ${JSON.stringify(e)}`) + } + throw e } }, [ anchorProvider, - cluster, currentAccount, - dispatch, + getAssertData, t, walletSignBottomSheetRef, ], diff --git a/src/locales/en.ts b/src/locales/en.ts index db630f4d0..e80c0e29c 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -17,6 +17,33 @@ export default { errors: { accountNotSelected: 'There must be a wallet selected to submit a txn', }, + hotspotOnboarding: { + scan: { + title: 'Scan for Hotspots', + subtitle: + "Please enable BLE on your hotspot. Depending on the manufacturer, there may be a button to enable BLE, or it may be enabled for the first 5 minutes after powering on. Please refer to your manufacturer instructions for more information. If your hotspot does not support BLE, you will need to onboard through your manufacturer's app.", + start: 'Start Scan', + stop: 'Stop Scan', + notEnabled: 'Bluetooth is not enabled', + scanning: 'Scanning for Hotspots', + }, + wifiSettings: { + title: 'Wifi Settings', + remove: 'Would you like to remove {{network}}?', + available: 'Available Networks', + configured: 'Configured Networks', + setup: 'Setup Wifi', + }, + diagnostics: { + title: 'Diagnostics', + }, + onboarding: { + title: 'Onboarding', + subtitle: + 'Onboard your hotspot to the IOT network. After onboarding this hotspot, you will be able to set the location and antenna details.', + onboard: 'Onboard Hotspot', + }, + }, accountImport: { accountLimit: 'You have reached the wallet limit.\nTo add another wallet, remove a wallet account and try again.', @@ -213,6 +240,7 @@ export default { hotspotCount_plural: '{{count}} Hotspots', chooseAmountOfHotspots: 'Choose amount of hotspots to show per page', filter: 'Filter', + new: 'Connect a Hotspot', currentDisplayedRewards: 'The rewards that are currently displayed as pending are only for the hotspots shown. Scroll to load more or click the filter to show more hotspots per page.', showAllHotspotsWarning: @@ -555,7 +583,9 @@ export default { insufficientBalance: 'Insufficient balance', ok: 'OK', period: '.', + password: 'Password', retry: 'Retry', + remove: 'Remove', share: 'Share', skip: 'Skip', success: 'Success', diff --git a/src/navigation/RootNavigator.tsx b/src/navigation/RootNavigator.tsx index 38eeb4c94..f124fc9bc 100644 --- a/src/navigation/RootNavigator.tsx +++ b/src/navigation/RootNavigator.tsx @@ -7,10 +7,10 @@ import { useColors } from '@theme/themeHooks' import React, { memo, useCallback, useEffect, useRef } from 'react' import changeNavigationBarColor from 'react-native-navigation-bar-color' import { useSelector } from 'react-redux' -import DappLoginScreen from '../features/dappLogin/DappLoginScreen' import ConnectedWallets, { ConnectedWalletsRef, } from '../features/account/ConnectedWallets' +import DappLoginScreen from '../features/dappLogin/DappLoginScreen' import { HomeNavigationProp } from '../features/home/homeTypes' import OnboardingNavigator from '../features/onboarding/OnboardingNavigator' import ImportPrivateKey from '../features/onboarding/import/ImportPrivateKey' diff --git a/src/solana/SolanaProvider.tsx b/src/solana/SolanaProvider.tsx index 238fd564b..b14a70f69 100644 --- a/src/solana/SolanaProvider.tsx +++ b/src/solana/SolanaProvider.tsx @@ -5,6 +5,7 @@ import { init as initHem } from '@helium/helium-entity-manager-sdk' import { init as initHsd } from '@helium/helium-sub-daos-sdk' import { init as initLazy } from '@helium/lazy-distributor-sdk' import { DC_MINT, HNT_MINT } from '@helium/spl-utils' +import { SolanaProvider as SolanaProviderRnHelium } from '@helium/react-native-sdk' import { AccountInfo, Cluster, @@ -184,7 +185,19 @@ const SolanaContext = const { Provider } = SolanaContext const SolanaProvider = ({ children }: { children: ReactNode }) => { - return {children} + const value = useSolanaHook() + return ( + + {value.connection && ( + + {children} + + )} + + ) } export const useSolana = (): SolanaManager => useContext(SolanaContext) diff --git a/src/store/slices/solanaSlice.ts b/src/store/slices/solanaSlice.ts index f669023f4..cafeaf2f3 100644 --- a/src/store/slices/solanaSlice.ts +++ b/src/store/slices/solanaSlice.ts @@ -123,20 +123,6 @@ type DelegateDataCreditsInput = { delegateDCTxn: Transaction } -type UpdateIotInfoInput = { - account: CSAccount - anchorProvider: AnchorProvider - cluster: Cluster - updateTxn: Transaction -} - -type UpdateMobileInfoInput = { - account: CSAccount - anchorProvider: AnchorProvider - cluster: Cluster - updateTxn: Transaction -} - export const makePayment = createAsyncThunk( 'solana/makePayment', async ({ account, cluster, anchorProvider, paymentTxns }: PaymentInput) => { @@ -521,38 +507,6 @@ export const getTxns = createAsyncThunk( }, ) -export const sendUpdateIotInfo = createAsyncThunk( - 'solana/sendUpdateIotInfo', - async ({ cluster, anchorProvider, updateTxn }: UpdateIotInfoInput) => { - try { - const signed = await anchorProvider.wallet.signTransaction(updateTxn) - const sig = await anchorProvider.sendAndConfirm(signed) - - postPayment({ signatures: [sig], cluster }) - } catch (error) { - Logger.error(error) - throw error - } - return true - }, -) - -export const sendUpdateMobileInfo = createAsyncThunk( - 'solana/sendUpdateMobileInfo', - async ({ cluster, anchorProvider, updateTxn }: UpdateMobileInfoInput) => { - try { - const signed = await anchorProvider.wallet.signTransaction(updateTxn) - const sig = await anchorProvider.sendAndConfirm(signed) - - postPayment({ signatures: [sig], cluster }) - } catch (error) { - Logger.error(error) - throw error - } - return true - }, -) - const solanaSlice = createSlice({ name: 'solana', initialState, @@ -843,32 +797,6 @@ const solanaSlice = createSlice({ state.activity.error = error } }) - builder.addCase(sendUpdateIotInfo.rejected, (state, action) => { - state.payment = { success: false, loading: false, error: action.error } - }) - builder.addCase(sendUpdateIotInfo.pending, (state, _action) => { - state.payment = { success: false, loading: true, error: undefined } - }) - builder.addCase(sendUpdateIotInfo.fulfilled, (state, _action) => { - state.payment = { - success: true, - loading: false, - error: undefined, - } - }) - builder.addCase(sendUpdateMobileInfo.rejected, (state, action) => { - state.payment = { success: false, loading: false, error: action.error } - }) - builder.addCase(sendUpdateMobileInfo.pending, (state, _action) => { - state.payment = { success: false, loading: true, error: undefined } - }) - builder.addCase(sendUpdateMobileInfo.fulfilled, (state, _action) => { - state.payment = { - success: true, - loading: false, - error: undefined, - } - }) }, }) diff --git a/src/utils/solanaUtils.ts b/src/utils/solanaUtils.ts index f2d97cd13..dfd1ae278 100644 --- a/src/utils/solanaUtils.ts +++ b/src/utils/solanaUtils.ts @@ -980,6 +980,18 @@ export const getCompressedCollectables = async ( return items as CompressedNFT[] } +export const getHotspotWithRewards = async ( + assetId: PublicKey, + anchorProvider: AnchorProvider, +): Promise => { + const conn = anchorProvider.connection as WrappedConnection + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const asset = ((await conn.getAsset(assetId.toBase58())) as any).result + + const withMetadata = await getCompressedNFTMetadata([asset as CompressedNFT]) + return (await annotateWithPendingRewards(anchorProvider, withMetadata))[0] +} + export const getCompressedCollectablesByCreator = async ( pubKey: PublicKey, anchorProvider: AnchorProvider, diff --git a/yarn.lock b/yarn.lock index 3dfabc494..8b2b6a4ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,6 +18,11 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" +"@azure/core-asynciterator-polyfill@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz#0dd3849fb8d97f062a39db0e5cadc9ffaf861fec" + integrity sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw== + "@babel/code-frame@7.10.4", "@babel/code-frame@~7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -1360,6 +1365,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.22.6": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" + integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" @@ -1478,6 +1490,27 @@ superstruct "^0.15.4" toml "^3.0.0" +"@coral-xyz/anchor@^0.28.1-beta.1": + version "0.28.1-beta.2" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.28.1-beta.2.tgz#4ddd4b2b66af04407be47cf9524147793ec514a0" + integrity sha512-xreUcOFF8+IQKWOBUrDKJbIw2ftpRVybFlEPVrbSlOBCbreCWrQ5754Gt9cHIcuBDAzearCDiBqzsGQdNgPJiw== + dependencies: + "@coral-xyz/borsh" "^0.28.0" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.68.0" + base64-js "^1.5.1" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + crypto-hash "^1.3.0" + eventemitter3 "^4.0.7" + pako "^2.0.3" + snake-case "^3.0.4" + superstruct "^0.15.4" + toml "^3.0.0" + "@coral-xyz/borsh@^0.26.0": version "0.26.0" resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.26.0.tgz#d054f64536d824634969e74138f9f7c52bbbc0d5" @@ -1486,6 +1519,14 @@ bn.js "^5.1.2" buffer-layout "^1.2.0" +"@coral-xyz/borsh@^0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.28.0.tgz#fa368a2f2475bbf6f828f4657f40a52102e02b6d" + integrity sha512-/u1VTzw7XooK7rqeD7JLUSwOyRSesPUk0U37BV9zK0axJc1q0nRbKFGFLYCQ16OtdOJTTwGfGp11Lx9B45bRCQ== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -2313,6 +2354,18 @@ bn.js "^5.2.0" bs58 "^4.0.1" +"@helium/circuit-breaker-sdk@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@helium/circuit-breaker-sdk/-/circuit-breaker-sdk-0.1.4.tgz#53a49a70d533540e4118c19f2d04cd63c556b390" + integrity sha512-9Fa1zxhYO9Nh+iZuPgA4dpyAp9Le2TRoVRu/caWWDC8DNC9Ba2Hd/xeWbRDExymryVZqq741U57OiAi3cPXwbQ== + dependencies: + "@coral-xyz/anchor" "^0.26.0" + "@helium/idls" "^0.1.1" + "@helium/spl-utils" "^0.1.4" + "@solana/spl-token" "^0.3.6" + bn.js "^5.2.0" + bs58 "^4.0.1" + "@helium/circuit-breaker-sdk@^0.2.17": version "0.2.17" resolved "https://registry.yarnpkg.com/@helium/circuit-breaker-sdk/-/circuit-breaker-sdk-0.2.17.tgz#79baa8778bec3bde091d6189eb03004e553e7da0" @@ -2334,7 +2387,7 @@ react-native-sodium "^0.4.0" safe-buffer "^5.2.1" -"@helium/currency-utils@0.1.1": +"@helium/currency-utils@0.1.1", "@helium/currency-utils@^0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@helium/currency-utils/-/currency-utils-0.1.1.tgz#20398359f5b44c805a35eae1a4655ecbabc7b35e" integrity sha512-3HyB4qu6HQ1UCQepF02n7fmw2YVUE+o6k//1Dy/ZQFqV9xlk/CaxguZ72MJC9nZaUD+im0pT2ZQjFoViGNRyrw== @@ -2350,6 +2403,21 @@ dependencies: bignumber.js "^9.0.0" +"@helium/data-credits-sdk@^0.1.3": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@helium/data-credits-sdk/-/data-credits-sdk-0.1.4.tgz#8ba9b9bde64ed65911d03bf6c77392c2c040c3a1" + integrity sha512-CgubrCg/iHWNPRW4G9XhHLqIppnrhddwSbcaeQlPT1B+R+CW7Zeft9bFqcamCge1l2/3vxzHlrKz5kR5tLRIVw== + dependencies: + "@coral-xyz/anchor" "0.26.0" + "@helium/circuit-breaker-sdk" "^0.1.4" + "@helium/helium-sub-daos-sdk" "^0.1.4" + "@helium/idls" "^0.1.1" + "@helium/spl-utils" "^0.1.4" + "@solana/spl-token" "^0.3.6" + bn.js "^5.2.0" + bs58 "^4.0.1" + crypto-js "^4.1.1" + "@helium/data-credits-sdk@^0.2.17": version "0.2.17" resolved "https://registry.yarnpkg.com/@helium/data-credits-sdk/-/data-credits-sdk-0.2.17.tgz#778bf5b0c19237ff02b8973f4ab2d6a0fc4b64db" @@ -2405,6 +2473,25 @@ bn.js "^5.2.0" bs58 "^4.0.1" +"@helium/helium-entity-manager-sdk@^0.1.3", "@helium/helium-entity-manager-sdk@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@helium/helium-entity-manager-sdk/-/helium-entity-manager-sdk-0.1.5.tgz#96457885d125c781831a1b595b3cb5e3d6a91f1d" + integrity sha512-1zvavQVXDXmH5IFKYJJwGPxV4iMPQYiGJKk6do1jGFFizfGelDD7raUq+csnMiNExYQeOT8vQSyjNpQuYKjU2g== + dependencies: + "@coral-xyz/anchor" "0.26.0" + "@helium/address" "^4.6.2" + "@helium/helium-sub-daos-sdk" "^0.1.4" + "@helium/idls" "^0.1.1" + "@helium/spl-utils" "^0.1.4" + "@metaplex-foundation/mpl-bubblegum" "^0.6.2" + "@metaplex-foundation/mpl-token-metadata" "^2.2.3" + "@solana/spl-account-compression" "^0.1.5" + "@solana/spl-token" "^0.3.6" + bn.js "^5.2.0" + bs58 "^4.0.1" + crypto-js "^4.1.1" + js-sha256 "^0.9.0" + "@helium/helium-entity-manager-sdk@^0.2.17": version "0.2.17" resolved "https://registry.yarnpkg.com/@helium/helium-entity-manager-sdk/-/helium-entity-manager-sdk-0.2.17.tgz#819ea9eaecf8c6c69218cb0e7be4daf5cfd98cb0" @@ -2435,6 +2522,19 @@ pako "^2.0.3" react-async-hook "^4.0.0" +"@helium/helium-sub-daos-sdk@^0.1.3", "@helium/helium-sub-daos-sdk@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@helium/helium-sub-daos-sdk/-/helium-sub-daos-sdk-0.1.4.tgz#99852310f0f8fa4e7afa2d128f3d2eff884b65d4" + integrity sha512-O7OiEYrZeLBHJJAdzPuG3JygrZ4i+cb3l5QnyQ+pIVpunuOfsA+fNpzgzDH2MBE9MDUkOr3kR3uSF3Jy3DA9ww== + dependencies: + "@coral-xyz/anchor" "0.26.0" + "@helium/circuit-breaker-sdk" "^0.1.4" + "@helium/spl-utils" "^0.1.4" + "@helium/treasury-management-sdk" "^0.1.4" + "@helium/voter-stake-registry-sdk" "^0.1.4" + bn.js "^5.2.0" + bs58 "^4.0.1" + "@helium/helium-sub-daos-sdk@^0.2.17": version "0.2.17" resolved "https://registry.yarnpkg.com/@helium/helium-sub-daos-sdk/-/helium-sub-daos-sdk-0.2.17.tgz#dbd279eceaff152791cea316495cc51c1d275391" @@ -2448,6 +2548,19 @@ bn.js "^5.2.0" bs58 "^4.0.1" +"@helium/hotspot-utils@^0.1.3": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@helium/hotspot-utils/-/hotspot-utils-0.1.5.tgz#e93ff9a15d7678c28a696ef77d617478eb83cfd7" + integrity sha512-bF0gMlkZk7DYJ7ugxvhNW/AKWlDM+hrxh+sm6MN67kNiNlGsqdAU+LwgoZ4Z5/C2FWNMgNqUMTE4CQIJRshubg== + dependencies: + "@coral-xyz/anchor" "^0.26.0" + "@helium/helium-entity-manager-sdk" "^0.1.5" + "@helium/helium-sub-daos-sdk" "^0.1.4" + "@helium/idls" "^0.1.1" + "@helium/spl-utils" "^0.1.4" + "@solana/web3.js" "^1.73.0" + bs58 "^5.0.0" + "@helium/http@4.7.5": version "4.7.5" resolved "https://registry.yarnpkg.com/@helium/http/-/http-4.7.5.tgz#c78ffcba77d29b9ef5514adfd41c08412ee8a8d3" @@ -2461,6 +2574,17 @@ retry-axios "^2.1.2" snakecase-keys "^5.1.0" +"@helium/idls@^0.0.26": + version "0.0.26" + resolved "https://registry.yarnpkg.com/@helium/idls/-/idls-0.0.26.tgz#379890824915edc3bdfb69bdf1db3936e17cb500" + integrity sha512-SpSmogohcQ0ZWcGmvx43RpGd3optlXmZmkXnZ5sVzbjq3bYwh2usVRMeVmzrrXsuT3uZaftofUXGpJi9qY4RtQ== + dependencies: + "@coral-xyz/anchor" "0.26.0" + "@solana/web3.js" "^1.43.4" + bn.js "^5.2.0" + borsh "^0.7.0" + bs58 "^4.0.1" + "@helium/idls@^0.0.43": version "0.0.43" resolved "https://registry.yarnpkg.com/@helium/idls/-/idls-0.0.43.tgz#de77dccd27411f6f2eed6daabb8b1ea1f600fe19" @@ -2514,6 +2638,15 @@ axios-retry "^3.4.0" qs "^6.10.3" +"@helium/onboarding@^4.10.0": + version "4.10.2" + resolved "https://registry.yarnpkg.com/@helium/onboarding/-/onboarding-4.10.2.tgz#5991f0c0c6d15ebbddeaf92ddf5be7625da3bf73" + integrity sha512-Kbh7WD7Fu/AEqHvn0GVhhusN0Zpp+h7ENFjq5+NKEOOyQj7k3m5skSwl1RmwPqpO58QdBM4DKNvBASX7XNuXjA== + dependencies: + axios "^0.24.0" + axios-retry "^3.4.0" + qs "^6.10.3" + "@helium/proto-ble@4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@helium/proto-ble/-/proto-ble-4.0.0.tgz#dd2800d0dbbfc793ea5cb6b45e7613f490f8a4d8" @@ -2521,6 +2654,13 @@ dependencies: protobufjs "^6.8.9" +"@helium/proto-ble@^4.8.1": + version "4.11.1" + resolved "https://registry.yarnpkg.com/@helium/proto-ble/-/proto-ble-4.11.1.tgz#23de2c6d112b77001d3c09808c5e4d6a6847f323" + integrity sha512-k3tvz+YmcXlGMcCLsjEagEyuQQvRD9Bfs8tzO5apgPI/ilbJQrjj5Oc6iv95TYj1RTJt1pdlDcc+qVI6y5m1uw== + dependencies: + protobufjs "^6.8.9" + "@helium/proto@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@helium/proto/-/proto-1.6.0.tgz#b97003bbc511afec1abac7cbffc5c088e348022d" @@ -2528,10 +2668,38 @@ dependencies: protobufjs "^6.11.3" -"@helium/react-native-sdk@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@helium/react-native-sdk/-/react-native-sdk-1.0.0.tgz#41024fa99859490bd8a0b717f52acc11ae72f114" - integrity sha512-Qi1Nnp/q2hsz2D7aeuM6LxXhNX8NrHz1U+PoQslwK2XfqPFZEYb4uAzjXDKlc+JBWPiF96GMJywv/ofxlZ9XLg== +"@helium/react-native-sdk@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@helium/react-native-sdk/-/react-native-sdk-2.1.4.tgz#fc32db2430d058bb1aa8b8dff2c9f3ee7599e632" + integrity sha512-a4bojy41vrFeHfvFcxtxt+Nsy/llS7k5GSOm/cmwIpy2r4SU3ScuNjgAaCsClmZ+A/OzTrjJMCbbKQdCq9cP4g== + dependencies: + "@azure/core-asynciterator-polyfill" "^1.0.2" + "@coral-xyz/anchor" "^0.26.0" + "@helium/currency-utils" "^0.1.1" + "@helium/data-credits-sdk" "^0.1.3" + "@helium/helium-entity-manager-sdk" "^0.1.3" + "@helium/helium-sub-daos-sdk" "^0.1.3" + "@helium/hotspot-utils" "^0.1.3" + "@helium/onboarding" "^4.10.0" + "@helium/proto-ble" "^4.8.1" + "@helium/spl-utils" "^0.1.3" + "@helium/voter-stake-registry-sdk" "^0.0.26" + "@metaplex-foundation/beet" "^0.7.1" + "@metaplex-foundation/beet-solana" "^0.4.0" + "@metaplex-foundation/mpl-bubblegum" "^0.6.2" + "@metaplex-foundation/mpl-token-metadata" "^2.5.2" + "@pythnetwork/client" "^2.11.0" + "@solana/spl-token" "^0.3.6" + "@solana/web3.js" "^1.69.0" + assert "^2.0.0" + axios "^1.3.0" + axios-retry "^3.3.1" + bignumber.js "^9.1.1" + bn.js "^5.2.1" + bs58 "^5.0.0" + react-native-crypto "^2.2.0" + text-encoding-polyfill "^0.6.7" + typescript-collections "^1.3.3" "@helium/rewards-oracle-sdk@^0.2.17": version "0.2.17" @@ -2544,6 +2712,19 @@ bn.js "^5.2.0" bs58 "^4.0.1" +"@helium/spl-utils@^0.0.26": + version "0.0.26" + resolved "https://registry.yarnpkg.com/@helium/spl-utils/-/spl-utils-0.0.26.tgz#539adfa29b7c043296ea01af02896a7a37219ee9" + integrity sha512-P++DOlzoqNkgGgMIo0yH2NqLAxjVAjy9IoP1V7jV+w5VCZEzyqRn+VTmSOwaH2WMK6qSosk6ex1xOQa/yalmEA== + dependencies: + "@coral-xyz/anchor" "0.26.0" + "@helium/address" "^4.8.1" + "@solana/spl-token" "^0.3.6" + "@solana/web3.js" "^1.43.4" + bn.js "^5.2.0" + borsh "^0.7.0" + bs58 "^4.0.1" + "@helium/spl-utils@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@helium/spl-utils/-/spl-utils-0.1.2.tgz#e12b924bf4bd3217f265250a2720cb7ed2316d1d" @@ -2558,6 +2739,20 @@ borsh "^0.7.0" bs58 "^5.0.0" +"@helium/spl-utils@^0.1.3", "@helium/spl-utils@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@helium/spl-utils/-/spl-utils-0.1.4.tgz#214b6076e76d2cd0095758ed3db33f0824048df3" + integrity sha512-QhEhJuOd9P8GbUKx5f9zI1m2zjN9si/IrAlDQk4gkFBDFsi4szzY03rj4CwyhmwIYJk/qi1b4JiMoRIinFutJg== + dependencies: + "@coral-xyz/anchor" "0.26.0" + "@helium/address" "^4.8.1" + "@solana/spl-account-compression" "^0.1.7" + "@solana/spl-token" "^0.3.6" + "@solana/web3.js" "^1.43.4" + bn.js "^5.2.0" + borsh "^0.7.0" + bs58 "^5.0.0" + "@helium/spl-utils@^0.2.17": version "0.2.17" resolved "https://registry.yarnpkg.com/@helium/spl-utils/-/spl-utils-0.2.17.tgz#150a7942039a844a3c11368fc5f93002db7dd708" @@ -2599,6 +2794,19 @@ bn.js "^5.2.0" bs58 "^4.0.1" +"@helium/treasury-management-sdk@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@helium/treasury-management-sdk/-/treasury-management-sdk-0.1.4.tgz#6d3ad274c3d3a7209ebeca6f901f9356e62c973a" + integrity sha512-w7hUTsP+kMMH5f0M/0VqOQ2KzdRACuY5qDHPt4X7VvjgjWFnps/mIHBXV1P2hG2YZDN9CiCSMwwjT9MFHISUiA== + dependencies: + "@coral-xyz/anchor" "0.26.0" + "@helium/circuit-breaker-sdk" "^0.1.4" + "@helium/idls" "^0.1.1" + "@helium/spl-utils" "^0.1.4" + "@solana/spl-token" "^0.3.6" + bn.js "^5.2.0" + bs58 "^4.0.1" + "@helium/treasury-management-sdk@^0.2.17": version "0.2.17" resolved "https://registry.yarnpkg.com/@helium/treasury-management-sdk/-/treasury-management-sdk-0.2.17.tgz#b5495352e948aa576ea2a24d68406d921c37079f" @@ -2624,6 +2832,32 @@ bn.js "^5.2.0" bs58 "^4.0.1" +"@helium/voter-stake-registry-sdk@^0.0.26": + version "0.0.26" + resolved "https://registry.yarnpkg.com/@helium/voter-stake-registry-sdk/-/voter-stake-registry-sdk-0.0.26.tgz#5b54ef6344e9b272850bdd29001c38902a43c7ed" + integrity sha512-pPB/YX6LIMuiZNBiQBTavlgspFHm35SNo2nbtk9CmGpgowLX0tVcGMcCV67Qa5c/s82vf9GCqsmdghVBh+GE/Q== + dependencies: + "@coral-xyz/anchor" "0.26.0" + "@helium/idls" "^0.0.26" + "@helium/spl-utils" "^0.0.26" + "@metaplex-foundation/mpl-token-metadata" "^2.2.3" + "@solana/spl-token" "^0.3.6" + bn.js "^5.2.0" + bs58 "^4.0.1" + +"@helium/voter-stake-registry-sdk@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@helium/voter-stake-registry-sdk/-/voter-stake-registry-sdk-0.1.4.tgz#41d46d1b0364c710aff51df756ed5a2521bf96e7" + integrity sha512-8f+dWaS1IbSuybrvyvchuOd/NP9fCx8jCVyl02pKkURFZC0WdPckiaw+5kh2/y29nwwZJlVqdu7I7C2TR/6uyQ== + dependencies: + "@coral-xyz/anchor" "0.26.0" + "@helium/idls" "^0.1.1" + "@helium/spl-utils" "^0.1.4" + "@metaplex-foundation/mpl-token-metadata" "^2.2.3" + "@solana/spl-token" "^0.3.6" + bn.js "^5.2.0" + bs58 "^4.0.1" + "@helium/voter-stake-registry-sdk@^0.2.17": version "0.2.17" resolved "https://registry.yarnpkg.com/@helium/voter-stake-registry-sdk/-/voter-stake-registry-sdk-0.2.17.tgz#3411a60c25d687502ed2dc6fe8f5b0f4aa3c4046" @@ -3193,6 +3427,21 @@ bn.js "^5.2.0" js-sha3 "^0.8.0" +"@metaplex-foundation/mpl-bubblegum@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-bubblegum/-/mpl-bubblegum-0.6.2.tgz#e1b098ccef10899b0d759a03e3d4b1ae7bdc9f0c" + integrity sha512-4tF7/FFSNtpozuIGD7gMKcqK2D49eVXZ144xiowC5H1iBeu009/oj2m8Tj6n4DpYFKWJ2JQhhhk0a2q7x0Begw== + dependencies: + "@metaplex-foundation/beet" "0.7.1" + "@metaplex-foundation/beet-solana" "0.4.0" + "@metaplex-foundation/cusper" "^0.0.2" + "@metaplex-foundation/mpl-token-metadata" "^2.5.2" + "@solana/spl-account-compression" "^0.1.4" + "@solana/spl-token" "^0.1.8" + "@solana/web3.js" "^1.50.1" + bn.js "^5.2.0" + js-sha3 "^0.8.0" + "@metaplex-foundation/mpl-bubblegum@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-bubblegum/-/mpl-bubblegum-0.7.0.tgz#b34067ad4fe846ceb60e47e49f221ecf4730add7" @@ -3278,6 +3527,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== +"@noble/hashes@^1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@noble/secp256k1@^1.6.3": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -3373,6 +3627,15 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@pythnetwork/client@^2.11.0": + version "2.19.0" + resolved "https://registry.yarnpkg.com/@pythnetwork/client/-/client-2.19.0.tgz#4b3b4fccb402d93ff00c01beec633e6f2bac94fe" + integrity sha512-0VSQ0NqBOa5EtloXbOVYZ6Wpu8CLP3oaOKVTaUMSX/HXbB00S6G+xdwF7stxo6emgrAMopotx3icEVug5Lpomg== + dependencies: + "@coral-xyz/anchor" "^0.28.1-beta.1" + "@coral-xyz/borsh" "^0.28.0" + buffer "^6.0.1" + "@pythnetwork/client@^2.12.0", "@pythnetwork/client@^2.17.0": version "2.17.0" resolved "https://registry.yarnpkg.com/@pythnetwork/client/-/client-2.17.0.tgz#b155af06958f4b729bfee1c07130c556598cf168" @@ -3813,6 +4076,18 @@ js-sha3 "^0.8.0" typescript-collections "^1.3.3" +"@solana/spl-account-compression@^0.1.5": + version "0.1.10" + resolved "https://registry.yarnpkg.com/@solana/spl-account-compression/-/spl-account-compression-0.1.10.tgz#b3135ce89349d6090832b3b1d89095badd57e969" + integrity sha512-IQAOJrVOUo6LCgeWW9lHuXo6JDbi4g3/RkQtvY0SyalvSWk9BIkHHe4IkAzaQw8q/BxEVBIjz8e9bNYWIAESNw== + dependencies: + "@metaplex-foundation/beet" "^0.7.1" + "@metaplex-foundation/beet-solana" "^0.4.0" + bn.js "^5.2.1" + borsh "^0.7.0" + js-sha3 "^0.8.0" + typescript-collections "^1.3.3" + "@solana/spl-memo@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@solana/spl-memo/-/spl-memo-0.2.3.tgz#594a28c37b40c0e22143f38f71b4f56d1f5b24fd" @@ -3982,6 +4257,27 @@ rpc-websockets "^7.5.1" superstruct "^0.14.2" +"@solana/web3.js@^1.69.0": + version "1.78.4" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.78.4.tgz#e8ca9abe4ec2af5fc540c1d272efee24aaffedb3" + integrity sha512-up5VG1dK+GPhykmuMIozJZBbVqpm77vbOG6/r5dS7NBGZonwHfTLdBbsYc3rjmaQ4DpCXUa3tUc4RZHRORvZrw== + dependencies: + "@babel/runtime" "^7.22.6" + "@noble/curves" "^1.0.0" + "@noble/hashes" "^1.3.1" + "@solana/buffer-layout" "^4.0.0" + agentkeepalive "^4.3.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.0" + node-fetch "^2.6.12" + rpc-websockets "^7.5.1" + superstruct "^0.14.2" + "@solana/web3.js@^1.73.0": version "1.76.0" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.76.0.tgz#0f888e25d727d0dadf3dd8a01967347555200b2b" @@ -5226,6 +5522,13 @@ agentkeepalive@^4.2.1: depd "^2.0.0" humanize-ms "^1.2.1" +agentkeepalive@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -5569,6 +5872,16 @@ assert@1.5.0: object-assign "^4.1.1" util "0.10.3" +assert@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32" + integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A== + dependencies: + es6-object-assign "^1.1.0" + is-nan "^1.2.1" + object-is "^1.0.1" + util "^0.12.0" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" @@ -5677,6 +5990,14 @@ axe-core@^4.4.3: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== +axios-retry@^3.3.1: + version "3.7.0" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.7.0.tgz#d5007755d257b97e08d846089976f838b9db9ef9" + integrity sha512-ZTnCkJbRtfScvwiRnoVskFAfvU0UG3xNcsjwTR0mawSbIJoothxn67gKsMaNAFHRXJ1RmuLhmZBzvyXi3+9WyQ== + dependencies: + "@babel/runtime" "^7.15.4" + is-retry-allowed "^2.2.0" + axios-retry@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.4.0.tgz#f464dbe9408e5aa78fa319afd38bb69b533d8854" @@ -5723,6 +6044,15 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" + integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -6005,6 +6335,11 @@ bignumber.js@9.1.1, bignumber.js@^9.0.0, bignumber.js@^9.0.1, bignumber.js@^9.0. resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== +bignumber.js@^9.1.1: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -7814,6 +8149,11 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es6-object-assign@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" + integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw== + es6-promise@^4.0.3: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -9806,6 +10146,14 @@ is-invalid-path@^0.1.0: dependencies: is-glob "^2.0.0" +is-nan@^1.2.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -10074,6 +10422,24 @@ jayson@^3.4.4: uuid "^8.3.2" ws "^7.4.5" +jayson@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.0.tgz#60dc946a85197317f2b1439d672a8b0a99cea2f9" + integrity sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + JSONStream "^1.3.5" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + uuid "^8.3.2" + ws "^7.4.5" + jest-changed-files@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" @@ -12062,6 +12428,13 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1.2.1, node-forge@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -13288,7 +13661,7 @@ react-native-config@1.4.6: resolved "https://registry.yarnpkg.com/react-native-config/-/react-native-config-1.4.6.tgz#2aefebf4d9cf02831e64bbc1307596bd212f6d42" integrity sha512-cSLdOfva2IPCxh6HjHN1IDVW9ratAvNnnAUx6ar2Byvr8KQU7++ysdFYPaoNVuJURuYoAKgvjab8ZcnwGZIO6Q== -react-native-crypto@2.2.0: +react-native-crypto@2.2.0, react-native-crypto@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/react-native-crypto/-/react-native-crypto-2.2.0.tgz#c999ed7c96064f830e1f958687f53d0c44025770" integrity sha512-eZu9Y8pa8BN9FU2pIex7MLRAi+Cd1Y6bsxfiufKh7sfraAACJvjQTeW7/zcQAT93WMfM+D0OVk+bubvkrbrUkw== @@ -13426,6 +13799,11 @@ react-native-pager-view@6.1.2: resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.2.tgz#3522079b9a9d6634ca5e8d153bc0b4d660254552" integrity sha512-qs2KSFc+7N7B+UZ6SG2sTvCkppagm5fVyRclv1KFKc7lDtrhXLzN59tXJw575LDP/dRJoXsNwqUAhZJdws6ABQ== +react-native-permissions@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-3.9.0.tgz#e3ba0460509dce6766e4728173dfa8bbf0886e0b" + integrity sha512-Z+PAx1Dld6ckQ4YecY+HvoKysbBBBLQEhawIOpPKZ8n2Pj+jy6KogRIAL00AaE2P76dEdPh6HGkWCDfR4k2siQ== + react-native-qrcode-svg@6.1.2: version "6.1.2" resolved "https://registry.yarnpkg.com/react-native-qrcode-svg/-/react-native-qrcode-svg-6.1.2.tgz#a7cb6c10199ab01418a7f7700ce17a6a014f544e" @@ -13812,6 +14190,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.2: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" @@ -15136,7 +15519,7 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -text-encoding-polyfill@0.6.7: +text-encoding-polyfill@0.6.7, text-encoding-polyfill@^0.6.7: version "0.6.7" resolved "https://registry.yarnpkg.com/text-encoding-polyfill/-/text-encoding-polyfill-0.6.7.tgz#4d27de0153e4c86eb2631ffd74c2f3f57969a9ec" integrity sha512-/DZ1XJqhbqRkCop6s9ZFu8JrFRwmVuHg4quIRm+ziFkR3N3ec6ck6yBvJ1GYeEQZhLVwRW0rZE+C3SSJpy0RTg== @@ -15697,7 +16080,7 @@ util@^0.10.3: dependencies: inherits "2.0.3" -util@^0.12.4: +util@^0.12.0, util@^0.12.4: version "0.12.5" resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== From 970fae62e1fd87d17483335acd1ff71357d893bf Mon Sep 17 00:00:00 2001 From: Chewing Glass Date: Mon, 11 Sep 2023 10:11:06 -0500 Subject: [PATCH 2/8] Add index screen of onboarding --- .../collectables/CollectablesNavigator.tsx | 8 +- src/features/collectables/HotspotList.tsx | 2 +- .../collectables/collectablesTypes.ts | 2 +- .../hotspot-onboarding/AddGatewayBle.tsx | 242 ------------------ .../hotspot-onboarding/Diagnostics.tsx | 54 ---- .../hotspot-onboarding/HotspotBLENav.tsx | 49 ---- .../hotspot-onboarding/ScanHotspots.tsx | 227 ---------------- .../hotspot-onboarding/WifiSettings.tsx | 176 ------------- src/features/hotspot-onboarding/WifiSetup.tsx | 90 ------- src/features/hotspot-onboarding/navTypes.tsx | 24 +- src/locales/en.ts | 8 +- 11 files changed, 27 insertions(+), 855 deletions(-) delete mode 100644 src/features/hotspot-onboarding/AddGatewayBle.tsx delete mode 100644 src/features/hotspot-onboarding/Diagnostics.tsx delete mode 100644 src/features/hotspot-onboarding/HotspotBLENav.tsx delete mode 100644 src/features/hotspot-onboarding/ScanHotspots.tsx delete mode 100644 src/features/hotspot-onboarding/WifiSettings.tsx delete mode 100644 src/features/hotspot-onboarding/WifiSetup.tsx diff --git a/src/features/collectables/CollectablesNavigator.tsx b/src/features/collectables/CollectablesNavigator.tsx index 4c92d72f3..9d2062e42 100644 --- a/src/features/collectables/CollectablesNavigator.tsx +++ b/src/features/collectables/CollectablesNavigator.tsx @@ -21,7 +21,7 @@ import CollectionScreen from './CollectionScreen' import NftDetailsScreen from './NftDetailsScreen' import AntennaSetupScreen from './AntennaSetupScreen' import SettingUpAntennaScreen from './SettingUpAntennaScreen' -import HotspotBLENav from '../hotspot-onboarding/HotspotBLENav' +import OnboardingNav from '../hotspot-onboarding/OnboardingNav' const CollectablesStack = createStackNavigator() @@ -115,9 +115,9 @@ const CollectablesStackScreen = () => { component={TransferCompleteScreen} /> diff --git a/src/features/collectables/HotspotList.tsx b/src/features/collectables/HotspotList.tsx index 59566adb5..f5f027bda 100644 --- a/src/features/collectables/HotspotList.tsx +++ b/src/features/collectables/HotspotList.tsx @@ -150,7 +150,7 @@ const HotspotList = () => { }, [navigation]) const handleNavigateToHotspotOnboard = useCallback(() => { - navigation.navigate('HotspotBLENavigator') + navigation.navigate('OnboardingNavigator') }, [navigation]) const toggleFiltersOpen = useCallback( diff --git a/src/features/collectables/collectablesTypes.ts b/src/features/collectables/collectablesTypes.ts index d542b9147..8b8e176a1 100644 --- a/src/features/collectables/collectablesTypes.ts +++ b/src/features/collectables/collectablesTypes.ts @@ -47,7 +47,7 @@ export type CollectableStackParamList = { PaymentQrScanner: undefined AddressBookNavigator: undefined ScanAddress: undefined - HotspotBLENavigator: undefined + OnboardingNavigator: undefined } export type CollectableNavigationProp = diff --git a/src/features/hotspot-onboarding/AddGatewayBle.tsx b/src/features/hotspot-onboarding/AddGatewayBle.tsx deleted file mode 100644 index 65b9533c6..000000000 --- a/src/features/hotspot-onboarding/AddGatewayBle.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import BackButton from '@components/BackButton' -import ButtonPressable from '@components/ButtonPressable' -import CircleLoader from '@components/CircleLoader' -import SafeAreaBox from '@components/SafeAreaBox' -import Text from '@components/Text' -import { - init, - iotInfoKey, - keyToAssetKey, - rewardableEntityConfigKey, -} from '@helium/helium-entity-manager-sdk' -import { - AddGatewayV1, - useHotspotBle, - useOnboarding, -} from '@helium/react-native-sdk' -import { - bufferToTransaction, - heliumAddressToSolAddress, - sendAndConfirmWithRetry, -} from '@helium/spl-utils' -import { useNavigation } from '@react-navigation/native' -import { LAMPORTS_PER_SOL, PublicKey, Transaction } from '@solana/web3.js' -import { useAccountStorage } from '@storage/AccountStorageProvider' -import { DAO_KEY, IOT_SUB_DAO_KEY } from '@utils/constants' -import { getHotspotWithRewards } from '@utils/solanaUtils' -import { Buffer } from 'buffer' -import React, { useCallback } from 'react' -import { useAsyncCallback } from 'react-async-hook' -import { useTranslation } from 'react-i18next' -import { Alert } from 'react-native' -import { TabBarNavigationProp } from '../../navigation/rootTypes' -import { useSolana } from '../../solana/SolanaProvider' -import { CollectableNavigationProp } from '../collectables/collectablesTypes' -import { HotspotBleNavProp } from './navTypes' - -const AddGatewayBle = () => { - const { getOnboardingRecord, getOnboardTransactions, onboardingClient } = - useOnboarding() - const { createGatewayTxn, getOnboardingAddress } = useHotspotBle() - const { currentAccount } = useAccountStorage() - const { anchorProvider } = useSolana() - const { t } = useTranslation() - const navigation = useNavigation() - const tabNav = useNavigation() - const collectNav = useNavigation() - const onBack = useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } - }, [navigation]) - - const { - execute: handleAddGateway, - loading, - error, - } = useAsyncCallback(async () => { - if (!anchorProvider) { - Alert.alert('Error', 'No anchor provider') - return - } - const accountAddress = currentAccount?.address - if (!accountAddress) { - Alert.alert( - 'Error', - 'You must first add a wallet address from the main menu', - ) - return - } - - const onboardAddress = await getOnboardingAddress() - const onboardRecord = await getOnboardingRecord(onboardAddress) - - if (!onboardRecord) { - throw new Error( - `This hotspot does not exist in the onboarding server. Contact your manufacturer to have them approve hotspot with id ${onboardAddress}`, - ) - } - - if (!onboardRecord?.maker.address) { - throw new Error('Could not get maker address') - } - const makerSolAddr = heliumAddressToSolAddress(onboardRecord?.maker.address) - const makerSolBalance = ( - await anchorProvider.connection.getAccountInfo( - new PublicKey(makerSolAddr), - ) - )?.lamports - if ( - !makerSolBalance || - makerSolBalance / LAMPORTS_PER_SOL < 0.00089088 + 0.00001 - ) { - throw new Error( - `Manufacturer ${onboardRecord?.maker.name} does not have enough SOL to onboard this hotspot. Please contact the manufacturer to resolve this issue.`, - ) - } - - const txnStr = await createGatewayTxn({ - ownerAddress: accountAddress, - payerAddress: onboardRecord?.maker.address, - }) - const tx = AddGatewayV1.fromString(txnStr) - if (!tx?.gateway?.b58) { - throw new Error('Error signing gateway txn') - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function wrapProgramError(e: any) { - if ( - e.toString().includes('Insufficient Balance') || - e.toString().includes('"Custom":1') || - e.InstructionError[1].Custom === 1 - ) { - throw new Error( - `Manufacturer ${onboardRecord?.maker.name} does not have enough SOL or Data Credits to onboard this hotspot. Please contact the manufacturer to resolve this issue.`, - ) - } - if (e.InstructionError) { - throw new Error(`Program Error: ${JSON.stringify(e)}`) - } - throw e - } - - const createTxns = await onboardingClient.createHotspot({ - transaction: txnStr, - }) - - const createHotspotTxns = createTxns.data?.solanaTransactions?.map( - (createHotspotTx) => Buffer.from(createHotspotTx), - ) - try { - // eslint-disable-next-line no-restricted-syntax - for (const txn of createHotspotTxns || []) { - // eslint-disable-next-line no-await-in-loop - await sendAndConfirmWithRetry( - anchorProvider.connection, - txn, - { - skipPreflight: true, - }, - 'confirmed', - ) - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - wrapProgramError(e) - } - - const hemProgram = await init(anchorProvider) - const [iotConfigKey] = rewardableEntityConfigKey(IOT_SUB_DAO_KEY, 'IOT') - const iotInfo = await hemProgram.account.iotHotspotInfoV0.fetchNullable( - iotInfoKey(iotConfigKey, tx?.gateway?.b58)[0], - ) - if (!iotInfo) { - const { solanaTransactions } = await getOnboardTransactions({ - hotspotAddress: onboardAddress, - hotspotTypes: ['iot'], - }) - let solanaSignedTransactions: Transaction[] | undefined - - if (solanaTransactions) { - solanaSignedTransactions = - await anchorProvider?.wallet.signAllTransactions( - solanaTransactions.map((txn) => { - return bufferToTransaction(Buffer.from(txn, 'base64')) - }), - ) - } - - if (solanaSignedTransactions) { - try { - // eslint-disable-next-line no-restricted-syntax - for (const txn of solanaSignedTransactions) { - // eslint-disable-next-line no-await-in-loop - await sendAndConfirmWithRetry( - anchorProvider.connection, - txn.serialize(), - { - skipPreflight: true, - }, - 'confirmed', - ) - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - wrapProgramError(e) - } - } - } - - const keyToAssetK = keyToAssetKey(DAO_KEY, tx.gateway.b58, 'b58')[0] - const keyToAsset = await hemProgram.account.keyToAssetV0.fetch(keyToAssetK) - const { asset } = keyToAsset - const collectable = await getHotspotWithRewards(asset, anchorProvider) - collectNav.navigate( - iotInfo ? 'HotspotDetailsScreen' : 'AssertLocationScreen', - { collectable }, - ) - }) - - return ( - - - {t('hotspotOnboarding.onboarding.title')} - - {t('hotspotOnboarding.onboarding.subtitle')} - - {loading && } - {error && ( - - {error.message ? error.message.toString() : error.toString()} - - )} - - tabNav.push('Collectables')} - /> - - ) -} - -export default AddGatewayBle diff --git a/src/features/hotspot-onboarding/Diagnostics.tsx b/src/features/hotspot-onboarding/Diagnostics.tsx deleted file mode 100644 index 487a21255..000000000 --- a/src/features/hotspot-onboarding/Diagnostics.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import BackButton from '@components/BackButton' -import ButtonPressable from '@components/ButtonPressable' -import SafeAreaBox from '@components/SafeAreaBox' -import Text from '@components/Text' -import { DiagnosticInfo, useHotspotBle } from '@helium/react-native-sdk' -import { useNavigation } from '@react-navigation/native' -import React, { useCallback, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import type { HotspotBleNavProp } from './navTypes' - -const Diagnostics = () => { - const navigation = useNavigation() - const [diagnosticInfo, setDiagnosticInfo] = useState() - const { getDiagnosticInfo } = useHotspotBle() - const { t } = useTranslation() - const onBack = useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } - }, [navigation]) - const navNext = useCallback( - () => navigation.push('AddGatewayBle'), - [navigation], - ) - - useEffect(() => { - getDiagnosticInfo() - .then(setDiagnosticInfo) - .catch(() => navNext()) - }, [getDiagnosticInfo, navNext]) - - return ( - - - {t('hotspotOnboarding.diagnostics.title')} - {diagnosticInfo && ( - - {JSON.stringify(diagnosticInfo, null, 2)} - - )} - - - ) -} - -export default Diagnostics diff --git a/src/features/hotspot-onboarding/HotspotBLENav.tsx b/src/features/hotspot-onboarding/HotspotBLENav.tsx deleted file mode 100644 index 3519880aa..000000000 --- a/src/features/hotspot-onboarding/HotspotBLENav.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { HotspotBleProvider } from '@helium/react-native-sdk' -import { - createNativeStackNavigator, - NativeStackNavigationOptions, -} from '@react-navigation/native-stack' -import * as React from 'react' -import AddGatewayBle from './AddGatewayBle' -import Diagnostics from './Diagnostics' -import ScanHotspots from './ScanHotspots' -import WifiSettings from './WifiSettings' -import WifiSetup from './WifiSetup' - -const Stack = createNativeStackNavigator() - -const screenOptions = { headerShown: false } as NativeStackNavigationOptions - -export default React.memo(function HotspotBLENav() { - return ( - - - - - - - - - - ) -}) diff --git a/src/features/hotspot-onboarding/ScanHotspots.tsx b/src/features/hotspot-onboarding/ScanHotspots.tsx deleted file mode 100644 index d3042af28..000000000 --- a/src/features/hotspot-onboarding/ScanHotspots.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import BackButton from '@components/BackButton' -import ButtonPressable from '@components/ButtonPressable' -import CircleLoader from '@components/CircleLoader' -import FabButton from '@components/FabButton' -import SafeAreaBox from '@components/SafeAreaBox' -import Text from '@components/Text' -import TouchableOpacityBox from '@components/TouchableOpacityBox' -import { Device, useHotspotBle } from '@helium/react-native-sdk' -import { useNavigation } from '@react-navigation/native' -import React, { useCallback, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { FlatList, Platform } from 'react-native' -import { - PERMISSIONS, - PermissionStatus, - RESULTS, - check, - request, -} from 'react-native-permissions' -import * as Logger from '../../utils/logger' -import type { HotspotBleNavProp } from './navTypes' - -const ScanHotspots = () => { - const { startScan, stopScan, connect, scannedDevices } = useHotspotBle() - const [scanning, setScanning] = useState(false) - const [canScan, setCanScan] = useState(undefined) - const navigation = useNavigation() - const { t } = useTranslation() - const onBack = useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } - }, [navigation]) - const [error, setError] = useState(undefined) - - const showError = (e: any) => { - Logger.error(e) - setError(e.toString()) - } - - const updateCanScan = useCallback((result: PermissionStatus) => { - switch (result) { - case RESULTS.UNAVAILABLE: - case RESULTS.BLOCKED: - case RESULTS.DENIED: - case RESULTS.LIMITED: - setCanScan(false) - break - case RESULTS.GRANTED: - setCanScan(true) - break - } - }, []) - - useEffect(() => { - if (Platform.OS === 'ios') { - setCanScan(true) - return - } - - check(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION) - .then(updateCanScan) - .catch(showError) - }, [updateCanScan]) - - useEffect(() => { - if (canScan !== false) return - - request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION) - .then(updateCanScan) - .catch(showError) - }, [canScan, updateCanScan]) - - const handleScanPress = useCallback(() => { - const shouldScan = !scanning - setScanning(shouldScan) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let timeout: any | undefined - if (shouldScan) { - setError(undefined) - timeout = setTimeout(() => { - stopScan() - setScanning(false) - if (scannedDevices.length === 0) { - setError( - 'No hotspots found. Please ensure bluetooth pairing is enabled', - ) - } - }, 30 * 1000) - } - - if (shouldScan) { - startScan((e) => { - if (e) { - showError(e) - } - }) - } else { - stopScan() - } - return () => { - if (timeout) { - clearTimeout(timeout) - } - } - }, [scannedDevices.length, scanning, startScan, stopScan]) - - const navNext = useCallback( - () => navigation.push('WifiSettings'), - [navigation], - ) - - const [connecting, setConnecting] = useState(false) - const connectDevice = useCallback( - (d: Device) => async () => { - try { - setConnecting(true) - await connect(d) - if (scanning) { - stopScan() - setScanning(false) - } - setConnecting(false) - navNext() - } catch (e) { - showError(e) - } finally { - setConnecting(false) - } - }, - [connect, navNext, scanning, stopScan], - ) - - const renderItem = React.useCallback( - // eslint-disable-next-line react/no-unused-prop-types - ({ item }: { item: Device }) => { - return ( - - - - {item.name} - - - ) - }, - [connectDevice, connecting], - ) - - const keyExtractor = React.useCallback(({ id }: Device) => id, []) - - return ( - - - {t('hotspotOnboarding.scan.title')} - - {t('hotspotOnboarding.scan.subtitle')} - - {scannedDevices.length === 0 && scanning && } - {scannedDevices.length === 0 && scanning && ( - - {t('hotspotOnboarding.scan.scanning')} - - )} - - - {error && ( - - {error} - - )} - - - ) -} - -export default ScanHotspots diff --git a/src/features/hotspot-onboarding/WifiSettings.tsx b/src/features/hotspot-onboarding/WifiSettings.tsx deleted file mode 100644 index d2822e4a4..000000000 --- a/src/features/hotspot-onboarding/WifiSettings.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import BackButton from '@components/BackButton' -import ButtonPressable from '@components/ButtonPressable' -import FabButton from '@components/FabButton' -import SafeAreaBox from '@components/SafeAreaBox' -import Text from '@components/Text' -import TouchableOpacityBox from '@components/TouchableOpacityBox' -import { useHotspotBle } from '@helium/react-native-sdk' -import { useNavigation } from '@react-navigation/native' -import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { Alert, SectionList } from 'react-native' -import type { HotspotBleNavProp } from './navTypes' - -type Section = { - title: string - data: string[] - type: 'configured' | 'available' -} - -const WifiSettings = () => { - const navigation = useNavigation() - const { t } = useTranslation() - const onBack = useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } - }, [navigation]) - const navNext = useCallback( - () => navigation.push('AddGatewayBle'), - [navigation], - ) - const [networks, setNetworks] = useState() - const [configuredNetworks, setConfiguredNetworks] = useState() - const [connected, setConnected] = useState(false) - - const { isConnected, readWifiNetworks, removeConfiguredWifi } = - useHotspotBle() - - useEffect(() => { - isConnected().then(setConnected) - }, [isConnected]) - - useEffect(() => { - if (!connected) return - - readWifiNetworks(true).then(setConfiguredNetworks) - readWifiNetworks(false).then(setNetworks) - }, [connected, readWifiNetworks]) - - const handleNetworkSelected = useCallback( - ({ - network, - type, - }: { - network: string - type: 'configured' | 'available' - }) => - async () => { - if (type === 'available') { - navigation.push('WifiSetup', { network }) - } else { - Alert.alert( - t('hotspotOnboarding.wifiSettings.title'), - t('hotspotOnboarding.wifiSettings.remove', { network }), - [ - { - text: t('generic.cancel'), - style: 'default', - }, - { - text: t('generic.remove'), - style: 'destructive', - onPress: async () => { - setConfiguredNetworks( - configuredNetworks?.filter((n) => n !== network), - ) - await removeConfiguredWifi(network) - readWifiNetworks(true).then(setConfiguredNetworks) - readWifiNetworks(false).then(setNetworks) - }, - }, - ], - ) - } - }, - [configuredNetworks, navigation, readWifiNetworks, removeConfiguredWifi, t], - ) - - const renderItem = useCallback( - ({ - item: network, - section: { type }, - }: { - // eslint-disable-next-line react/no-unused-prop-types - item: string - // eslint-disable-next-line react/no-unused-prop-types - section: Section - }) => { - return ( - - - - {network} - - - ) - }, - [handleNetworkSelected], - ) - - const keyExtractor = useCallback((name: string) => name, []) - - const renderSectionHeader = ({ - section: { title }, - }: { - section: Section - }) => ( - - {title} - - ) - - const sections = useMemo( - (): Section[] => [ - { - data: configuredNetworks || [], - title: t('hotspotOnboarding.wifiSettings.configured'), - type: 'configured', - }, - { - data: networks || [], - title: t('hotspotOnboarding.wifiSettings.available'), - type: 'available', - }, - ], - [configuredNetworks, networks, t], - ) - - return ( - - - - {t('hotspotOnboarding.wifiSettings.title')} - - - - - ) -} - -export default WifiSettings diff --git a/src/features/hotspot-onboarding/WifiSetup.tsx b/src/features/hotspot-onboarding/WifiSetup.tsx deleted file mode 100644 index 25c6f3d24..000000000 --- a/src/features/hotspot-onboarding/WifiSetup.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import BackButton from '@components/BackButton' -import Box from '@components/Box' -import ButtonPressable from '@components/ButtonPressable' -import SafeAreaBox from '@components/SafeAreaBox' -import Text from '@components/Text' -import TextInput from '@components/TextInput' -import { BleError, useHotspotBle } from '@helium/react-native-sdk' -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' -import React, { useCallback, useState } from 'react' -import { useTranslation } from 'react-i18next' -import type { HotspotBLEStackParamList, HotspotBleNavProp } from './navTypes' - -type Route = RouteProp -const WifiSetup = () => { - const { - params: { network }, - } = useRoute() - const [secureTextEntry, setSecureTextEntry] = useState(true) - const [loading, setLoading] = useState(false) - const [status, setStatus] = useState('') - const [password, setPassword] = useState('') - const { setWifi } = useHotspotBle() - const { t } = useTranslation() - const navigation = useNavigation() - const onBack = useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } - }, [navigation]) - - const toggleSecureEntry = useCallback(() => { - setSecureTextEntry(!secureTextEntry) - }, [secureTextEntry]) - - const handleSetWifi = useCallback(async () => { - setLoading(true) - try { - const nextStatus = await setWifi(network, password) - setStatus(nextStatus) - } catch (e) { - if (typeof e === 'string') { - setStatus(e) - } else { - setStatus((e as BleError).toString()) - } - } - setLoading(false) - }, [network, password, setWifi]) - - return ( - - - {network} - - - - - - - {loading ? 'loading...' : status} - - ) -} - -export default WifiSetup diff --git a/src/features/hotspot-onboarding/navTypes.tsx b/src/features/hotspot-onboarding/navTypes.tsx index 0ef6ba4c5..0497add9b 100644 --- a/src/features/hotspot-onboarding/navTypes.tsx +++ b/src/features/hotspot-onboarding/navTypes.tsx @@ -1,12 +1,20 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' -export type HotspotBLEStackParamList = { - ScanHotspots: undefined - WifiSettings: undefined - WifiSetup: { network: string } - AddGatewayBle: undefined - Diagnostics: undefined +export type IotBleOptions = { + bleInstructions?: string } -export type HotspotBleNavProp = - NativeStackNavigationProp +export type OnboardableDevice = { + name: string + type: 'IotBle' + image: string + options: IotBleOptions +} + +export type OnboardingtackParamList = { + IotBle: IotBleOptions + SelectDevice: undefined +} + +export type OnboardingNavProp = + NativeStackNavigationProp diff --git a/src/locales/en.ts b/src/locales/en.ts index e80c0e29c..a6a78186c 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -34,15 +34,17 @@ export default { configured: 'Configured Networks', setup: 'Setup Wifi', }, - diagnostics: { - title: 'Diagnostics', - }, onboarding: { title: 'Onboarding', subtitle: 'Onboard your hotspot to the IOT network. After onboarding this hotspot, you will be able to set the location and antenna details.', onboard: 'Onboard Hotspot', }, + selectDevice: { + title: 'Select Device', + subtitle: + "Select your Hotspot to continue. Don't see your device? Your manufacturer may have their own app for onboarding the hotspot. Check the instructions. You can also try onboarding it as a Generic Bluetooth Enabled Hotspot", + }, }, accountImport: { accountLimit: From 1079c240525c3dda05e0b2eb0c185f92f6cce922 Mon Sep 17 00:00:00 2001 From: Chewing Glass Date: Mon, 11 Sep 2023 10:11:19 -0500 Subject: [PATCH 3/8] Add index screen of onboarding --- .../hotspot-onboarding/OnboardingNav.tsx | 31 +++ .../hotspot-onboarding/SelectDevice.tsx | 94 +++++++ .../iot-ble/AddGatewayBle.tsx | 232 ++++++++++++++++++ .../iot-ble/HotspotBLENav.tsx | 51 ++++ .../iot-ble/ScanHotspots.tsx | 221 +++++++++++++++++ .../iot-ble/WifiSettings.tsx | 166 +++++++++++++ .../hotspot-onboarding/iot-ble/WifiSetup.tsx | 88 +++++++ .../hotspot-onboarding/iot-ble/navTypes.tsx | 13 + .../iot-ble/optionsContext.tsx | 22 ++ 9 files changed, 918 insertions(+) create mode 100644 src/features/hotspot-onboarding/OnboardingNav.tsx create mode 100644 src/features/hotspot-onboarding/SelectDevice.tsx create mode 100644 src/features/hotspot-onboarding/iot-ble/AddGatewayBle.tsx create mode 100644 src/features/hotspot-onboarding/iot-ble/HotspotBLENav.tsx create mode 100644 src/features/hotspot-onboarding/iot-ble/ScanHotspots.tsx create mode 100644 src/features/hotspot-onboarding/iot-ble/WifiSettings.tsx create mode 100644 src/features/hotspot-onboarding/iot-ble/WifiSetup.tsx create mode 100644 src/features/hotspot-onboarding/iot-ble/navTypes.tsx create mode 100644 src/features/hotspot-onboarding/iot-ble/optionsContext.tsx diff --git a/src/features/hotspot-onboarding/OnboardingNav.tsx b/src/features/hotspot-onboarding/OnboardingNav.tsx new file mode 100644 index 000000000..d11673be8 --- /dev/null +++ b/src/features/hotspot-onboarding/OnboardingNav.tsx @@ -0,0 +1,31 @@ +import { HotspotBleProvider } from '@helium/react-native-sdk' +import { + createNativeStackNavigator, + NativeStackNavigationOptions, +} from '@react-navigation/native-stack' +import * as React from 'react' +import HotspotBLENav from './iot-ble/HotspotBLENav' +import SelectDevice from './SelectDevice' + +const Stack = createNativeStackNavigator() + +const screenOptions = { headerShown: false } as NativeStackNavigationOptions + +export default React.memo(function OnboardingNav() { + return ( + + + + + + + ) +}) diff --git a/src/features/hotspot-onboarding/SelectDevice.tsx b/src/features/hotspot-onboarding/SelectDevice.tsx new file mode 100644 index 000000000..253bd7ddd --- /dev/null +++ b/src/features/hotspot-onboarding/SelectDevice.tsx @@ -0,0 +1,94 @@ +import BackScreen from '@components/BackScreen' +import ImageBox from '@components/ImageBox' +import Text from '@components/Text' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import { useNavigation } from '@react-navigation/native' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList } from 'react-native' +import { OnboardableDevice, OnboardingNavProp } from './navTypes' + +const data: OnboardableDevice[] = [ + { + name: 'Rak V2', + type: 'IotBle', + image: + 'https://cdn.shopify.com/s/files/1/0177/8784/6756/t/58/assets/pf-0b98ac45--pfa8f02b76untitled106.png?v=1611822325', + options: { + bleInstructions: + 'Power on your hotspot. Then press the button on the side of the hotspot to enable BLE.', + }, + }, + { + name: 'Generic Bluetooth Enabled Hotspot', + type: 'IotBle', + image: + 'https://shdw-drive.genesysgo.net/6tcnBSybPG7piEDShBcrVtYJDPSvGrDbVvXmXKpzBvWP/hotspot.png', + options: { + bleInstructions: + 'Power on your hotspot. Depending on your hotspot, there is either a button to enable BLE, or BLE will be enabled for the first 5 minutes after powering on.', + }, + }, +] + +const SelectOnboardableDevice = () => { + const { t } = useTranslation() + const navigation = useNavigation() + const renderItem = React.useCallback( + // eslint-disable-next-line react/no-unused-prop-types + ({ item, index }: { item: OnboardableDevice; index: number }) => { + return ( + { + navigation.push(item.type, item.options) + }} + alignItems="center" + padding="s" + flexDirection="row" + borderTopWidth={index === 0 ? 0 : 1} + borderColor="grey900" + borderBottomWidth={1} + > + + + {item.name} + + + ) + }, + [navigation], + ) + + const keyExtractor = React.useCallback( + ({ name }: OnboardableDevice) => name, + [], + ) + + return ( + + + {t('hotspotOnboarding.selectDevice.subtitle')} + + + + ) +} + +export default SelectOnboardableDevice diff --git a/src/features/hotspot-onboarding/iot-ble/AddGatewayBle.tsx b/src/features/hotspot-onboarding/iot-ble/AddGatewayBle.tsx new file mode 100644 index 000000000..bfb3c2ab4 --- /dev/null +++ b/src/features/hotspot-onboarding/iot-ble/AddGatewayBle.tsx @@ -0,0 +1,232 @@ +import BackScreen from '@components/BackScreen' +import ButtonPressable from '@components/ButtonPressable' +import CircleLoader from '@components/CircleLoader' +import Text from '@components/Text' +import { + init, + iotInfoKey, + keyToAssetKey, + rewardableEntityConfigKey, +} from '@helium/helium-entity-manager-sdk' +import { + AddGatewayV1, + useHotspotBle, + useOnboarding, +} from '@helium/react-native-sdk' +import { + bufferToTransaction, + heliumAddressToSolAddress, + sendAndConfirmWithRetry, +} from '@helium/spl-utils' +import { useNavigation } from '@react-navigation/native' +import { LAMPORTS_PER_SOL, PublicKey, Transaction } from '@solana/web3.js' +import { useAccountStorage } from '@storage/AccountStorageProvider' +import { DAO_KEY, IOT_SUB_DAO_KEY } from '@utils/constants' +import { getHotspotWithRewards } from '@utils/solanaUtils' +import { Buffer } from 'buffer' +import React from 'react' +import { useAsyncCallback } from 'react-async-hook' +import { useTranslation } from 'react-i18next' +import { Alert } from 'react-native' +import { TabBarNavigationProp } from '../../../navigation/rootTypes' +import { useSolana } from '../../../solana/SolanaProvider' +import { CollectableNavigationProp } from '../../collectables/collectablesTypes' + +const AddGatewayBle = () => { + const { getOnboardingRecord, getOnboardTransactions, onboardingClient } = + useOnboarding() + const { createGatewayTxn, getOnboardingAddress } = useHotspotBle() + const { currentAccount } = useAccountStorage() + const { anchorProvider } = useSolana() + const { t } = useTranslation() + const tabNav = useNavigation() + const collectNav = useNavigation() + + const { + execute: handleAddGateway, + loading, + error, + } = useAsyncCallback(async () => { + if (!anchorProvider) { + Alert.alert('Error', 'No anchor provider') + return + } + const accountAddress = currentAccount?.address + if (!accountAddress) { + Alert.alert( + 'Error', + 'You must first add a wallet address from the main menu', + ) + return + } + + const onboardAddress = await getOnboardingAddress() + const onboardRecord = await getOnboardingRecord(onboardAddress) + + if (!onboardRecord) { + throw new Error( + `This hotspot does not exist in the onboarding server. Contact your manufacturer to have them approve hotspot with id ${onboardAddress}`, + ) + } + + if (!onboardRecord?.maker.address) { + throw new Error('Could not get maker address') + } + const makerSolAddr = heliumAddressToSolAddress(onboardRecord?.maker.address) + const makerSolBalance = ( + await anchorProvider.connection.getAccountInfo( + new PublicKey(makerSolAddr), + ) + )?.lamports + if ( + !makerSolBalance || + makerSolBalance / LAMPORTS_PER_SOL < 0.00089088 + 0.00001 + ) { + throw new Error( + `Manufacturer ${onboardRecord?.maker.name} does not have enough SOL to onboard this hotspot. Please contact the manufacturer to resolve this issue.`, + ) + } + + const txnStr = await createGatewayTxn({ + ownerAddress: accountAddress, + payerAddress: onboardRecord?.maker.address, + }) + const tx = AddGatewayV1.fromString(txnStr) + if (!tx?.gateway?.b58) { + throw new Error('Error signing gateway txn') + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function wrapProgramError(e: any) { + if ( + e.toString().includes('Insufficient Balance') || + e.toString().includes('"Custom":1') || + e.InstructionError[1].Custom === 1 + ) { + throw new Error( + `Manufacturer ${onboardRecord?.maker.name} does not have enough SOL or Data Credits to onboard this hotspot. Please contact the manufacturer to resolve this issue.`, + ) + } + if (e.InstructionError) { + throw new Error(`Program Error: ${JSON.stringify(e)}`) + } + throw e + } + + const createTxns = await onboardingClient.createHotspot({ + transaction: txnStr, + }) + + const createHotspotTxns = createTxns.data?.solanaTransactions?.map( + (createHotspotTx) => Buffer.from(createHotspotTx), + ) + try { + // eslint-disable-next-line no-restricted-syntax + for (const txn of createHotspotTxns || []) { + // eslint-disable-next-line no-await-in-loop + await sendAndConfirmWithRetry( + anchorProvider.connection, + txn, + { + skipPreflight: true, + }, + 'confirmed', + ) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + wrapProgramError(e) + } + + const hemProgram = await init(anchorProvider) + const [iotConfigKey] = rewardableEntityConfigKey(IOT_SUB_DAO_KEY, 'IOT') + const iotInfo = await hemProgram.account.iotHotspotInfoV0.fetchNullable( + iotInfoKey(iotConfigKey, tx?.gateway?.b58)[0], + ) + if (!iotInfo) { + const { solanaTransactions } = await getOnboardTransactions({ + hotspotAddress: onboardAddress, + hotspotTypes: ['iot'], + }) + let solanaSignedTransactions: Transaction[] | undefined + + if (solanaTransactions) { + solanaSignedTransactions = + await anchorProvider?.wallet.signAllTransactions( + solanaTransactions.map((txn) => { + return bufferToTransaction(Buffer.from(txn, 'base64')) + }), + ) + } + + if (solanaSignedTransactions) { + try { + // eslint-disable-next-line no-restricted-syntax + for (const txn of solanaSignedTransactions) { + // eslint-disable-next-line no-await-in-loop + await sendAndConfirmWithRetry( + anchorProvider.connection, + txn.serialize(), + { + skipPreflight: true, + }, + 'confirmed', + ) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + wrapProgramError(e) + } + } + } + + const keyToAssetK = keyToAssetKey(DAO_KEY, tx.gateway.b58, 'b58')[0] + const keyToAsset = await hemProgram.account.keyToAssetV0.fetch(keyToAssetK) + const { asset } = keyToAsset + const collectable = await getHotspotWithRewards(asset, anchorProvider) + collectNav.navigate( + iotInfo ? 'HotspotDetailsScreen' : 'AssertLocationScreen', + { collectable }, + ) + }) + + return ( + + + {t('hotspotOnboarding.onboarding.subtitle')} + + {loading && } + {error && ( + + {error.message ? error.message.toString() : error.toString()} + + )} + + tabNav.push('Collectables')} + /> + + ) +} + +export default AddGatewayBle diff --git a/src/features/hotspot-onboarding/iot-ble/HotspotBLENav.tsx b/src/features/hotspot-onboarding/iot-ble/HotspotBLENav.tsx new file mode 100644 index 000000000..05f3d7e82 --- /dev/null +++ b/src/features/hotspot-onboarding/iot-ble/HotspotBLENav.tsx @@ -0,0 +1,51 @@ +import { HotspotBleProvider } from '@helium/react-native-sdk' +import { RouteProp, useRoute } from '@react-navigation/native' +import { + createNativeStackNavigator, + NativeStackNavigationOptions, +} from '@react-navigation/native-stack' +import * as React from 'react' +import { OnboardingtackParamList } from '../navTypes' +import AddGatewayBle from './AddGatewayBle' +import ScanHotspots from './ScanHotspots' +import WifiSettings from './WifiSettings' +import WifiSetup from './WifiSetup' +import { IotBleOptionsProvider } from './optionsContext' + +const Stack = createNativeStackNavigator() + +const screenOptions = { headerShown: false } as NativeStackNavigationOptions + +type Route = RouteProp +export default React.memo(function HotspotBLENav() { + const route = useRoute() + const iotParams = route.params + return ( + + + + + + + + + + + ) +}) diff --git a/src/features/hotspot-onboarding/iot-ble/ScanHotspots.tsx b/src/features/hotspot-onboarding/iot-ble/ScanHotspots.tsx new file mode 100644 index 000000000..88a17580b --- /dev/null +++ b/src/features/hotspot-onboarding/iot-ble/ScanHotspots.tsx @@ -0,0 +1,221 @@ +import BackScreen from '@components/BackScreen' +import ButtonPressable from '@components/ButtonPressable' +import CircleLoader from '@components/CircleLoader' +import FabButton from '@components/FabButton' +import Text from '@components/Text' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import { Device, useHotspotBle } from '@helium/react-native-sdk' +import { useNavigation } from '@react-navigation/native' +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList, Platform } from 'react-native' +import { + PERMISSIONS, + PermissionStatus, + RESULTS, + check, + request, +} from 'react-native-permissions' +import * as Logger from '../../../utils/logger' +import type { HotspotBleNavProp } from './navTypes' +import { useIotBleOptions } from './optionsContext' + +const ScanHotspots = () => { + const { startScan, stopScan, connect, scannedDevices } = useHotspotBle() + const [scanning, setScanning] = useState(false) + const { bleInstructions } = useIotBleOptions() + const [canScan, setCanScan] = useState(undefined) + const navigation = useNavigation() + const { t } = useTranslation() + const [error, setError] = useState(undefined) + + const showError = (e: any) => { + Logger.error(e) + setError(e.toString()) + } + + const updateCanScan = useCallback((result: PermissionStatus) => { + switch (result) { + case RESULTS.UNAVAILABLE: + case RESULTS.BLOCKED: + case RESULTS.DENIED: + case RESULTS.LIMITED: + setCanScan(false) + break + case RESULTS.GRANTED: + setCanScan(true) + break + } + }, []) + + useEffect(() => { + if (Platform.OS === 'ios') { + setCanScan(true) + return + } + + check(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION) + .then(updateCanScan) + .catch(showError) + }, [updateCanScan]) + + useEffect(() => { + if (canScan !== false) return + + request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION) + .then(updateCanScan) + .catch(showError) + }, [canScan, updateCanScan]) + + const handleScanPress = useCallback(() => { + const shouldScan = !scanning + setScanning(shouldScan) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let timeout: any | undefined + if (shouldScan) { + setError(undefined) + timeout = setTimeout(() => { + stopScan() + setScanning(false) + if (scannedDevices.length === 0) { + setError( + 'No hotspots found. Please ensure bluetooth pairing is enabled', + ) + } + }, 30 * 1000) + } + + if (shouldScan) { + startScan((e) => { + if (e) { + showError(e) + } + }) + } else { + stopScan() + } + return () => { + if (timeout) { + clearTimeout(timeout) + } + } + }, [scannedDevices.length, scanning, startScan, stopScan]) + + const navNext = useCallback( + () => navigation.push('WifiSettings'), + [navigation], + ) + + const [connecting, setConnecting] = useState(false) + const connectDevice = useCallback( + (d: Device) => async () => { + try { + setConnecting(true) + await connect(d) + if (scanning) { + stopScan() + setScanning(false) + } + setConnecting(false) + navNext() + } catch (e) { + showError(e) + } finally { + setConnecting(false) + } + }, + [connect, navNext, scanning, stopScan], + ) + + const renderItem = React.useCallback( + // eslint-disable-next-line react/no-unused-prop-types + ({ item }: { item: Device }) => { + return ( + + + + {item.name} + + + ) + }, + [connectDevice, connecting], + ) + + const keyExtractor = React.useCallback(({ id }: Device) => id, []) + + return ( + + + {bleInstructions || t('hotspotOnboarding.scan.subtitle')} + + {scannedDevices.length === 0 && scanning && } + {scannedDevices.length === 0 && scanning && ( + + {t('hotspotOnboarding.scan.scanning')} + + )} + + + {error && ( + + {error} + + )} + + + ) +} + +export default ScanHotspots diff --git a/src/features/hotspot-onboarding/iot-ble/WifiSettings.tsx b/src/features/hotspot-onboarding/iot-ble/WifiSettings.tsx new file mode 100644 index 000000000..955718a6a --- /dev/null +++ b/src/features/hotspot-onboarding/iot-ble/WifiSettings.tsx @@ -0,0 +1,166 @@ +import BackScreen from '@components/BackScreen' +import ButtonPressable from '@components/ButtonPressable' +import FabButton from '@components/FabButton' +import Text from '@components/Text' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import { useHotspotBle } from '@helium/react-native-sdk' +import { useNavigation } from '@react-navigation/native' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert, SectionList } from 'react-native' +import type { HotspotBleNavProp } from './navTypes' + +type Section = { + title: string + data: string[] + type: 'configured' | 'available' +} + +const WifiSettings = () => { + const navigation = useNavigation() + const { t } = useTranslation() + const navNext = useCallback( + () => navigation.push('AddGatewayBle'), + [navigation], + ) + const [networks, setNetworks] = useState() + const [configuredNetworks, setConfiguredNetworks] = useState() + const [connected, setConnected] = useState(false) + + const { isConnected, readWifiNetworks, removeConfiguredWifi } = + useHotspotBle() + + useEffect(() => { + isConnected().then(setConnected) + }, [isConnected]) + + useEffect(() => { + if (!connected) return + + readWifiNetworks(true).then(setConfiguredNetworks) + readWifiNetworks(false).then(setNetworks) + }, [connected, readWifiNetworks]) + + const handleNetworkSelected = useCallback( + ({ + network, + type, + }: { + network: string + type: 'configured' | 'available' + }) => + async () => { + if (type === 'available') { + navigation.push('WifiSetup', { network }) + } else { + Alert.alert( + t('hotspotOnboarding.wifiSettings.title'), + t('hotspotOnboarding.wifiSettings.remove', { network }), + [ + { + text: t('generic.cancel'), + style: 'default', + }, + { + text: t('generic.remove'), + style: 'destructive', + onPress: async () => { + setConfiguredNetworks( + configuredNetworks?.filter((n) => n !== network), + ) + await removeConfiguredWifi(network) + readWifiNetworks(true).then(setConfiguredNetworks) + readWifiNetworks(false).then(setNetworks) + }, + }, + ], + ) + } + }, + [configuredNetworks, navigation, readWifiNetworks, removeConfiguredWifi, t], + ) + + const renderItem = useCallback( + ({ + item: network, + section: { type }, + }: { + // eslint-disable-next-line react/no-unused-prop-types + item: string + // eslint-disable-next-line react/no-unused-prop-types + section: Section + }) => { + return ( + + + + {network} + + + ) + }, + [handleNetworkSelected], + ) + + const keyExtractor = useCallback((name: string) => name, []) + + const renderSectionHeader = ({ + section: { title }, + }: { + section: Section + }) => ( + + {title} + + ) + + const sections = useMemo( + (): Section[] => [ + { + data: configuredNetworks || [], + title: t('hotspotOnboarding.wifiSettings.configured'), + type: 'configured', + }, + { + data: networks || [], + title: t('hotspotOnboarding.wifiSettings.available'), + type: 'available', + }, + ], + [configuredNetworks, networks, t], + ) + + return ( + + + + + ) +} + +export default WifiSettings diff --git a/src/features/hotspot-onboarding/iot-ble/WifiSetup.tsx b/src/features/hotspot-onboarding/iot-ble/WifiSetup.tsx new file mode 100644 index 000000000..44ac434b5 --- /dev/null +++ b/src/features/hotspot-onboarding/iot-ble/WifiSetup.tsx @@ -0,0 +1,88 @@ +import BackScreen from '@components/BackScreen' +import Box from '@components/Box' +import ButtonPressable from '@components/ButtonPressable' +import Text from '@components/Text' +import TextInput from '@components/TextInput' +import { BleError, useHotspotBle } from '@helium/react-native-sdk' +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import type { HotspotBLEStackParamList, HotspotBleNavProp } from './navTypes' + +type Route = RouteProp +const WifiSetup = () => { + const { + params: { network }, + } = useRoute() + const [secureTextEntry, setSecureTextEntry] = useState(true) + const [loading, setLoading] = useState(false) + const [status, setStatus] = useState('') + const [password, setPassword] = useState('') + const { setWifi } = useHotspotBle() + const { t } = useTranslation() + const navigation = useNavigation() + const onBack = useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } + }, [navigation]) + + const toggleSecureEntry = useCallback(() => { + setSecureTextEntry(!secureTextEntry) + }, [secureTextEntry]) + + const handleSetWifi = useCallback(async () => { + setLoading(true) + try { + const nextStatus = await setWifi(network, password) + setStatus(nextStatus) + onBack() + } catch (e) { + if (typeof e === 'string') { + setStatus(e) + } else { + setStatus((e as BleError).toString()) + } + } + setLoading(false) + }, [onBack, network, password, setWifi]) + + return ( + + + + + + + + {loading ? 'loading...' : status} + + ) +} + +export default WifiSetup diff --git a/src/features/hotspot-onboarding/iot-ble/navTypes.tsx b/src/features/hotspot-onboarding/iot-ble/navTypes.tsx new file mode 100644 index 000000000..c95457865 --- /dev/null +++ b/src/features/hotspot-onboarding/iot-ble/navTypes.tsx @@ -0,0 +1,13 @@ +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import { IotBleOptions } from '../navTypes' + +export type HotspotBLEStackParamList = { + ScanHotspots: IotBleOptions + WifiSettings: undefined + WifiSetup: { network: string } + AddGatewayBle: undefined + Diagnostics: undefined +} + +export type HotspotBleNavProp = + NativeStackNavigationProp diff --git a/src/features/hotspot-onboarding/iot-ble/optionsContext.tsx b/src/features/hotspot-onboarding/iot-ble/optionsContext.tsx new file mode 100644 index 000000000..8f56b8832 --- /dev/null +++ b/src/features/hotspot-onboarding/iot-ble/optionsContext.tsx @@ -0,0 +1,22 @@ +import React, { useContext } from 'react' +import { IotBleOptions } from '../navTypes' + +const IotBleOptionsContext = React.createContext({}) + +export const useIotBleOptions = () => { + return useContext(IotBleOptionsContext) +} + +export const IotBleOptionsProvider = ({ + value, + children, +}: { + value: IotBleOptions + children: React.ReactNode +}) => { + return ( + + {children} + + ) +} From b04b3951579981cddb5942dd537300a6e0e10858 Mon Sep 17 00:00:00 2001 From: Chewing Glass Date: Mon, 11 Sep 2023 12:18:08 -0500 Subject: [PATCH 4/8] Assert UX improvements, load device list from server --- src/components/index.tsx | 30 ++++ .../collectables/AssertLocationScreen.tsx | 149 ++++++++++-------- .../hotspot-onboarding/SelectDevice.tsx | 30 +--- src/utils/walletApiV2.ts | 10 ++ tsconfig.json | 1 + 5 files changed, 133 insertions(+), 87 deletions(-) create mode 100644 src/components/index.tsx diff --git a/src/components/index.tsx b/src/components/index.tsx new file mode 100644 index 000000000..f5209bd38 --- /dev/null +++ b/src/components/index.tsx @@ -0,0 +1,30 @@ +import { ReAnimatedBlurBox, ReAnimatedBox } from '@components/AnimatedBox' +import BackScreen from '@components/BackScreen' +import Box from '@components/Box' +import ButtonPressable from '@components/ButtonPressable' +import CircleLoader from '@components/CircleLoader' +import FabButton from '@components/FabButton' +import FadeInOut, { DelayedFadeIn, FadeInFast } from '@components/FadeInOut' +import ImageBox from '@components/ImageBox' +import SafeAreaBox from '@components/SafeAreaBox' +import SearchInput from '@components/SearchInput' +import Text from '@components/Text' +import TextInput from '@components/TextInput' + +export { + ReAnimatedBlurBox, + ReAnimatedBox, + BackScreen, + Box, + ButtonPressable, + CircleLoader, + FabButton, + FadeInOut, + DelayedFadeIn, + FadeInFast, + ImageBox, + SafeAreaBox, + SearchInput, + Text, + TextInput, +} diff --git a/src/features/collectables/AssertLocationScreen.tsx b/src/features/collectables/AssertLocationScreen.tsx index 3a0febe22..e8157c32a 100644 --- a/src/features/collectables/AssertLocationScreen.tsx +++ b/src/features/collectables/AssertLocationScreen.tsx @@ -1,16 +1,22 @@ import MapPin from '@assets/images/mapPin.svg' -import { ReAnimatedBlurBox, ReAnimatedBox } from '@components/AnimatedBox' -import BackScreen from '@components/BackScreen' -import Box from '@components/Box' -import ButtonPressable from '@components/ButtonPressable' -import CircleLoader from '@components/CircleLoader' -import FabButton from '@components/FabButton' -import { DelayedFadeIn, FadeInFast } from '@components/FadeInOut' -import ImageBox from '@components/ImageBox' -import SafeAreaBox from '@components/SafeAreaBox' -import SearchInput from '@components/SearchInput' -import Text from '@components/Text' -import TextInput from '@components/TextInput' +import { + BackScreen, + Box, + ButtonPressable, + CircleLoader, + DelayedFadeIn, + FabButton, + FadeInFast, + FadeInOut, + ImageBox, + ReAnimatedBlurBox, + ReAnimatedBox, + SafeAreaBox, + SearchInput, + Text, + TextInput, +} from '@components' +import TouchableOpacityBox from '@components/TouchableOpacityBox' import { HotspotType } from '@helium/onboarding' import useAlert from '@hooks/useAlert' import { useEntityKey } from '@hooks/useEntityKey' @@ -23,6 +29,10 @@ import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' import MapboxGL from '@rnmapbox/maps' import turfBbox from '@turf/bbox' import { points } from '@turf/helpers' +import { parseH3BNLocation } from '@utils/h3' +import { removeDashAndCapitalize } from '@utils/hotspotNftsUtils' +import * as Logger from '@utils/logger' +import { MAX_MAP_ZOOM, MIN_MAP_ZOOM } from '@utils/mapbox' import debounce from 'lodash/debounce' import React, { memo, @@ -42,10 +52,7 @@ import { import { Config } from 'react-native-config' import { Edge } from 'react-native-safe-area-context' import 'text-encoding-polyfill' -import { parseH3BNLocation } from '../../utils/h3' -import { removeDashAndCapitalize } from '../../utils/hotspotNftsUtils' -import * as Logger from '../../utils/logger' -import { MAX_MAP_ZOOM, MIN_MAP_ZOOM } from '../../utils/mapbox' +import { useDebounce } from 'use-debounce' import { CollectableNavigationProp, CollectableStackParamList, @@ -53,6 +60,7 @@ import { const BUTTON_HEIGHT = 65 type Route = RouteProp + const AssertLocationScreen = () => { const { t } = useTranslation() const route = useRoute() @@ -241,26 +249,31 @@ const AssertLocationScreen = () => { const assertLocation = useCallback( async (type: HotspotType) => { - if (mapCenter && entityKey) { - setTransactionError(undefined) - setAsserting(true) - try { - hideElevGain() - await submitUpdateEntityInfo({ - type, - entityKey, - lng: mapCenter[0], - lat: mapCenter[1], - elevation, - decimalGain: gain, - }) - setAsserting(false) - collectNav.navigate('HotspotDetailsScreen', { collectable }) - } catch (error) { - setAsserting(false) - Logger.error(error) - setTransactionError((error as Error).message) - } + if (!mapCenter || !entityKey) return + + setTransactionError(undefined) + setAsserting(true) + try { + hideElevGain() + await submitUpdateEntityInfo({ + type, + entityKey, + lng: mapCenter[0], + lat: mapCenter[1], + elevation, + decimalGain: gain, + }) + setAsserting(false) + + await showOKAlert({ + title: t('assertLocationScreen.success.title'), + message: t('assertLocationScreen.success.message'), + }) + collectNav.navigate('HotspotDetailsScreen', { collectable }) + } catch (error) { + setAsserting(false) + Logger.error(error) + setTransactionError((error as Error).message) } }, [ @@ -270,6 +283,8 @@ const AssertLocationScreen = () => { submitUpdateEntityInfo, elevation, gain, + showOKAlert, + t, collectNav, collectable, ], @@ -306,13 +321,20 @@ const AssertLocationScreen = () => { if (transactionError) return transactionError }, [transactionError]) + const disabled = useMemo( + () => !mapCenter || reverseGeo.loading || asserting, + [asserting, mapCenter, reverseGeo.loading], + ) + const [debouncedDisabled] = useDebounce(disabled, 300) + const [reverseGeoLoading] = useDebounce(reverseGeo.loading, 300) + return ( { marginVertical="s" minHeight={40} > - {reverseGeo.loading && ( + {reverseGeoLoading && ( )} {showError && ( @@ -537,35 +559,36 @@ const AssertLocationScreen = () => { )} {!reverseGeo.loading && !showError && ( - - {reverseGeo.result} - + + + {reverseGeo.result} + + )} - - ) : undefined - } - /> + paddingVertical="lm" + disabled={disabled} + height={65} + alignItems="center" + justifyContent="center" + onPress={handleAssertLocationPress} + > + {debouncedDisabled || asserting ? ( + + ) : ( + + {t('assertLocationScreen.title')} + + )} + diff --git a/src/features/hotspot-onboarding/SelectDevice.tsx b/src/features/hotspot-onboarding/SelectDevice.tsx index 253bd7ddd..918cc6df4 100644 --- a/src/features/hotspot-onboarding/SelectDevice.tsx +++ b/src/features/hotspot-onboarding/SelectDevice.tsx @@ -1,36 +1,16 @@ import BackScreen from '@components/BackScreen' +import CircleLoader from '@components/CircleLoader' import ImageBox from '@components/ImageBox' import Text from '@components/Text' import TouchableOpacityBox from '@components/TouchableOpacityBox' import { useNavigation } from '@react-navigation/native' +import { getOnboardingDevices } from '@utils/walletApiV2' import React from 'react' +import { useAsync } from 'react-async-hook' import { useTranslation } from 'react-i18next' import { FlatList } from 'react-native' import { OnboardableDevice, OnboardingNavProp } from './navTypes' -const data: OnboardableDevice[] = [ - { - name: 'Rak V2', - type: 'IotBle', - image: - 'https://cdn.shopify.com/s/files/1/0177/8784/6756/t/58/assets/pf-0b98ac45--pfa8f02b76untitled106.png?v=1611822325', - options: { - bleInstructions: - 'Power on your hotspot. Then press the button on the side of the hotspot to enable BLE.', - }, - }, - { - name: 'Generic Bluetooth Enabled Hotspot', - type: 'IotBle', - image: - 'https://shdw-drive.genesysgo.net/6tcnBSybPG7piEDShBcrVtYJDPSvGrDbVvXmXKpzBvWP/hotspot.png', - options: { - bleInstructions: - 'Power on your hotspot. Depending on your hotspot, there is either a button to enable BLE, or BLE will be enabled for the first 5 minutes after powering on.', - }, - }, -] - const SelectOnboardableDevice = () => { const { t } = useTranslation() const navigation = useNavigation() @@ -66,6 +46,7 @@ const SelectOnboardableDevice = () => { }, [navigation], ) + const { result: data, loading } = useAsync(() => getOnboardingDevices(), []) const keyExtractor = React.useCallback( ({ name }: OnboardableDevice) => name, @@ -82,8 +63,9 @@ const SelectOnboardableDevice = () => { > {t('hotspotOnboarding.selectDevice.subtitle')} + {loading && } diff --git a/src/utils/walletApiV2.ts b/src/utils/walletApiV2.ts index 91a6ad675..d7deb30cd 100644 --- a/src/utils/walletApiV2.ts +++ b/src/utils/walletApiV2.ts @@ -6,6 +6,7 @@ import { getSecureItem } from '../storage/secureStorage' import { AccountBalance, Prices } from '../types/balance' import makeApiToken from './makeApiToken' import { CSAccount } from '../storage/cloudStorage' +import { OnboardableDevice } from '../features/hotspot-onboarding/navTypes' export type Notification = { title: string @@ -131,6 +132,15 @@ export const getRecommendedDapps = async () => { return data } +export const getOnboardingDevices = async () => { + const { data, ...rest } = await axiosInstance.get( + '/onboardingDevices', + ) + console.log("rest", rest) + + return data +} + export const getSessionKey = async () => { const { data } = await axiosInstance.get<{ sessionKey: string }>( '/sessionKey', diff --git a/tsconfig.json b/tsconfig.json index 38c51bba8..7e9ef5dca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "@assets": ["./src/assets"], "@components/*": ["./src/components/*"], "@constants/*": ["./src/constants/*"], + "@components": ["src/components/index"], "@hooks/*": ["./src/hooks/*"], "@theme/*": ["./src/theme/*"], "@utils/*": ["./src/utils/*"], From 43e05ffe8c74dfc8d0e0f971b5e5866079d241cf Mon Sep 17 00:00:00 2001 From: Chewing Glass Date: Mon, 11 Sep 2023 15:35:32 -0500 Subject: [PATCH 5/8] Simplify flow --- src/assets/images/collectableIcon.svg | 14 +++++++++++ src/assets/images/gem.svg | 3 --- .../hotspot-onboarding/SelectDevice.tsx | 24 ++++++++++++------- src/locales/en.ts | 11 ++++----- src/navigation/TabBarNavigator.tsx | 2 +- src/utils/walletApiV2.ts | 9 ------- 6 files changed, 35 insertions(+), 28 deletions(-) create mode 100644 src/assets/images/collectableIcon.svg delete mode 100644 src/assets/images/gem.svg diff --git a/src/assets/images/collectableIcon.svg b/src/assets/images/collectableIcon.svg new file mode 100644 index 000000000..092e0bfbe --- /dev/null +++ b/src/assets/images/collectableIcon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/gem.svg b/src/assets/images/gem.svg deleted file mode 100644 index cef27ac9d..000000000 --- a/src/assets/images/gem.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/features/hotspot-onboarding/SelectDevice.tsx b/src/features/hotspot-onboarding/SelectDevice.tsx index 918cc6df4..4ed075f4e 100644 --- a/src/features/hotspot-onboarding/SelectDevice.tsx +++ b/src/features/hotspot-onboarding/SelectDevice.tsx @@ -1,16 +1,26 @@ import BackScreen from '@components/BackScreen' -import CircleLoader from '@components/CircleLoader' import ImageBox from '@components/ImageBox' import Text from '@components/Text' import TouchableOpacityBox from '@components/TouchableOpacityBox' import { useNavigation } from '@react-navigation/native' -import { getOnboardingDevices } from '@utils/walletApiV2' import React from 'react' -import { useAsync } from 'react-async-hook' import { useTranslation } from 'react-i18next' import { FlatList } from 'react-native' import { OnboardableDevice, OnboardingNavProp } from './navTypes' +const data: OnboardableDevice[] = [ + { + name: 'Bluetooth Enabled Hotspot', + type: 'IotBle', + image: + 'https://shdw-drive.genesysgo.net/6tcnBSybPG7piEDShBcrVtYJDPSvGrDbVvXmXKpzBvWP/hotspot.png', + options: { + bleInstructions: + 'Power on your Hotspot. Follow manufacturer instructions for enabling bluetooth discovery on the Hotspot.', + }, + }, +] + const SelectOnboardableDevice = () => { const { t } = useTranslation() const navigation = useNavigation() @@ -46,7 +56,6 @@ const SelectOnboardableDevice = () => { }, [navigation], ) - const { result: data, loading } = useAsync(() => getOnboardingDevices(), []) const keyExtractor = React.useCallback( ({ name }: OnboardableDevice) => name, @@ -54,18 +63,17 @@ const SelectOnboardableDevice = () => { ) return ( - + - {t('hotspotOnboarding.selectDevice.subtitle')} + {t('hotspotOnboarding.selectOnboardingMethod.subtitle')} - {loading && } diff --git a/src/locales/en.ts b/src/locales/en.ts index a6a78186c..887f3645d 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -20,8 +20,6 @@ export default { hotspotOnboarding: { scan: { title: 'Scan for Hotspots', - subtitle: - "Please enable BLE on your hotspot. Depending on the manufacturer, there may be a button to enable BLE, or it may be enabled for the first 5 minutes after powering on. Please refer to your manufacturer instructions for more information. If your hotspot does not support BLE, you will need to onboard through your manufacturer's app.", start: 'Start Scan', stop: 'Stop Scan', notEnabled: 'Bluetooth is not enabled', @@ -37,13 +35,12 @@ export default { onboarding: { title: 'Onboarding', subtitle: - 'Onboard your hotspot to the IOT network. After onboarding this hotspot, you will be able to set the location and antenna details.', + 'Onboard your Hotspot to the IOT network. After onboarding this Hotspot, you will be able to set the location and antenna details.', onboard: 'Onboard Hotspot', }, - selectDevice: { - title: 'Select Device', - subtitle: - "Select your Hotspot to continue. Don't see your device? Your manufacturer may have their own app for onboarding the hotspot. Check the instructions. You can also try onboarding it as a Generic Bluetooth Enabled Hotspot", + selectOnboardingMethod: { + title: 'Select Onboarding Method', + subtitle: 'Select your onboarding method to continue.', }, }, accountImport: { diff --git a/src/navigation/TabBarNavigator.tsx b/src/navigation/TabBarNavigator.tsx index 2134934bc..af6df1520 100644 --- a/src/navigation/TabBarNavigator.tsx +++ b/src/navigation/TabBarNavigator.tsx @@ -6,7 +6,7 @@ import { } from '@react-navigation/bottom-tabs' import { Edge, useSafeAreaInsets } from 'react-native-safe-area-context' import Dollar from '@assets/images/dollar.svg' -import Gem from '@assets/images/gem.svg' +import Gem from '@assets/images/collectableIcon.svg' import Transactions from '@assets/images/transactions.svg' import Notifications from '@assets/images/notifications.svg' import { Portal } from '@gorhom/portal' diff --git a/src/utils/walletApiV2.ts b/src/utils/walletApiV2.ts index d7deb30cd..db63c97d9 100644 --- a/src/utils/walletApiV2.ts +++ b/src/utils/walletApiV2.ts @@ -132,15 +132,6 @@ export const getRecommendedDapps = async () => { return data } -export const getOnboardingDevices = async () => { - const { data, ...rest } = await axiosInstance.get( - '/onboardingDevices', - ) - console.log("rest", rest) - - return data -} - export const getSessionKey = async () => { const { data } = await axiosInstance.get<{ sessionKey: string }>( '/sessionKey', From adfaae65bf6ac9d677648604fce0373442c696ee Mon Sep 17 00:00:00 2001 From: Chewing Glass Date: Tue, 12 Sep 2023 16:01:14 -0500 Subject: [PATCH 6/8] Bluetooth logo --- src/assets/images/bluetooth.svg | 3 +++ .../hotspot-onboarding/SelectDevice.tsx | 25 +++++++++++-------- src/features/hotspot-onboarding/navTypes.tsx | 3 ++- 3 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 src/assets/images/bluetooth.svg diff --git a/src/assets/images/bluetooth.svg b/src/assets/images/bluetooth.svg new file mode 100644 index 000000000..43e690a02 --- /dev/null +++ b/src/assets/images/bluetooth.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/features/hotspot-onboarding/SelectDevice.tsx b/src/features/hotspot-onboarding/SelectDevice.tsx index 4ed075f4e..f79ff4a13 100644 --- a/src/features/hotspot-onboarding/SelectDevice.tsx +++ b/src/features/hotspot-onboarding/SelectDevice.tsx @@ -1,4 +1,5 @@ import BackScreen from '@components/BackScreen' +import Bluetooth from '@assets/images/bluetooth.svg' import ImageBox from '@components/ImageBox' import Text from '@components/Text' import TouchableOpacityBox from '@components/TouchableOpacityBox' @@ -12,8 +13,7 @@ const data: OnboardableDevice[] = [ { name: 'Bluetooth Enabled Hotspot', type: 'IotBle', - image: - 'https://shdw-drive.genesysgo.net/6tcnBSybPG7piEDShBcrVtYJDPSvGrDbVvXmXKpzBvWP/hotspot.png', + icon: , options: { bleInstructions: 'Power on your Hotspot. Follow manufacturer instructions for enabling bluetooth discovery on the Hotspot.', @@ -39,15 +39,18 @@ const SelectOnboardableDevice = () => { borderColor="grey900" borderBottomWidth={1} > - + {item.image && ( + + )} + {item.icon && item.icon} {item.name} diff --git a/src/features/hotspot-onboarding/navTypes.tsx b/src/features/hotspot-onboarding/navTypes.tsx index 0497add9b..8c01db29a 100644 --- a/src/features/hotspot-onboarding/navTypes.tsx +++ b/src/features/hotspot-onboarding/navTypes.tsx @@ -7,7 +7,8 @@ export type IotBleOptions = { export type OnboardableDevice = { name: string type: 'IotBle' - image: string + image?: string + icon?: React.ReactElement options: IotBleOptions } From ce90eb132fdbc0567c89f12075e7cee06347c0e3 Mon Sep 17 00:00:00 2001 From: Chewing Glass Date: Tue, 12 Sep 2023 16:21:29 -0500 Subject: [PATCH 7/8] Cr comments --- .../hotspot-onboarding/iot-ble/AddGatewayBle.tsx | 14 ++++++++++---- src/locales/en.ts | 7 +++++++ src/utils/walletApiV2.ts | 1 - 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/features/hotspot-onboarding/iot-ble/AddGatewayBle.tsx b/src/features/hotspot-onboarding/iot-ble/AddGatewayBle.tsx index bfb3c2ab4..c7e5cfe1b 100644 --- a/src/features/hotspot-onboarding/iot-ble/AddGatewayBle.tsx +++ b/src/features/hotspot-onboarding/iot-ble/AddGatewayBle.tsx @@ -65,12 +65,14 @@ const AddGatewayBle = () => { if (!onboardRecord) { throw new Error( - `This hotspot does not exist in the onboarding server. Contact your manufacturer to have them approve hotspot with id ${onboardAddress}`, + t('hotspotOnboarding.onboarding.hotspotNotFound', { + onboardAddress, + }), ) } if (!onboardRecord?.maker.address) { - throw new Error('Could not get maker address') + throw new Error(t('hotspotOnboarding.onboarding.makerNotFound')) } const makerSolAddr = heliumAddressToSolAddress(onboardRecord?.maker.address) const makerSolBalance = ( @@ -83,7 +85,9 @@ const AddGatewayBle = () => { makerSolBalance / LAMPORTS_PER_SOL < 0.00089088 + 0.00001 ) { throw new Error( - `Manufacturer ${onboardRecord?.maker.name} does not have enough SOL to onboard this hotspot. Please contact the manufacturer to resolve this issue.`, + t('hotspotOnboarding.onboarding.manufacturerMissingSol', { + name: onboardRecord?.maker.name, + }), ) } @@ -104,7 +108,9 @@ const AddGatewayBle = () => { e.InstructionError[1].Custom === 1 ) { throw new Error( - `Manufacturer ${onboardRecord?.maker.name} does not have enough SOL or Data Credits to onboard this hotspot. Please contact the manufacturer to resolve this issue.`, + t('hotspotOnboarding.onboarding.manufacturerMissingDcOrSol', { + name: onboardRecord?.maker.name, + }), ) } if (e.InstructionError) { diff --git a/src/locales/en.ts b/src/locales/en.ts index 887f3645d..b132f2327 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -37,6 +37,13 @@ export default { subtitle: 'Onboard your Hotspot to the IOT network. After onboarding this Hotspot, you will be able to set the location and antenna details.', onboard: 'Onboard Hotspot', + hotspotNotFound: + 'This hotspot does not exist in the onboarding server. Contact your manufacturer to have them approve hotspot with id {{onboardAddress}}', + makerNotFound: 'Maker does not exist', + manufacturerMissingSol: + 'Manufacturer {{name}} does not have enough SOL to onboard this hotspot. Please contact the manufacturer to resolve this issue.', + manufacturerMissingDcOrSol: + 'Manufacturer {{name}} does not have enough Data Credits or SOL to onboard this hotspot. Please contact the manufacturer to resolve this issue.', }, selectOnboardingMethod: { title: 'Select Onboarding Method', diff --git a/src/utils/walletApiV2.ts b/src/utils/walletApiV2.ts index db63c97d9..91a6ad675 100644 --- a/src/utils/walletApiV2.ts +++ b/src/utils/walletApiV2.ts @@ -6,7 +6,6 @@ import { getSecureItem } from '../storage/secureStorage' import { AccountBalance, Prices } from '../types/balance' import makeApiToken from './makeApiToken' import { CSAccount } from '../storage/cloudStorage' -import { OnboardableDevice } from '../features/hotspot-onboarding/navTypes' export type Notification = { title: string From f733662569b5cf9857af13b9f69c30fb1832504d Mon Sep 17 00:00:00 2001 From: Chewing Glass Date: Mon, 25 Sep 2023 15:56:30 -0500 Subject: [PATCH 8/8] Fix lint --- src/features/payment/PaymentScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/payment/PaymentScreen.tsx b/src/features/payment/PaymentScreen.tsx index ec65b6e33..db89e0915 100644 --- a/src/features/payment/PaymentScreen.tsx +++ b/src/features/payment/PaymentScreen.tsx @@ -50,7 +50,6 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view import { useSafeAreaInsets } from 'react-native-safe-area-context' import Toast from 'react-native-simple-toast' import { useSelector } from 'react-redux' -import { useDebouncedCallback } from 'use-debounce' import useSubmitTxn from '../../hooks/useSubmitTxn' import { RootNavigationProp } from '../../navigation/rootTypes' import { useSolana } from '../../solana/SolanaProvider'