Commit 0ddf355b by G

feat: Payment processing utilizing Orange Money as the payment gateway, with…

feat: Payment processing utilizing Orange Money as the payment gateway, with browser support and verification functionality activated upon browser closure. This may need more work once the api is fixed.
parent b7457249
import { axiosInstance } from "@/axios"; import { axiosInstance } from "@/axios";
import type { DjangoPaginated, PaymentType } from "./types"; import type {
DjangoPaginated,
OmTransaction,
OmInitializationPayload as OmTransactionInitializationPayload,
OmInitializationResponse as OmTransactionInitializationResponse,
PaymentType,
} from "./types";
export const getPaymentTypes = () => { export const getPaymentTypes = () => {
return axiosInstance.get<DjangoPaginated<Record<string, PaymentType>>>("/operateur/"); return axiosInstance.get<DjangoPaginated<Record<string, PaymentType>>>("/operateur/");
}; };
// OM
export const omInitializeTransaction = (payload: OmTransactionInitializationPayload) => {
return axiosInstance.post<OmTransactionInitializationResponse>("/transactions/", payload);
};
export const omVerifyTransactionState = (orderId: string) => {
return axiosInstance.get<OmTransaction>(`/api/TransactionCheckStatus/${orderId}/`);
};
export const omVerifyTransactionStateWithTimeout = async (
orderId: string,
timeout: number,
retryAfter = 5000,
) => {
const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
const now = Date.now();
while (true) {
const result = await omVerifyTransactionState(orderId);
if (result.data.status !== "INITIATED") {
return result;
}
if (Date.now() - now > timeout) {
throw result;
}
await sleep(retryAfter);
}
};
...@@ -34,3 +34,34 @@ export interface IuserInformations { ...@@ -34,3 +34,34 @@ export interface IuserInformations {
last_name: string; last_name: string;
marchand: Merchant; marchand: Merchant;
} }
// ORANGE MONEY
export type OmInitializationPayload = {
type_paiement: number;
marchand: string;
service: string;
montant: number;
commentaire: string;
numero: string;
};
export type OmInitializationResponse = {
status: number;
message: string;
pay_token: string;
payment_url: string;
notif_token: string;
order_id: string;
};
export type OmTransactionState = "INITIATED" | "SUCCESS" | "FAILED";
export interface OmTransaction {
status: OmTransactionState;
code: number;
message: {
status: OmTransactionState;
order_id: string;
txnid?: string;
};
}
import { useModalsManagerContext } from "@/contexts/ModalsManagerContext"; import { asp as g } from "@asp/asp";
import type { PaymentStackScreenComponentProps } from "@/navigations/Types"; import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BeasyLogoIcon from "@components/BeasyLogoIcon"; import BeasyLogoIcon from "@components/BeasyLogoIcon";
import Button from "@components/Button"; import * as Button from "@components/ButtonNew";
import GoBackIconButton from "@components/GoBackIconButton"; import * as Input from "@components/InputNew";
import InputWithTopLabel from "@components/InputWithTopLabel"; import * as Modal from "@components/Modal";
import PaymentOption from "@components/PaymentOption";
import BeasyDefaultBackgroundWrapper from "@components/backgrounds/BeasyDefaultBackground";
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import useOrangeMoney from "@hooks/useOrangeMoney";
import useWave from "@hooks/useWave";
import { LOG } from "@logger"; import { LOG } from "@logger";
import { useCallback, useState } from "react"; import { useMutation } from "@tanstack/react-query";
import { Keyboard, View } from "react-native"; import type { AxiosError } from "axios";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import * as WebBrowser from "expo-web-browser";
import { useEffect, useRef, useState } from "react";
import { AppState, Text, View } from "react-native";
import { omInitializeTransaction, omVerifyTransactionStateWithTimeout } from "@/features/pay/api";
import PaymentType from "@/features/pay/components/PaymentType";
import type { PaymentStackScreenComponentProps } from "@/navigations/types";
const log = LOG.extend("PaymentAmountInputScreen"); const log = LOG.extend("PaymentAmountInputScreen");
const PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountInputScreen"> = ({ const PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountInputScreen"> = ({
...@@ -21,158 +20,161 @@ const PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountI ...@@ -21,158 +20,161 @@ const PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountI
navigation, navigation,
}) => { }) => {
log.debug("PaymentAmountInputScreen"); log.debug("PaymentAmountInputScreen");
const { paymentType } = route.params;
const [amountToPay, setAmountToPay] = useState(0);
const { showModal, closeModal } = useModalsManagerContext();
const {
orangeTransactionInitializerMutation,
handlePaymentUsingBrowser,
isBrowserOpen,
isWaitingForOmPaymentUrl,
isCheckingForTransactionStatus,
transactionsStatusMutation,
orangePaymentTransactionHandler,
} = useOrangeMoney(navigation);
const { waveTransactionHandler } = useWave(navigation);
const insets = useSafeAreaInsets();
log.debug({
isWaitingForOmPaymentUrl,
isCheckingForTransactionStatus,
isBrowserOpen,
});
const updateAmountToPay = (amount: string) => { const [amount, setAmount] = useState(0);
const amountParsed = Number.parseInt(amount); const [error, setError] = useState("");
if (!Number.isNaN(amountParsed)) { const appState = useRef(AppState.currentState);
return setAmountToPay(amountParsed);
}
return setAmountToPay(0); const handlePayment = async () => {
try {
const code = route.params.paymentType.code;
if (code === "OM") {
// TODO: ASK THE BOSS WHY THE PAYLOAD IS MOSTLY USELESS HERE.
const res = await omInitializeTransaction({
type_paiement: 1,
marchand: "1",
service: "1",
montant: amount,
commentaire: "un commentaire",
numero: "0707070707",
});
WebBrowser.openBrowserAsync(res.data.payment_url);
} else {
navigation.navigate("numberAndOtpForPaymentScreen", {
paymentType: route.params.paymentType,
amount: amount,
});
}
} catch (error) {
const err = error as AxiosError;
setError(JSON.stringify(err.response?.data) || err.message);
}
}; };
const handlePaymentButton = useCallback(async () => { const omTransactionVerification = useMutation({
switch (paymentType) { mutationFn: () => omVerifyTransactionStateWithTimeout("1", 10000, 2000),
case "OM": { onSuccess: (_data) => {
Keyboard.dismiss(); navigation?.getParent()?.navigate("paymentResultScreen");
log.info("OM so we stays on screen !!"); },
// await orangePaymentSequence(); onError: (err: AxiosError) => {
try { setError(JSON.stringify(err.response?.data) || err.message);
await orangePaymentTransactionHandler(amountToPay); },
// navigation.getParent()?.navigate("paymentResultScreen"); });
} catch (error) {
log.error("handlePaymentButton |", error); useEffect(() => {
} if (route.params.paymentType.code !== "OM") return; // only for orange money payment should this effect be run
const subscription = AppState.addEventListener("change", (nextAppState) => {
break; if (
} appState.current.match(/inactive|background/) &&
case "WAVE": { nextAppState === "active" &&
try { route.params.paymentType.code === "OM"
log.info("Wave so we stay on screen."); ) {
await waveTransactionHandler(amountToPay); console.log("App has come to the foreground!");
} catch (error) { omTransactionVerification.mutate();
log.error("handlePaymentButton Wave|", error);
}
break;
} }
default:
log.info("Navigating to numberAndOtpForPaymentScreen"); appState.current = nextAppState;
navigation.navigate("numberAndOtpForPaymentScreen");
break; console.log("AppState", appState.current);
} });
}, [
paymentType, return () => {
navigation, subscription.remove();
amountToPay, };
orangePaymentTransactionHandler, }, [route, omTransactionVerification]);
waveTransactionHandler,
]); // switch (paymentType) {
// case "OM": {
// Keyboard.dismiss();
// log.info("OM so we stays on screen !!");
// // await orangePaymentSequence();
// try {
// await orangePaymentTransactionHandler(amountToPay);
// // navigation.getParent()?.navigate("paymentResultScreen");
// } catch (error) {
// log.error("handlePaymentButton |", error);
// }
// break;
// }
// case "WAVE": {
// try {
// log.info("Wave so we stay on screen.");
// await waveTransactionHandler(amountToPay);
// } catch (error) {
// log.error("handlePaymentButton Wave|", error);
// }
// break;
// }
// default:
// log.info("Navigating to numberAndOtpForPaymentScreen");
// navigation.navigate("numberAndOtpForPaymentScreen");
// break;
// }
// }, [
// paymentType,
// navigation,
// amountToPay,
// orangePaymentTransactionHandler,
// waveTransactionHandler,
// ]);
return ( return (
<BeasyDefaultBackgroundWrapper> <BarnoinPayBackground style={[g.flex_col, g.gap_lg, g.relative]}>
{/* <SafeAreaView> */} <View style={[g.px_lg]}>
<BeasyLogoIcon />
{transactionsStatusMutation.isPending && <LoadingScreen />} </View>
<Box
style={{ <View style={[g.px_lg]}>
height: "100%", <Text style={[g.font_bold, g.text_2xl]}>Montant à payé </Text>
width: "100%", <Text style={[g.font_bold, g.text_2xl]}>{amount}</Text>
// marginTop: insets.top, </View>
}}
<View
style={[
g.flex_1,
g.p_5xl,
g.gap_5xl,
{ backgroundColor: "white", borderTopLeftRadius: 20, borderTopRightRadius: 20 },
]}
> >
<Box <PaymentType type={route.params.paymentType.code} />
px={"l"} <Input.Container>
flexDirection={"row"} <Input.Header>Entrer le montant</Input.Header>
justifyContent={"space-between"} <Input.FieldContainer>
alignItems={"center"} <Input.Field
mb={"m"} keyboardType="number-pad"
> value={amount === 0 ? "" : amount.toString()}
<BeasyLogoIcon /> onChangeText={(v) => setAmount(Number(v))}
<GoBackIconButton onPress={() => navigation.goBack()} />
</Box>
{/* <Box height={150} alignItems={"center"} justifyContent={"center"}>
<BalanceContainer balance={78000} label="Total des ventes" />
</Box> */}
<Box height={90} padding={"s"} paddingLeft={"l"}>
<Text color={"black"}>Montant à payé</Text>
<Text color={"black"} variant={"header"}>
{amountToPay}
</Text>
</Box>
<Box
p={"l"}
paddingTop={"xl"}
backgroundColor={"white"}
flex={1}
borderTopLeftRadius={20}
borderTopRightRadius={20}
>
<Box width={75} height={50} mb={"l"} borderRadius={10} overflow={"hidden"}>
<PaymentOption onPress={() => {}} paymentMethod={paymentType} />
</Box>
<Box mb={"xl"}>
<InputWithTopLabel
label="Entrez le montant"
autoFocus={true}
keyboardType="numeric"
onChangeText={(e) => updateAmountToPay(e)}
/> />
</Box> </Input.FieldContainer>
<Button </Input.Container>
onPress={handlePaymentButton}
variant={"full"}
textVariants={"primary"}
label={`${isWaitingForOmPaymentUrl ? "Chargement..." : "Payer"} `}
/>
</Box>
</Box>
{/* </SafeAreaView> */}
</BeasyDefaultBackgroundWrapper>
);
};
export default PaymentAmountInputScreen; <Button.Container onPress={handlePayment}>
<Button.Label>Payer</Button.Label>
</Button.Container>
</View>
const LoadingScreen = () => { <Modal.RnModal visible={!!error} style={{ backgroundColor: "rgba(0, 0, 0, 0.25)" }}>
return ( <Modal.OuterView style={[g.p_md, g.shadow_elevated, { backgroundColor: "white" }]}>
<View <Text>{error}</Text>
style={{
flex: 1, <Button.Container onPress={() => setError("")}>
width: "100%", <Button.Label>OK</Button.Label>
height: "100%", </Button.Container>
justifyContent: "center", </Modal.OuterView>
alignItems: "center", </Modal.RnModal>
zIndex: 1000000000000,
backgroundColor: "black", <Modal.RnModal
opacity: 0.5, visible={omTransactionVerification.isPending}
position: "absolute", style={{ backgroundColor: "rgba(0, 0, 0, 0.25)" }}
}} >
> <Modal.OuterView style={[g.p_md, g.shadow_elevated, { backgroundColor: "white" }]}>
<Text color={"primary"}>Verification du status de la transaction.</Text> <Text>Loading...</Text>
<Text>Veuillez patienter</Text> </Modal.OuterView>
</View> </Modal.RnModal>
</BarnoinPayBackground>
); );
}; };
export default PaymentAmountInputScreen;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment