diff --git a/package-lock.json b/package-lock.json
index d8d20c3..d80df58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,6 +41,7 @@
"react-dom": "18.2.0",
"react-i18next": "13.2.2",
"react-native": "0.73.2",
+ "react-native-animated-bottom-drawer": "0.0.23",
"react-native-animated-linear-gradient": "1.3.0",
"react-native-bars": "2.3.0",
"react-native-blur-effect": "1.1.3",
@@ -52,6 +53,7 @@
"react-native-dotenv": "3.4.9",
"react-native-linear-gradient": "2.8.3",
"react-native-nfc-manager": "3.14.11",
+ "react-native-pin-view": "3.0.3",
"react-native-progress": "5.0.1",
"react-native-qrcode-svg": "^6.3.0",
"react-native-safe-area-context": "4.7.2",
@@ -24671,6 +24673,16 @@
"react": "18.2.0"
}
},
+ "node_modules/react-native-animated-bottom-drawer": {
+ "version": "0.0.23",
+ "resolved": "https://registry.npmjs.org/react-native-animated-bottom-drawer/-/react-native-animated-bottom-drawer-0.0.23.tgz",
+ "integrity": "sha512-xOYOWxNViNno9wtYtvDpW90M5N4RDgdLq2jRjR9lxTAcpKsl93C7oex/NANQzd5aC9Vh7SEnyh0DlbSqprB6kg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-animated-linear-gradient": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-native-animated-linear-gradient/-/react-native-animated-linear-gradient-1.3.0.tgz",
@@ -24848,6 +24860,15 @@
"@expo/config-plugins": "~7.2.5"
}
},
+ "node_modules/react-native-pin-view": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/react-native-pin-view/-/react-native-pin-view-3.0.3.tgz",
+ "integrity": "sha512-0xCQBpwrP06wDjUlfwViFMzi1ph+9Y1ytYWkGtdIcy9XcR5RK94DOGDisNUb8LrvHjVIoeuoDIkYPVcvv43GGA==",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-progress": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-native-progress/-/react-native-progress-5.0.1.tgz",
diff --git a/package.json b/package.json
index b14076e..7445a2c 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"react-dom": "18.2.0",
"react-i18next": "13.2.2",
"react-native": "0.73.2",
+ "react-native-animated-bottom-drawer": "0.0.23",
"react-native-animated-linear-gradient": "1.3.0",
"react-native-bars": "2.3.0",
"react-native-blur-effect": "1.1.3",
@@ -58,6 +59,7 @@
"react-native-dotenv": "3.4.9",
"react-native-linear-gradient": "2.8.3",
"react-native-nfc-manager": "3.14.11",
+ "react-native-pin-view": "3.0.3",
"react-native-progress": "5.0.1",
"react-native-qrcode-svg": "^6.3.0",
"react-native-safe-area-context": "4.7.2",
diff --git a/src/components/PinPad/PinPad.tsx b/src/components/PinPad/PinPad.tsx
new file mode 100644
index 0000000..fece970
--- /dev/null
+++ b/src/components/PinPad/PinPad.tsx
@@ -0,0 +1,79 @@
+import { useRef, useState, useEffect } from 'react';
+import ReactNativePinView from "react-native-pin-view";
+import BottomDrawer, {
+ BottomDrawerMethods,
+} from 'react-native-animated-bottom-drawer';
+import { Icon } from "@components";
+import { faDeleteLeft } from "@fortawesome/free-solid-svg-icons";
+import * as S from "./styled";
+
+type PinViewFunctions = {
+ clearAll: () => void;
+};
+
+type PinPadProps = {
+ onPinEntered: (pin: string) => void;
+}
+
+const LeftButton = () => ;
+
+export const PinPad = (props: PinPadProps) => {
+ const pinView = useRef(null);
+ const bottomDrawerRef = useRef(null);
+ const [showRemoveButton, setShowRemoveButton] = useState(false);
+ const [enteredPin, setEnteredPin] = useState("");
+ const [buttonPressed, setButtonPressed] = useState("");
+
+ useEffect(() => {
+ if (enteredPin.length > 0) {
+ setShowRemoveButton(true)
+ } else {
+ setShowRemoveButton(false)
+ }
+ if (enteredPin.length === 4) {
+ props.onPinEntered(enteredPin)
+ }
+ }, [enteredPin]);
+
+ useEffect(() => {
+ if (buttonPressed === "custom_left" && pinView.current) {
+ pinView.current.clearAll()
+ }
+ }, [buttonPressed]);
+
+ return (
+ <>
+
+
+
+ Boltcard PIN
+
+ setEnteredPin(value)}
+ buttonAreaStyle={{ marginTop: 24 }}
+ inputAreaStyle={{ marginBottom: 24 }}
+ inputViewEmptyStyle={S.PinViewStyles.whiteBorderTransparent}
+ inputViewFilledStyle={{ backgroundColor: "#FFF" }}
+ buttonViewStyle={S.PinViewStyles.whiteBorder}
+ buttonTextStyle={{ color: "#FFF" }}
+ onButtonPress={key => setButtonPressed(key)}
+ // @ts-ignore
+ customLeftButton={showRemoveButton ? : undefined}
+ />
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/PinPad/index.ts b/src/components/PinPad/index.ts
new file mode 100644
index 0000000..3be94e5
--- /dev/null
+++ b/src/components/PinPad/index.ts
@@ -0,0 +1 @@
+export { PinPad } from "./PinPad";
diff --git a/src/components/PinPad/styled.ts b/src/components/PinPad/styled.ts
new file mode 100644
index 0000000..a05454a
--- /dev/null
+++ b/src/components/PinPad/styled.ts
@@ -0,0 +1,37 @@
+import styled from "styled-components";
+import { View, Text } from "@components";
+import { StyleSheet } from "react-native";
+
+export const PinPadContainer = styled(View)`
+ backgroundColor: transparent;
+ alignItems: center;
+ flex: 1;
+`;
+
+export const PinPadTitle = styled(Text)`
+ paddingTop: 48px;
+ paddingBottom: 24px;
+ color: rgba(255,255,255,1);
+ fontSize: 48px;
+`;
+
+export const BottomDrawerStyles = StyleSheet.create({
+ container: {
+ backgroundColor: "rgba(0,0,0,0.5)",
+ },
+ handleContainer: {
+ backgroundColor: "transparent",
+ }
+});
+
+export const PinViewStyles = StyleSheet.create({
+ whiteBorder: {
+ borderWidth: 1,
+ borderColor: "#FFF",
+ },
+ whiteBorderTransparent: {
+ borderWidth: 1,
+ borderColor: "#FFF",
+ backgroundColor: "transparent"
+ },
+});
\ No newline at end of file
diff --git a/src/components/index.ts b/src/components/index.ts
index 59dbaf9..086f6f8 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -39,3 +39,4 @@ export { SplashScreen } from "./SplashScreen";
export { StatusBar } from "./StatusBar";
export { CircleProgress } from "./CircleProgress";
export { BitcoinIcon } from "./BitcoinIcon";
+export { PinPad } from "./PinPad";
diff --git a/src/hooks/useNfc.ts b/src/hooks/useNfc.ts
index eb78537..54ccf4b 100644
--- a/src/hooks/useNfc.ts
+++ b/src/hooks/useNfc.ts
@@ -17,6 +17,31 @@ export const useNfc = () => {
const [isNfcNeedsTap, setIsNfcNeedsTap] = useState(false);
const [isNfcNeedsPermission, setIsNfcNeedsPermission] = useState(false);
const [isNfcActionSuccess, setIsNfcActionSuccess] = useState(false);
+ const [isPinRequired, setIsPinRequired] = useState(false);
+ const [isPinConfirmed, setIsPinConfirmed] = useState(false);
+
+ const [ pinResolver, setPinResolver ] = useState<{
+ resolve: (v: string) => void;
+ }>();
+
+ const setPin = (pin: string) => {
+ if(pinResolver?.resolve) {
+ pinResolver.resolve(pin);
+ }
+ }
+
+ const getPin = useCallback(async () => {
+ const pinInputPromise = () => {
+ let _pinResolver;
+ return [ new Promise(( resolve ) => {
+ _pinResolver = resolve
+ }), _pinResolver]
+ }
+
+ const [ promise, resolve ] = pinInputPromise()
+ setPinResolver({ resolve } as unknown as { resolve: (v: string) => void })
+ return promise as Promise;
+ }, []);
const setupNfc = useCallback(async () => {
if (await getIsNfcSupported()) {
@@ -49,7 +74,8 @@ export const useNfc = () => {
}, []);
const readingNfcLoop = useCallback(
- async (pr: string) => {
+ async (pr: string, amount?: number | null) => {
+
setIsNfcActionSuccess(false);
await NFC.stopRead();
@@ -73,7 +99,7 @@ export const useNfc = () => {
});
if (!isIos) {
- readingNfcLoop(pr);
+ readingNfcLoop(pr, amount);
} else {
setIsNfcAvailable(false);
}
@@ -114,19 +140,41 @@ export const useNfc = () => {
tag: "withdrawRequest";
callback: string;
k1: string;
+ pinLimit?: number;
}>(cardData);
- const { data: callbackResponseData } = await axios.get<{
- reason: { detail: string };
- status: "OK" | "ERROR";
- }>(cardDataResponse.callback, {
- params: {
- k1: cardDataResponse.k1,
- pr
+ let pin = "";
+ if (cardDataResponse.pinLimit !== undefined) {
+
+ if (!amount) {
+ error = { reason: "No amount set. Can't make withdrawRequest"}
+ } else {
+ //if the card has pin enabled
+ //check the amount didn't exceed the limit
+ const limitSat = cardDataResponse.pinLimit;
+ if (limitSat <= amount) {
+ setIsPinRequired(true);
+ pin = await getPin();
+ setIsPinConfirmed(true);
+ }
}
- });
+ } else {
+ setIsPinRequired(false);
+ }
- debitCardData = callbackResponseData;
+ if (!error) {
+ const { data: callbackResponseData } = await axios.get<{
+ reason: { detail: string };
+ status: "OK" | "ERROR";
+ }>(cardDataResponse.callback, {
+ params: {
+ k1: cardDataResponse.k1,
+ pr,
+ pin
+ }
+ })
+ debitCardData = callbackResponseData;
+ }
} else {
// const { data: cardRequest } = await axios.get<{ payLink?: string }>(
// lnHttpsRequest
@@ -190,9 +238,14 @@ export const useNfc = () => {
await NFC.stopRead();
setIsNfcLoading(false);
+ setIsPinConfirmed(false);
+ setIsPinRequired(false);
if (debitCardData?.status === "OK" && !error) {
setIsNfcActionSuccess(true);
} else {
+ if (debitCardData?.status === "ERROR" && !error) {
+ error = debitCardData;
+ }
toast.show(
typeof error?.reason === "string"
? error.reason
@@ -203,7 +256,7 @@ export const useNfc = () => {
);
if (!isIos) {
- readingNfcLoop(pr);
+ readingNfcLoop(pr, amount);
}
}
@@ -213,7 +266,7 @@ export const useNfc = () => {
}
});
},
- [toast, t]
+ [toast, t, getPin]
);
const stopNfc = useCallback(() => {
@@ -233,6 +286,9 @@ export const useNfc = () => {
isNfcNeedsTap,
isNfcNeedsPermission,
isNfcActionSuccess,
+ isPinRequired,
+ isPinConfirmed,
+ setPin,
setupNfc,
stopNfc,
readingNfcLoop
diff --git a/src/screens/Invoice/Invoice.tsx b/src/screens/Invoice/Invoice.tsx
index e8dc10e..0cad627 100644
--- a/src/screens/Invoice/Invoice.tsx
+++ b/src/screens/Invoice/Invoice.tsx
@@ -6,7 +6,8 @@ import {
CheckboxField,
ComponentStack,
Loader,
- Text
+ Text,
+ PinPad
} from "@components";
import {
faBolt,
@@ -61,7 +62,10 @@ export const Invoice = () => {
isNfcScanning,
isNfcLoading,
isNfcNeedsTap,
- isNfcActionSuccess
+ isNfcActionSuccess,
+ isPinRequired,
+ isPinConfirmed,
+ setPin
} = useNfc();
const {
@@ -98,9 +102,9 @@ export const Invoice = () => {
useEffect(() => {
if (isNfcAvailable && !isNfcNeedsTap && lightningInvoice) {
- void readingNfcLoop(lightningInvoice);
+ void readingNfcLoop(lightningInvoice, satoshis);
}
- }, [readingNfcLoop, isNfcAvailable, isNfcNeedsTap, lightningInvoice]);
+ }, [readingNfcLoop, isNfcAvailable, isNfcNeedsTap, lightningInvoice, satoshis]);
useEffect(() => {
if (isNfcActionSuccess) {
@@ -164,7 +168,7 @@ export const Invoice = () => {
type: "bitcoin",
title: t("startScanning"),
onPress: () => {
- void readingNfcLoop(lightningInvoice);
+ void readingNfcLoop(lightningInvoice, satoshis);
}
}
}
@@ -280,6 +284,8 @@ export const Invoice = () => {
onPress={onReturnToHome}
/>
+ ) : isPinRequired && !isPinConfirmed ? (
+
) : isNfcLoading || isNfcScanning ? (