From 0ddf355bd3bfbe200eba325074bc671dbffd22d5 Mon Sep 17 00:00:00 2001 From: G Date: Sat, 6 Sep 2025 13:30:12 +0000 Subject: [PATCH] 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. --- src/features/pay/api.ts | 42 +++++++++++++++++++++++++++++++++++++++++- src/features/pay/types.ts | 31 +++++++++++++++++++++++++++++++ src/screens/PaymentAmountInputScreen.tsx | 314 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------------ 3 files changed, 230 insertions(+), 157 deletions(-) diff --git a/src/features/pay/api.ts b/src/features/pay/api.ts index fae028a..34b39f0 100644 --- a/src/features/pay/api.ts +++ b/src/features/pay/api.ts @@ -1,6 +1,46 @@ 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>>("/operateur/"); }; + +// OM + +export const omInitializeTransaction = (payload: OmTransactionInitializationPayload) => { + return axiosInstance.post("/transactions/", payload); +}; + +export const omVerifyTransactionState = (orderId: string) => { + return axiosInstance.get(`/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); + } +}; diff --git a/src/features/pay/types.ts b/src/features/pay/types.ts index 2a01fa1..2f6c32c 100644 --- a/src/features/pay/types.ts +++ b/src/features/pay/types.ts @@ -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; + }; +} diff --git a/src/screens/PaymentAmountInputScreen.tsx b/src/screens/PaymentAmountInputScreen.tsx index 94460c6..d551f8c 100644 --- a/src/screens/PaymentAmountInputScreen.tsx +++ b/src/screens/PaymentAmountInputScreen.tsx @@ -1,19 +1,18 @@ -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 ( - - {/* */} - - {transactionsStatusMutation.isPending && } - + + + + + + Montant à payé + {amount} + + + - - - navigation.goBack()} /> - - {/* - - */} - - Montant à payé - - {amountToPay} - - - - - {}} paymentMethod={paymentType} /> - - - updateAmountToPay(e)} + + + Entrer le montant + + setAmount(Number(v))} /> - -