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 amountParsed = Number.parseInt(amount);
if (!Number.isNaN(amountParsed)) {
return setAmountToPay(amountParsed);
}
return setAmountToPay(0); const [amount, setAmount] = useState(0);
}; const [error, setError] = useState("");
const appState = useRef(AppState.currentState);
const handlePaymentButton = useCallback(async () => { const handlePayment = async () => {
switch (paymentType) {
case "OM": {
Keyboard.dismiss();
log.info("OM so we stays on screen !!");
// await orangePaymentSequence();
try { try {
await orangePaymentTransactionHandler(amountToPay); const code = route.params.paymentType.code;
// navigation.getParent()?.navigate("paymentResultScreen"); if (code === "OM") {
} catch (error) { // TODO: ASK THE BOSS WHY THE PAYLOAD IS MOSTLY USELESS HERE.
log.error("handlePaymentButton |", error); const res = await omInitializeTransaction({
} type_paiement: 1,
marchand: "1",
break; 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,
});
} }
case "WAVE": {
try {
log.info("Wave so we stay on screen.");
await waveTransactionHandler(amountToPay);
} catch (error) { } catch (error) {
log.error("handlePaymentButton Wave|", error); const err = error as AxiosError;
} setError(JSON.stringify(err.response?.data) || err.message);
break;
} }
default: };
log.info("Navigating to numberAndOtpForPaymentScreen");
navigation.navigate("numberAndOtpForPaymentScreen"); const omTransactionVerification = useMutation({
break; mutationFn: () => omVerifyTransactionStateWithTimeout("1", 10000, 2000),
onSuccess: (_data) => {
navigation?.getParent()?.navigate("paymentResultScreen");
},
onError: (err: AxiosError) => {
setError(JSON.stringify(err.response?.data) || err.message);
},
});
useEffect(() => {
if (route.params.paymentType.code !== "OM") return; // only for orange money payment should this effect be run
const subscription = AppState.addEventListener("change", (nextAppState) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active" &&
route.params.paymentType.code === "OM"
) {
console.log("App has come to the foreground!");
omTransactionVerification.mutate();
} }
}, [
paymentType, appState.current = nextAppState;
navigation,
amountToPay, console.log("AppState", appState.current);
orangePaymentTransactionHandler, });
waveTransactionHandler,
]); return () => {
subscription.remove();
};
}, [route, omTransactionVerification]);
// 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]}>
{transactionsStatusMutation.isPending && <LoadingScreen />}
<Box
style={{
height: "100%",
width: "100%",
// marginTop: insets.top,
}}
>
<Box
px={"l"}
flexDirection={"row"}
justifyContent={"space-between"}
alignItems={"center"}
mb={"m"}
>
<BeasyLogoIcon /> <BeasyLogoIcon />
<GoBackIconButton onPress={() => navigation.goBack()} /> </View>
</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>
<Button
onPress={handlePaymentButton}
variant={"full"}
textVariants={"primary"}
label={`${isWaitingForOmPaymentUrl ? "Chargement..." : "Payer"} `}
/>
</Box>
</Box>
{/* </SafeAreaView> */}
</BeasyDefaultBackgroundWrapper>
);
};
export default PaymentAmountInputScreen; <View style={[g.px_lg]}>
<Text style={[g.font_bold, g.text_2xl]}>Montant à payé </Text>
<Text style={[g.font_bold, g.text_2xl]}>{amount}</Text>
</View>
const LoadingScreen = () => {
return (
<View <View
style={{ style={[
flex: 1, g.flex_1,
width: "100%", g.p_5xl,
height: "100%", g.gap_5xl,
justifyContent: "center", { backgroundColor: "white", borderTopLeftRadius: 20, borderTopRightRadius: 20 },
alignItems: "center", ]}
zIndex: 1000000000000,
backgroundColor: "black",
opacity: 0.5,
position: "absolute",
}}
> >
<Text color={"primary"}>Verification du status de la transaction.</Text> <PaymentType type={route.params.paymentType.code} />
<Text>Veuillez patienter</Text> <Input.Container>
<Input.Header>Entrer le montant</Input.Header>
<Input.FieldContainer>
<Input.Field
keyboardType="number-pad"
value={amount === 0 ? "" : amount.toString()}
onChangeText={(v) => setAmount(Number(v))}
/>
</Input.FieldContainer>
</Input.Container>
<Button.Container onPress={handlePayment}>
<Button.Label>Payer</Button.Label>
</Button.Container>
</View> </View>
<Modal.RnModal visible={!!error} style={{ backgroundColor: "rgba(0, 0, 0, 0.25)" }}>
<Modal.OuterView style={[g.p_md, g.shadow_elevated, { backgroundColor: "white" }]}>
<Text>{error}</Text>
<Button.Container onPress={() => setError("")}>
<Button.Label>OK</Button.Label>
</Button.Container>
</Modal.OuterView>
</Modal.RnModal>
<Modal.RnModal
visible={omTransactionVerification.isPending}
style={{ backgroundColor: "rgba(0, 0, 0, 0.25)" }}
>
<Modal.OuterView style={[g.p_md, g.shadow_elevated, { backgroundColor: "white" }]}>
<Text>Loading...</Text>
</Modal.OuterView>
</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