You need to sign in or sign up before continuing.
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 type { DjangoPaginated, PaymentType } from "./types";
import type {
DjangoPaginated,
OmTransaction,
OmInitializationPayload as OmTransactionInitializationPayload,
OmInitializationResponse as OmTransactionInitializationResponse,
PaymentType,
} from "./types";
export const getPaymentTypes = () => {
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 {
last_name: string;
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 type { PaymentStackScreenComponentProps } from "@/navigations/Types";
import { asp as g } from "@asp/asp";
import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BeasyLogoIcon from "@components/BeasyLogoIcon";
import Button from "@components/Button";
import GoBackIconButton from "@components/GoBackIconButton";
import InputWithTopLabel from "@components/InputWithTopLabel";
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 * as Button from "@components/ButtonNew";
import * as Input from "@components/InputNew";
import * as Modal from "@components/Modal";
import { LOG } from "@logger";
import { useCallback, useState } from "react";
import { Keyboard, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useMutation } from "@tanstack/react-query";
import type { AxiosError } from "axios";
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 PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountInputScreen"> = ({
......@@ -21,158 +20,161 @@ const PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountI
navigation,
}) => {
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);
}
const [amount, setAmount] = useState(0);
const [error, setError] = useState("");
const appState = useRef(AppState.currentState);
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 () => {
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;
const omTransactionVerification = useMutation({
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();
}
default:
log.info("Navigating to numberAndOtpForPaymentScreen");
navigation.navigate("numberAndOtpForPaymentScreen");
break;
}
}, [
paymentType,
navigation,
amountToPay,
orangePaymentTransactionHandler,
waveTransactionHandler,
]);
appState.current = nextAppState;
console.log("AppState", appState.current);
});
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 (
<BeasyDefaultBackgroundWrapper>
{/* <SafeAreaView> */}
{transactionsStatusMutation.isPending && <LoadingScreen />}
<Box
style={{
height: "100%",
width: "100%",
// marginTop: insets.top,
}}
<BarnoinPayBackground style={[g.flex_col, g.gap_lg, g.relative]}>
<View style={[g.px_lg]}>
<BeasyLogoIcon />
</View>
<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>
<View
style={[
g.flex_1,
g.p_5xl,
g.gap_5xl,
{ backgroundColor: "white", borderTopLeftRadius: 20, borderTopRightRadius: 20 },
]}
>
<Box
px={"l"}
flexDirection={"row"}
justifyContent={"space-between"}
alignItems={"center"}
mb={"m"}
>
<BeasyLogoIcon />
<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)}
<PaymentType type={route.params.paymentType.code} />
<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))}
/>
</Box>
<Button
onPress={handlePaymentButton}
variant={"full"}
textVariants={"primary"}
label={`${isWaitingForOmPaymentUrl ? "Chargement..." : "Payer"} `}
/>
</Box>
</Box>
{/* </SafeAreaView> */}
</BeasyDefaultBackgroundWrapper>
);
};
</Input.FieldContainer>
</Input.Container>
export default PaymentAmountInputScreen;
<Button.Container onPress={handlePayment}>
<Button.Label>Payer</Button.Label>
</Button.Container>
</View>
const LoadingScreen = () => {
return (
<View
style={{
flex: 1,
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center",
zIndex: 1000000000000,
backgroundColor: "black",
opacity: 0.5,
position: "absolute",
}}
>
<Text color={"primary"}>Verification du status de la transaction.</Text>
<Text>Veuillez patienter</Text>
</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