-
Notifications
You must be signed in to change notification settings - Fork 57
feat(pos-app): unified Update keys screen with QR import/export + PIN gate #517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
c6afc28
16f353a
4a33b2b
95513cc
951994d
3720fcf
9d56147
6235f03
4364eac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| import { Button } from "@/components/button"; | ||
| import { PinModal } from "@/components/pin-modal"; | ||
| import QRCode from "@/components/qr-code"; | ||
| import { ThemedText } from "@/components/themed-text"; | ||
| import { BorderRadius, Spacing } from "@/constants/spacing"; | ||
| import { useBiometricAuth } from "@/hooks/use-biometric-auth"; | ||
| import { usePinGate } from "@/hooks/use-pin-gate"; | ||
| import { useTheme } from "@/hooks/use-theme-color"; | ||
| import { useSettingsStore } from "@/store/useSettingsStore"; | ||
| import { encodeDeviceSetupQr } from "@/utils/device-setup-qr"; | ||
| import { router } from "expo-router"; | ||
| import { useCallback, useEffect, useRef, useState } from "react"; | ||
| import { StyleSheet, View } from "react-native"; | ||
| import { ScrollView } from "react-native-gesture-handler"; | ||
|
|
||
| export default function ExportKeysScreen() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Auto Review Issue: Severity: HIGH Context:
Recommendation: export default function ExportKeysScreen() {
// Web-only: native builds must never expose an export path.
useEffect(() => {
if (Platform.OS !== 'web') {
router.replace('/update-keys');
}
}, []);
if (Platform.OS !== 'web') return null;
// ...rest of component
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 951994d. |
||
| const theme = useTheme(); | ||
| const storedMerchantId = useSettingsStore((state) => state.merchantId); | ||
| const getCustomerApiKey = useSettingsStore( | ||
| (state) => state.getCustomerApiKey, | ||
| ); | ||
|
|
||
| const { | ||
| activeModal, | ||
| pinError, | ||
| requireAuth, | ||
| handlePinVerifyComplete, | ||
| handlePinSetupComplete, | ||
| handleBiometricSuccess, | ||
| handleBiometricFailure, | ||
| cancel, | ||
| } = usePinGate(); | ||
| const { biometricLabel, canUseBiometric, authenticate } = useBiometricAuth(); | ||
|
|
||
| const [unlocked, setUnlocked] = useState(false); | ||
| const [qrValue, setQrValue] = useState<string | null>(null); | ||
| const [isLoading, setIsLoading] = useState(true); | ||
| const startedRef = useRef(false); | ||
|
|
||
| // Require a PIN once, before anything sensitive is read or shown. | ||
| useEffect(() => { | ||
| if (startedRef.current) return; | ||
| startedRef.current = true; | ||
| requireAuth(() => setUnlocked(true)); | ||
| }, [requireAuth]); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 951994d. |
||
|
|
||
| // Read the customer key only after the user has unlocked. | ||
| useEffect(() => { | ||
| if (!unlocked) return; | ||
| let active = true; | ||
| (async () => { | ||
| const customerApiKey = await getCustomerApiKey(); | ||
| if (!active) return; | ||
| if (storedMerchantId && customerApiKey) { | ||
| setQrValue( | ||
| encodeDeviceSetupQr({ | ||
| merchantId: storedMerchantId, | ||
| customerApiKey, | ||
| }), | ||
| ); | ||
| } else { | ||
| setQrValue(null); | ||
| } | ||
| setIsLoading(false); | ||
| })(); | ||
| return () => { | ||
| active = false; | ||
| }; | ||
| }, [unlocked, storedMerchantId, getCustomerApiKey]); | ||
|
|
||
| const handleBiometricAuth = useCallback(async () => { | ||
| const success = await authenticate(`Use ${biometricLabel} to export keys`); | ||
| if (success) { | ||
| handleBiometricSuccess(); | ||
| } else { | ||
| handleBiometricFailure(); | ||
| } | ||
| }, [ | ||
| authenticate, | ||
| biometricLabel, | ||
| handleBiometricSuccess, | ||
| handleBiometricFailure, | ||
| ]); | ||
|
|
||
| // Cancelling the PIN prompt leaves the screen entirely. | ||
| const handleCancel = useCallback(() => { | ||
| cancel(); | ||
| router.back(); | ||
| }, [cancel]); | ||
|
|
||
| const goToUpdateKeys = useCallback(() => { | ||
| if (router.canGoBack()) { | ||
| router.back(); | ||
| } else { | ||
| router.replace("/update-keys"); | ||
| } | ||
| }, []); | ||
|
|
||
| return ( | ||
| <View style={styles.container}> | ||
| {unlocked && ( | ||
| <ScrollView | ||
| contentContainerStyle={styles.content} | ||
| showsVerticalScrollIndicator={false} | ||
| > | ||
| {!isLoading && !qrValue ? ( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Auto Review Issue: QR renders with Severity: LOW Context:
Recommendation: {isLoading ? (
<ActivityIndicator />
) : !qrValue ? (
// error state
) : (
<QRCode size={280} uri={qrValue} arenaClear />
)}
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't fix — not a broken state. The |
||
| <> | ||
| <ThemedText | ||
| fontSize={16} | ||
| lineHeight={22} | ||
| color="text-secondary" | ||
| style={styles.centerText} | ||
| > | ||
| Set a merchant ID and customer API key first. | ||
| </ThemedText> | ||
| <Button | ||
| onPress={goToUpdateKeys} | ||
| style={[ | ||
| styles.cta, | ||
| { backgroundColor: theme["bg-accent-primary"] }, | ||
| ]} | ||
| > | ||
| <ThemedText fontSize={16} lineHeight={18} color="text-invert"> | ||
| Go to Update keys | ||
| </ThemedText> | ||
| </Button> | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <ThemedText | ||
| fontSize={16} | ||
| lineHeight={22} | ||
| color="text-secondary" | ||
| style={styles.centerText} | ||
| > | ||
| On the new device, open Settings → Update keys → Import keys, | ||
| then scan this setup code. | ||
| </ThemedText> | ||
|
|
||
| <QRCode size={280} uri={qrValue ?? undefined} arenaClear /> | ||
|
|
||
| <View | ||
| style={[ | ||
| styles.warning, | ||
| { | ||
| backgroundColor: theme["foreground-primary"], | ||
| borderColor: theme["border-primary"], | ||
| }, | ||
| ]} | ||
| > | ||
| <ThemedText fontSize={14} lineHeight={20} color="text-tertiary"> | ||
| Anyone who scans this gets full access to your merchant | ||
| account. Only show it to devices you trust, and don't | ||
| share a photo of it. | ||
| </ThemedText> | ||
| </View> | ||
| </> | ||
| )} | ||
| </ScrollView> | ||
| )} | ||
|
|
||
| <PinModal | ||
| visible={activeModal !== "none"} | ||
| title={activeModal === "pin-verify" ? "Enter PIN" : "Create PIN"} | ||
| subtitle={ | ||
| activeModal === "pin-verify" | ||
| ? "Enter your PIN to export keys." | ||
| : "Set a 4-digit PIN to protect your settings." | ||
| } | ||
| onComplete={ | ||
| activeModal === "pin-verify" | ||
| ? handlePinVerifyComplete | ||
| : handlePinSetupComplete | ||
| } | ||
| onCancel={handleCancel} | ||
| error={pinError} | ||
| showBiometric={activeModal === "pin-verify" && !!canUseBiometric} | ||
| onBiometricPress={handleBiometricAuth} | ||
| /> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| }, | ||
| content: { | ||
| flexGrow: 1, | ||
| alignItems: "center", | ||
| justifyContent: "center", | ||
| paddingHorizontal: Spacing["spacing-5"], | ||
| paddingVertical: Spacing["spacing-6"], | ||
| gap: Spacing["spacing-6"], | ||
| }, | ||
| centerText: { | ||
| textAlign: "center", | ||
| }, | ||
| cta: { | ||
| height: 56, | ||
| borderRadius: BorderRadius["4"], | ||
| paddingHorizontal: Spacing["spacing-6"], | ||
| justifyContent: "center", | ||
| alignItems: "center", | ||
| }, | ||
| warning: { | ||
| borderWidth: 1, | ||
| borderRadius: 16, | ||
| padding: Spacing["spacing-4"], | ||
| }, | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 951994d.
export-keys.tsxnow bails out beforerequireAuthwhenPlatform.OS !== "web"(and also when on the bundled default keys), navigating back vialeaveScreen()— so a native deep link never reaches the QR.