From ac700b6566073fab3669a6699d6a5bfa29ef3864 Mon Sep 17 00:00:00 2001 From: MichaelBrew Date: Sun, 17 Oct 2021 16:22:42 -0500 Subject: [PATCH 1/7] Define new AppLinkAntenna type --- src/providers/appLinkTypes.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/providers/appLinkTypes.ts b/src/providers/appLinkTypes.ts index 1a2d5e926..a7e7396b3 100644 --- a/src/providers/appLinkTypes.ts +++ b/src/providers/appLinkTypes.ts @@ -6,6 +6,7 @@ export const AppLinkCategories = [ 'validator', 'add_gateway', 'hotspot_location', + 'hotspot_antenna', ] as const export type AppLinkCategoryType = typeof AppLinkCategories[number] @@ -38,3 +39,10 @@ export type AppLinkLocation = { latitude: number longitude: number } + +export type AppLinkAntenna = { + type: AppLinkCategoryType + hotspotAddress: string + gain: number + elevation?: number +} From a2893a982eb0f9336b591e3b9fd003a3a7e42e8c Mon Sep 17 00:00:00 2001 From: MichaelBrew Date: Sun, 17 Oct 2021 16:23:48 -0500 Subject: [PATCH 2/7] Add support for hotspot_antenna QR codes in ScanView --- src/features/wallet/scan/ScanView.tsx | 13 ++++++++--- src/providers/AppLinkProvider.tsx | 33 +++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/features/wallet/scan/ScanView.tsx b/src/features/wallet/scan/ScanView.tsx index f1913779f..bb160477d 100644 --- a/src/features/wallet/scan/ScanView.tsx +++ b/src/features/wallet/scan/ScanView.tsx @@ -23,6 +23,7 @@ import { MismatchedAddressError, } from '../../../providers/AppLinkProvider' import { + AppLinkAntenna, AppLinkCategoryType, AppLinkLocation, } from '../../../providers/appLinkTypes' @@ -66,8 +67,14 @@ const ScanView = ({ scanType = 'payment', showBottomSheet = true }: Props) => { try { await handleBarCode(result, scanType, undefined, (scanResult) => { - if (scanResult.type === 'hotspot_location') { - const { hotspotAddress } = scanResult as AppLinkLocation + const shouldAssertHotspotOwnership = [ + 'hotspot_location', + 'hotspot_antenna', + ].includes(scanResult.type) + if (shouldAssertHotspotOwnership) { + const { hotspotAddress } = scanResult as + | AppLinkLocation + | AppLinkAntenna const hotspot = hotspots.find((h) => h.address === hotspotAddress) if (!hotspot) throw new InvalidAddressError() } @@ -76,7 +83,7 @@ const ScanView = ({ scanType = 'payment', showBottomSheet = true }: Props) => { setScanned(true) triggerNotification('success') } catch (error) { - handleFailedScan(error) + handleFailedScan(error as Error) } } diff --git a/src/providers/AppLinkProvider.tsx b/src/providers/AppLinkProvider.tsx index f10a1786e..a0bee8b09 100644 --- a/src/providers/AppLinkProvider.tsx +++ b/src/providers/AppLinkProvider.tsx @@ -24,6 +24,7 @@ import { AppLinkPayment, Payee, AppLinkLocation, + AppLinkAntenna, } from './appLinkTypes' const APP_LINK_PROTOCOL = 'helium://' @@ -108,7 +109,7 @@ const useAppLink = () => { }) const navToAppLink = useCallback( - (record: AppLink | AppLinkPayment | AppLinkLocation) => { + (record: AppLink | AppLinkPayment | AppLinkLocation | AppLinkAntenna) => { if (isLocked || !isBackedUp) { setUnhandledLink(record as AppLink) return @@ -149,6 +150,16 @@ const useAppLink = () => { }) break } + + case 'hotspot_antenna': { + // const { hotspotAddress, gain, elevation } = record as AppLinkAntenna + // navigator.updateHotspotAntenna({ + // hotspotAddress, + // gain, + // elevation, + // }) + break + } } }, [isLocked, isBackedUp], @@ -206,17 +217,18 @@ const useAppLink = () => { /** * The data scanned from the QR code is expected to be one of these possibilities: * (1) A helium deeplink URL - * (2) A lat/lng pair + hotspot address for hotspot location updates + * (2) stringified JSON object { address, lat, lng } (for hotspot location updates) * (3) address string * (4) stringified JSON object { type, senderAddress?, address, amount?, memo? } * (5) stringified JSON object { type, senderAddress?, payees: {[payeeAddress]: amount} } * (6) stringified JSON object { type, senderAddress?, payees: {[payeeAddress]: { amount, memo? }} } + * (7) stringified JSON object { type: "antenna_update", hotspot_address, elevation, gain } */ const parseBarCodeData = useCallback( async ( data: string, scanType: AppLinkCategoryType, - ): Promise => { + ): Promise => { // Case (1) helium deeplink URL const urlParams = parseUrl(data) if (urlParams) { @@ -320,6 +332,19 @@ const useAppLink = () => { ) return scanResult } + + if (type === 'antenna_update') { + const { hotspot_address: hotspotAddress, gain, elevation } = JSON.parse( + data, + ) + assertValidAddress(hotspotAddress) + return { + type: 'hotspot_antenna', + hotspotAddress, + gain, + elevation, + } + } throw new Error('Unknown scan type') }, [parseUrl], @@ -331,7 +356,7 @@ const useAppLink = () => { scanType: AppLinkCategoryType, opts?: Record, assertScanResult?: ( - scanResult: AppLink | AppLinkPayment | AppLinkLocation, + scanResult: AppLink | AppLinkPayment | AppLinkLocation | AppLinkAntenna, ) => void, ) => { const scanResult = await parseBarCodeData(data, scanType) From 94b6dfb5520fa8657afd417b72a87d861e0f17ad Mon Sep 17 00:00:00 2001 From: MichaelBrew Date: Sun, 17 Oct 2021 18:00:20 -0500 Subject: [PATCH 3/7] Update HotspotConfigurationPicker so that gain and elevation use controlled inputs, with state management delegated to UpdateHotspotConfig --- src/components/HotspotConfigurationPicker.tsx | 128 ++++++++++-------- .../updateHotspot/UpdateHotspotConfig.tsx | 28 +++- 2 files changed, 97 insertions(+), 59 deletions(-) diff --git a/src/components/HotspotConfigurationPicker.tsx b/src/components/HotspotConfigurationPicker.tsx index 5807ed4a0..204f29cc8 100644 --- a/src/components/HotspotConfigurationPicker.tsx +++ b/src/components/HotspotConfigurationPicker.tsx @@ -16,12 +16,41 @@ import { useColors } from '../theme/themeHooks' import { AntennaModelKeys, AntennaModels } from '../makers' import { MakerAntenna } from '../makers/antennaMakerTypes' +function gainFloatToString(gainFloat?: number): string { + return gainFloat != null + ? gainFloat.toLocaleString(locale, { + maximumFractionDigits: 1, + minimumFractionDigits: 1, + }) + : '' +} +function gainStringToFloat(gainStr?: string): number | undefined { + return gainStr + ? parseFloat( + gainStr.replace(groupSeparator, '').replace(decimalSeparator, '.'), + ) + : undefined +} +function elevationStringToInt(elevationStr?: string): number | undefined { + return elevationStr + ? parseInt( + elevationStr.replace(groupSeparator, '').replace(decimalSeparator, '.'), + 10, + ) + : undefined +} +function elevationIntToString(elevationInt?: number): string { + return elevationInt != null ? elevationInt.toLocaleString(locale) : '' +} + type Props = { onAntennaUpdated: (antenna: MakerAntenna) => void - onGainUpdated: (gain: number) => void - onElevationUpdated: (elevation: number) => void + onGainUpdated: (gain: number | undefined) => void + onElevationUpdated: (elevation: number | undefined) => void selectedAntenna?: MakerAntenna outline?: boolean + gain?: number + elevation?: number } const HotspotConfigurationPicker = ({ selectedAntenna, @@ -29,6 +58,8 @@ const HotspotConfigurationPicker = ({ onGainUpdated, onElevationUpdated, outline, + gain, + elevation, }: Props) => { const { t } = useTranslation() const colors = useColors() @@ -36,13 +67,17 @@ const HotspotConfigurationPicker = ({ const gainInputRef = useRef(null) const elevationInputRef = useRef(null) - const [gain, setGain] = useState( - selectedAntenna - ? selectedAntenna.gain.toLocaleString(locale, { - maximumFractionDigits: 1, - minimumFractionDigits: 1, - }) - : undefined, + // Use state to track temporary raw edits for gain and elevation so that we can delay actual + // updates (delegated to parent component) until the user has finished editing. This prevents + // the need to reformat input as the user is actively typing, while ensuring the parent component + // only receives updates when the user has finished. + const [isEditingGain, setIsEditingGain] = useState(false) + const [isEditingElevation, setIsEditingElevation] = useState(false) + const [tmpGain, setTmpGain] = useState( + gain != null ? gainFloatToString(gain) : undefined, + ) + const [tmpElevation, setTmpElevation] = useState( + elevation != null ? elevationIntToString(elevation) : undefined, ) const antennas = useMemo( @@ -59,12 +94,7 @@ const HotspotConfigurationPicker = ({ const antenna = antennas[index] onAntennaUpdated(antenna) onGainUpdated(antenna.gain) - setGain( - antenna.gain.toLocaleString(locale, { - maximumFractionDigits: 1, - minimumFractionDigits: 1, - }), - ) + setTmpGain(gainFloatToString(antenna.gain)) } const showElevationInfo = () => @@ -82,53 +112,37 @@ const HotspotConfigurationPicker = ({ elevationInputRef.current?.focus() } - const onChangeGain = (text: string) => setGain(text) + const onChangeGain = (text: string) => { + if (!isEditingGain) setIsEditingGain(true) + setTmpGain(text) + } const onDoneEditingGain = () => { - const gainFloat = gain - ? parseFloat( - gain.replace(groupSeparator, '').replace(decimalSeparator, '.'), - ) - : 0 - let gainString - if (!gainFloat || gainFloat <= 1) { - gainString = '1' - } else if (gainFloat >= 15) { - gainString = '15' - } else { - gainString = gainFloat.toLocaleString(locale, { - maximumFractionDigits: 1, - }) - } - setGain(gainString) + setIsEditingGain(false) + const gainStrRaw = tmpGain + const gainFloat = gainStringToFloat(gainStrRaw) + const gainStr = gainFloatToString(gainFloat) + setTmpGain(gainStr) onGainUpdated(gainFloat) } - const onChangeElevation = (text: string) => { - const elevationInteger = text - ? parseInt( - text.replace(groupSeparator, '').replace(decimalSeparator, '.'), - 10, - ) - : 0 - let stringElevation - if (!elevationInteger) { - stringElevation = '0' - } else { - stringElevation = elevationInteger.toString() - } - onElevationUpdated(parseInt(stringElevation, 10)) + if (!isEditingElevation) setIsEditingElevation(true) + setTmpElevation(text) + } + const onDoneEditingElevation = () => { + setIsEditingElevation(false) + const elevationStrRaw = tmpElevation + const elevationInt = elevationStringToInt(elevationStrRaw) + const elevationStr = elevationIntToString(elevationInt) + setTmpElevation(elevationStr) + onElevationUpdated(elevationInt) } useEffect(() => { if (selectedAntenna) { - setGain( - selectedAntenna.gain.toLocaleString(locale, { - maximumFractionDigits: 1, - minimumFractionDigits: 1, - }), - ) + onGainUpdated(selectedAntenna.gain) + setTmpGain(gainFloatToString(selectedAntenna.gain)) } - }, [selectedAntenna]) + }, [selectedAntenna, onGainUpdated]) return ( diff --git a/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx b/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx index 4c98a17cd..d636d251b 100644 --- a/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx +++ b/src/features/hotspots/settings/updateHotspot/UpdateHotspotConfig.tsx @@ -39,20 +39,36 @@ type Props = { onClose: () => void onCloseSettings: () => void hotspot: Hotspot | Witness + antennaGain?: number + antennaElevation?: number + initState?: State } type State = 'antenna' | 'location' | 'confirm' -const UpdateHotspotConfig = ({ onClose, onCloseSettings, hotspot }: Props) => { +const UpdateHotspotConfig = ({ + onClose, + onCloseSettings, + hotspot, + antennaGain, + antennaElevation, + initState, +}: Props) => { const { t } = useTranslation() const submitTxn = useSubmitTxn() const navigation = useNavigation() const [state, setState] = useState( - isDataOnly(hotspot) ? 'location' : 'antenna', + initState ?? (isDataOnly(hotspot) ? 'location' : 'antenna'), + ) + const [antenna, setAntenna] = useState( + antennaGain != null + ? ({ name: 'Custom Antenna', gain: antennaGain } as MakerAntenna) + : undefined, + ) + const [gain, setGain] = useState(antennaGain) + const [elevation, setElevation] = useState( + antennaElevation, ) - const [antenna, setAntenna] = useState() - const [gain, setGain] = useState() - const [elevation, setElevation] = useState(0) const [location, setLocation] = useState() const [locationName, setLocationName] = useState() const [fullScreen, setFullScreen] = useState(false) @@ -340,6 +356,8 @@ const UpdateHotspotConfig = ({ onClose, onCloseSettings, hotspot }: Props) => { onGainUpdated={setGain} onElevationUpdated={setElevation} selectedAntenna={antenna} + gain={gain} + elevation={elevation} />