Commit 0757fd97 by G

Merge branch 'refactor/major-changes' into dev

ClOSES #8
parents 41b04c8b 71609896
{
"*": ["biome check --apply --no-errors-on-unmatched --files-ignore-unknown=true"]
"*": ["biome check --write --no-errors-on-unmatched"]
}
......@@ -2,7 +2,8 @@
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
"source.organizeImports.biome": "explicit"
// "source.fixAll.biome": "explicit"
},
"npm.packageManager": "yarn",
"editor.defaultFormatter": "biomejs.biome",
......
import { ModalsManagerProvider } from "@/contexts/ModalsManagerContext";
import { UserAuthenticationContextProvider } from "@/contexts/UserAuthenticationContext";
import { AppMainStackNavigatorAuthWrapper } from "@/navigations/AppMainStackNavigator";
import theme from "@/themes/Theme";
import ProvideQueryClient from "@components/providers_wrappers/ProvideQueryClient";
import { LOG } from "@logger";
import { NavigationContainer } from "@react-navigation/native";
import { DefaultTheme, NavigationContainer } from "@react-navigation/native";
import { ThemeProvider } from "@shopify/restyle";
import ModalContainer from "react-modal-promise";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { injectStoreIntoAxiosInstance } from "@/axios";
import ProvideQueryClient from "@/contexts/ProvideQueryClient";
import { AppMainStackNavigatorAuthWrapper } from "@/navigations/AppMainStackNavigator";
import theme from "@/themes/Theme";
import "react-native-gesture-handler";
import "react-native-reanimated";
import { StatusBar } from "expo-status-bar";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Provider } from "react-redux";
import { store } from "./src/redux";
const log = LOG.extend("App");
injectStoreIntoAxiosInstance(store);
export default function App() {
log.verbose("App started...");
return (
<Provider store={store}>
<GestureHandlerRootView style={{ flex: 1 }}>
<ThemeProvider theme={theme}>
<ModalsManagerProvider>
<SafeAreaProvider>
<ProvideQueryClient>
<UserAuthenticationContextProvider>
<NavigationContainer>
<NavigationContainer
theme={{
...DefaultTheme,
colors: {
...DefaultTheme.colors,
background: "white",
},
}}
>
<AppMainStackNavigatorAuthWrapper />
<StatusBar translucent backgroundColor="transparent" style="dark" />
</NavigationContainer>
</UserAuthenticationContextProvider>
</ProvideQueryClient>
</SafeAreaProvider>
</ModalsManagerProvider>
<ModalContainer />
</ThemeProvider>
</GestureHandlerRootView>
</Provider>
);
}
......@@ -15,9 +15,21 @@
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.idrissouattara.beasy-mobile"
"bundleIdentifier": "com.idrissouattara.barnoinpay-mobile",
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false,
"NSAppTransportSecurity": {
"NSExceptionDomains": {
"51.77.152.180": {
"NSIncludesSubdomains": true,
"NSExceptionAllowsInsecureHTTPLoads": true
}
}
}
}
},
"android": {
"softwareKeyboardLayoutMode": "pan",
"adaptiveIcon": {
"foregroundImage": "./assets/beasy_icon.png",
"backgroundColor": "#00875A"
......@@ -26,7 +38,7 @@
"android.permission.USE_BIOMETRIC",
"android.permission.USE_FINGERPRINT"
],
"package": "com.idrissouattara.beasymobile"
"package": "com.idrissouattara.barnoinpaymobile"
},
"web": {
"favicon": "./assets/favicon.png"
......@@ -35,13 +47,13 @@
[
"expo-local-authentication",
{
"faceIDPermission": "Allow B-Easy to use Face ID."
"faceIDPermission": "Allow BarnoinPay to use Face ID."
}
],
[
"expo-dev-launcher",
{
"launchMode": "most-recent"
"launchMode": "launcher"
}
],
[
......
module.exports = function(api) {
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin']
};
};
{
"$schema": "https://biomejs.dev/schemas/1.7.1/schema.json",
"organizeImports": {
"enabled": true
},
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"formatter": {
"enabled": true,
"formatWithErrors": false,
"ignore": [],
"includes": ["**"],
"attributePosition": "auto",
"indentStyle": "tab",
"indentWidth": 4,
......@@ -19,10 +17,12 @@
"recommended": true,
"correctness": {
"noUnusedImports": "warn",
"noUnusedVariables": "warn"
"noUnusedVariables": "warn",
"useUniqueElementIds": "warn"
},
"complexity": {
"noExcessiveCognitiveComplexity": "error"
"noExcessiveCognitiveComplexity": "error",
"noUselessFragments": "off"
},
"style": {
"useConsistentArrayType": "warn",
......@@ -36,23 +36,49 @@
"useNamingConvention": {
"level": "error",
"options": {
"requireAscii": true
"requireAscii": true,
"conventions": [
{
"selector": {
"kind": "objectLiteralProperty"
},
"formats": ["snake_case", "camelCase"]
},
{
"selector": {
"kind": "typeMember"
},
"formats": ["snake_case", "camelCase", "PascalCase"]
},
{
"selector": {
"kind": "typeMember"
},
"formats": ["snake_case", "camelCase", "PascalCase"]
}
]
}
},
"useShorthandAssign": "warn"
},
"suspicious": {
"noConsoleLog": "warn",
"useAwait": "warn"
"useAwait": "warn",
"noConsole": { "level": "warn", "options": { "allow": ["log"] } },
"noDuplicateElseIf": "warn",
"noAssignInExpressions": "off"
},
"nursery": {
"noDuplicateElseIf": "warn"
}
"nursery": {}
}
},
"overrides": [
{
"include": ["src/utils/*"],
"includes": [
"**/src/utils/**/*",
"**/src/api/**/*",
"**/types.ts",
"**/*.ts",
"**/hooks/**/*"
],
"linter": {
"rules": {
"style": {
......@@ -65,6 +91,16 @@
}
}
}
},
{
"includes": ["**/src/appStylingPrimitives/**/*"],
"linter": {
"rules": {
"style": {
"useNamingConvention": "off"
}
}
}
}
],
"vcs": {
......@@ -72,6 +108,16 @@
"clientKind": "git"
},
"files": {
"ignore": ["babel.config.js", "eas.json"]
"includes": [
"**",
"!**/*.config.js",
"!**/*.d.ts",
"!**/dist",
"!**/android",
"!**/ios",
"!**/node_modules",
"!**/.expo",
"!**/.vscode"
]
}
}
......@@ -5,16 +5,19 @@
},
"build": {
"development": {
"env": {
"EXPO_PUBLIC_API_URL": "http://192.168.1.223:8001"
},
"developmentClient": true,
"distribution": "internal",
"channel": "development"
"channel": "development",
"android": {
"buildType": "apk"
},
"env": {
"EXPO_PUBLIC_API_URL": "http://51.77.152.180:8000"
}
},
"preview": {
"env": {
"EXPO_PUBLIC_API_URL": "http://192.168.1.223:8001"
"EXPO_PUBLIC_API_URL": "http://51.77.152.180:8000"
},
"distribution": "internal",
"android": {
......
{
"name": "beasy-mobile",
"name": "barnoinpay-mobile",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
......@@ -11,19 +11,26 @@
"format": "biome format --no-errors-on-unmatched --write .",
"lint": "biome lint --no-errors-on-unmatched --apply .",
"biome-check": "biome check --no-errors-on-unmatched --apply .",
"prepare": "husky"
"prepare": "husky",
"version": "conventional-changelog && git add CHANGELOG.md"
},
"dependencies": {
"@gorhom/bottom-sheet": "^5",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-navigation/bottom-tabs": "^7.0.0-alpha.22",
"@react-navigation/native": "^7.0.0-alpha.18",
"@react-navigation/native-stack": "^7.0.0-alpha.20",
"@react-navigation/bottom-tabs": "^7.4.6",
"@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.3.25",
"@reduxjs/toolkit": "^2.8.2",
"@shopify/flash-list": "1.7.6",
"@shopify/restyle": "^2.4.4",
"@tanstack/react-query": "^5.35.1",
"axios": "^1.6.8",
"expo": "53.0.22",
"expo-build-properties": "~0.14.8",
"expo-checkbox": "~4.1.4",
"expo-contacts": "~14.2.5",
"expo-dev-client": "~5.2.4",
"expo-image": "~2.4.0",
"expo-local-authentication": "~16.0.5",
"expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3",
......@@ -32,28 +39,35 @@
"moment": "^2.30.1",
"moti": "^0.29.0",
"react": "19.0.0",
"react-modal-promise": "^1.0.2",
"react-native": "0.79.5",
"react-native-base64": "^0.2.1",
"react-native-date-picker": "5.0.12",
"react-native-gesture-handler": "~2.24.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-logs": "^5.1.0",
"react-native-qrcode-svg": "^6.3.1",
"react-native-keyboard-controller": "^1.18.5",
"react-native-logs": "^5.3.0",
"react-native-mask-input": "^1.2.3",
"react-native-mmkv": "^3.3.0",
"react-native-qrcode-svg": "^6.3.15",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-svg": "15.11.2",
"react-native-vector-icons": "^10.1.0"
"react-native-vector-icons": "^10.1.0",
"react-redux": "^9.2.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@biomejs/biome": "1.7.1",
"@biomejs/biome": "^2.2.0",
"@react-native-community/cli": "^15.1.2",
"@types/react": "~19.0.10",
"@types/react-native-base64": "^0.2.2",
"@types/react-native-vector-icons": "^6.4.18",
"conventional-changelog": "^7.1.1",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"prop-types": "^15.8.1",
"react-native-svg-transformer": "^1.5.0",
"typescript": "~5.8.3"
},
"private": true
......
# A set of primitives to make styling components faster through the use of tailwind type anotations.
import { Platform } from "react-native";
export const TRACKING = Platform.OS === "android" ? 0.1 : 0;
export const space = {
_2xs: 2,
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
_2xl: 24,
_3xl: 28,
_4xl: 32,
_5xl: 40,
} as const;
export const fontSize = {
_2xs: 10,
xs: 12,
sm: 14,
md: 16,
lg: 18,
xl: 20,
_2xl: 22,
_3xl: 26,
_4xl: 32,
_5xl: 40,
} as const;
export const lineHeight = {
none: 1,
normal: 1.5,
relaxed: 1.625,
} as const;
export const borderRadius = {
_2xs: 2,
xs: 4,
sm: 8,
md: 12,
lg: 16,
full: 999,
} as const;
/**
* These correspond to Inter font files we actually load.
*/
export const fontWeight = {
thin: "100",
extralight: "200",
light: "300",
normal: "400",
medium: "500",
semibold: "600",
bold: "700",
extrabold: "800",
black: "900",
} as const;
import { LOG } from "@logger";
import axios from "axios";
import type { AppReduxStore } from "@/redux";
const log = LOG.extend("AxiosInstance");
let store: AppReduxStore;
export const injectStoreIntoAxiosInstance = (_store: AppReduxStore) => {
store = _store;
};
export const axiosInstance = axios.create({
// biome-ignore lint/style/useNamingConvention: <Axios config require baseURL.>
baseURL: process.env.EXPO_PUBLIC_API_URL,
});
axiosInstance.interceptors.request.use(
(config) => {
config.headers.authorization = `Bearer ${store.getState().auth.token.access}`;
config.auth = {
username: "admin",
password: "admin",
};
log.http({ "Request Interceptor Config": config });
return config;
},
(error) => {
log.error({ "Request Interceptor Error": error });
return Promise.reject(error);
},
);
axiosInstance.interceptors.response.use(
(response) => {
log.http({ "Response Interceptor Response Data": response.data });
return response;
},
(error) => {
log.error({ "Response Interceptor Error": error });
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 400:
log.error({ message: "Bad Request", status, data });
break;
case 401:
log.error({
message: "Unauthorized, setting auth status to False",
status,
data,
});
// Handle unauthorized access, e.g., redirect to login
break;
case 404:
log.error({ message: "Not Found", status, data });
break;
case 500:
log.error("Server Error:", "Please try again later");
// Display a user-friendly message, log the error
break;
default:
log.error({ message: "Unknown Error", status, data });
break;
}
}
return Promise.reject(error);
},
);
import { asp as g } from "@asp/asp";
import Entypo from "@expo/vector-icons/Entypo";
import { ImageBackground } from "expo-image";
import * as LocalAuthentication from "expo-local-authentication";
import { type FC, useState } from "react";
import type { ViewProps } from "react-native";
import { Text, TouchableOpacity } from "react-native";
type BalanceProps = Omit<ViewProps, "children"> & { amount: number };
export const Balance: FC<BalanceProps> = ({ style, amount = 0, ...rest }) => {
const [showBalance, setShowBalance] = useState(false);
const handleLocalAuthentication = async () => {
console.log("handleLocalAuthentication :: start");
if (showBalance) {
return setShowBalance(false);
}
const result = await LocalAuthentication.authenticateAsync();
if (result.success) {
setShowBalance(true);
}
console.log("handleLocalAuthentication :: end", result);
};
return (
<ImageBackground
style={[
g.p_md,
g.align_center,
g.justify_center,
g.rounded_md,
g.overflow_hidden,
{ height: 130, width: 300 },
style,
]}
source={require("@assets/balance_container.png")}
cachePolicy={"memory-disk"}
{...rest}
>
{showBalance ? (
<TouchableOpacity onPress={() => setShowBalance(false)}>
<Text style={[g.font_bold, g.text_2xl]}>{amount}</Text>
</TouchableOpacity>
) : (
<Entypo
name="eye-with-line"
size={24}
color="black"
onPress={handleLocalAuthentication}
/>
)}
</ImageBackground>
);
};
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import Card from "@re-card";
import { images } from "@styles/Commons";
import * as LocalAuthentication from "expo-local-authentication";
import { useState } from "react";
import { Image, TouchableOpacity } from "react-native";
type Props = { balance: number; label: string };
const BalanceContainer = ({ label, balance }: Props) => {
const [showBalance, setShowBalance] = useState(false);
const handleLocalAuthentication = async () => {
console.log("handleLocalAuthentication :: start");
if (showBalance) {
return setShowBalance(false);
}
const result = await LocalAuthentication.authenticateAsync();
if (result.success) {
setShowBalance(true);
}
console.log("handleLocalAuthentication :: end", result);
};
return (
<Card position={"relative"} variant={"balanceContainer"}>
<Card variant={"balanceContainer"} position={"absolute"}>
<Image
source={require("../../assets/balance_container.png")}
style={images.cover}
/>
</Card>
<Box alignItems={"center"} gap={"s"}>
<TouchableOpacity onPress={handleLocalAuthentication}>
<Box height={50} alignItems={"center"} justifyContent={"center"}>
{showBalance ? (
<Text fontSize={30} variant={"black"}>
{balance}
</Text>
) : (
<Box width={40} height={40}>
<Image
source={require("../../assets/eye_hidden.png")}
style={images.contain}
/>
</Box>
)}
</Box>
</TouchableOpacity>
<Text color={"gray"}>{label}</Text>
</Box>
</Card>
);
};
export default BalanceContainer;
import {} from "react-native";
import BeasyLogoIcon from "./BeasyLogoIcon";
import NotificationIconButton from "./NotificationIconButton";
import Box from "./bases/Box";
const BarWithBeasyAndNotificationsIcon = () => {
return (
<Box px={"l"} flexDirection={"row"} justifyContent={"space-between"} alignItems={"center"}>
<BeasyLogoIcon />
<NotificationIconButton />
</Box>
);
};
export default BarWithBeasyAndNotificationsIcon;
import { asp as g } from "@asp/asp";
import { ImageBackground } from "expo-image";
import type { FC } from "react";
import type { ViewProps } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export const BarnoinPayBackground: FC<ViewProps> = ({ children, style, ...rest }) => {
const insets = useSafeAreaInsets();
return (
<ImageBackground
style={[g.flex_1, { paddingTop: insets.top }, style]}
source={require("@assets/background.png")}
{...rest}
>
{children}
</ImageBackground>
);
};
import { asp as g } from "@asp/asp";
import Box from "@components/bases/Box";
import { images } from "@styles/Commons";
import { Image } from "react-native";
const BeasyLogoIcon = () => {
return (
<Box width={100} height={30}>
<Image source={require("../../assets/logo_beasy.png")} style={images.contain} />
<Image
source={require("../../assets/logo_beasy.png")}
style={[g.flex_1]}
resizeMode="contain"
/>
</Box>
);
};
......
import type { BoxProps, VariantProps } from "@shopify/restyle";
import type { Theme } from "@themes/Theme";
import { ActivityIndicator, TouchableOpacity } from "react-native";
import ButtonBase from "./bases/ButtonBase";
import Text from "./bases/Text";
import { asp as g } from "@asp/asp";
import type { FC } from "react";
import {
ActivityIndicator,
Text,
type TextProps,
TouchableOpacity,
type TouchableOpacityProps,
} from "react-native";
type Props = BoxProps<Theme> &
VariantProps<Theme, "buttonVariants"> &
VariantProps<Theme, "textVariants", "textVariants"> & {
label: string;
onPress: () => void;
const DEFAULT_HEIGHT = 50;
type ContainerProps = TouchableOpacityProps & {
isLoading?: boolean;
};
};
const Button = ({ onPress, label, isLoading, textVariants, variant, ...rest }: Props) => {
export const Container: FC<ContainerProps> = ({ children, style, isLoading, onPress, ...rest }) => {
return (
<TouchableOpacity onPress={onPress}>
<ButtonBase
variant={variant}
justifyContent="center"
alignItems="center"
flexDirection={"row"}
<TouchableOpacity
onPress={onPress}
style={[
g.p_md,
g.rounded_xs,
g.align_center,
g.justify_center,
{ height: DEFAULT_HEIGHT, backgroundColor: "#03875b" },
style,
]}
{...rest}
gap={"m"}
>
{isLoading ? (
<ActivityIndicator color="white" />
) : (
<Text variant={textVariants}>{label}</Text>
)}
</ButtonBase>
{isLoading ? <ActivityIndicator /> : children}
</TouchableOpacity>
);
};
export default Button;
export const Label: FC<TextProps> = ({ children, style, ...rest }) => {
return (
<Text style={[g.font_bold, { color: "white" }, style]} {...rest}>
{children}
</Text>
);
};
import { containers } from "@styles/Commons";
import { useEffect } from "react";
import { Animated, Dimensions } from "react-native";
import Box from "./bases/Box";
type Props = { children: React.ReactNode };
const ContainerBorderTopCurved = ({ children }: Props) => {
const animated = new Animated.Value(0);
// biome-ignore lint/correctness/useExhaustiveDependencies: <we do not want animation to replay everytime this component rerender>
useEffect(() => {
Animated.spring(animated, {
toValue: 1,
useNativeDriver: true,
}).start();
}, []);
return (
<Animated.View
style={[
containers.containerFull,
{
transform: [
{
translateY: animated.interpolate({
inputRange: [0, 1],
outputRange: [Dimensions.get("window").height, 0],
}),
},
],
},
]}
>
<Box
backgroundColor={"white"}
borderTopLeftRadius={30}
borderTopRightRadius={30}
style={containers.containerFull}
>
{children}
</Box>
</Animated.View>
);
};
export default ContainerBorderTopCurved;
import Box from "@components/bases/Box";
import { Ionicons } from "@expo/vector-icons";
import { TouchableOpacity } from "react-native";
type Props = {
onPress: () => void;
};
const GoBackIconButton = ({ onPress }: Props) => {
return (
<TouchableOpacity onPress={onPress}>
<Box width={40} height={40} alignItems={"center"} justifyContent={"center"}>
<Ionicons name="arrow-back" size={24} color="black" />
</Box>
</TouchableOpacity>
);
};
export default GoBackIconButton;
import type { VariantProps } from "@shopify/restyle";
import type { Theme } from "@themes/Theme";
import type { TextInputProps } from "react-native";
import { TextInput } from "react-native";
import Box from "./bases/Box";
import { asp as g } from "@asp/asp";
import type { FC } from "react";
import {
Text,
TextInput,
type TextInputProps,
type TextProps,
View,
type ViewProps,
} from "react-native";
type Props = TextInputProps & VariantProps<Theme, "textVariants", "textVariants">;
const DEFAULT_HEIGHT = 50;
const Input = ({ textVariants, ...rest }: Props) => {
export const Container: FC<ViewProps> = ({ children, style, ...rest }) => {
return (
<Box>
<Box backgroundColor={"lightGray"} height={50} borderRadius={10} my={"m"} p={"s"}>
<TextInput style={{ height: "100%", width: "100%" }} {...rest} />
</Box>
</Box>
<View style={[g.gap_md, style]} {...rest}>
{children}
</View>
);
};
export default Input;
export const Header: FC<TextProps> = ({ children, style, ...rest }) => {
return (
<Text style={[g.font_bold, style]} {...rest}>
{children}
</Text>
);
};
export const FieldContainer: FC<ViewProps> = ({ children, style, ...rest }) => {
return (
<View
style={[
g.p_md,
g.gap_xs,
g.rounded_xs,
g.flex_row,
{ backgroundColor: "#e8e8e9ff", height: DEFAULT_HEIGHT },
style,
]}
{...rest}
>
{children}
</View>
);
};
export const Field: FC<TextInputProps & { ref?: React.Ref<TextInput> }> = ({
style,
ref,
...props
}) => {
return (
<TextInput
ref={ref}
placeholderTextColor={"black"}
style={[
g.flex_1,
g.rounded_xs,
g.p_0,
{
color: "black",
},
style,
]}
{...props}
/>
);
};
import type { VariantProps } from "@shopify/restyle";
import type { Theme } from "@themes/Theme";
import type { TextInputProps } from "react-native";
import { TextInput } from "react-native";
import Box from "./bases/Box";
import Text from "./bases/Text";
type Props = TextInputProps &
VariantProps<Theme, "textVariants", "textVariants"> & {
label: string;
};
const InputWithTopLabel = ({ label, textVariants, ...rest }: Props) => {
return (
<Box>
<Text variant={textVariants}>{label}</Text>
<Box backgroundColor={"lightGray"} height={50} borderRadius={10} my={"m"} p={"s"}>
<TextInput style={{ height: "100%", width: "100%" }} {...rest} />
</Box>
</Box>
);
};
export default InputWithTopLabel;
import { asp as g } from "@asp/asp";
import type { FC } from "react";
import { Modal, type ModalProps, View, type ViewProps, type ViewStyle } from "react-native";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
/**
* A view container that wraps a {@link SafeAreaView} and a {@link SafeAreaProvider}.
* This is useful for modal components that should be centered and have a safe area.
*
* @param {ViewProps} props The props to pass to the {@link View} component.
* @returns {React.ReactElement} The container component.
*/
export const SafeView: FC<ViewProps> = ({ children, style, ...rest }) => {
return (
<SafeAreaProvider>
<SafeAreaView style={[g.flex_1, g.justify_center, g.align_center, style]} {...rest}>
{children}
</SafeAreaView>
</SafeAreaProvider>
);
};
/**
* RnModal is a styled Modal component that provides a basic styling for the
* inner content of a modal. It centers the content horizontally and vertically
* and provides a light green background color with rounded corners.
*
* @param {JSX.Element | JSX.Element[]} children The content to be rendered
* inside the modal.
* @param {ViewStyle} style Optional style overrides for the modal's outer View.
* @param {ModalProps} rest Optional props.
*
* @returns A styled Modal component with the inner content.
*/
export const RnModal: FC<ModalProps & { style?: ViewStyle }> = ({ children, style, ...rest }) => {
return (
<Modal animationType="slide" transparent={true} visible={true} {...rest}>
<View style={[g.flex_1, g.justify_center, g.align_center, style]}>{children}</View>
</Modal>
);
};
/**
* OuterView is a styled View component that provides a basic styling for the
* inner content of a modal. It centers the content horizontally and vertically
* and provides a light green background color with rounded corners.
*
* @param {JSX.Element | JSX.Element[]} children The content to be rendered
* inside the modal.
* @param {ViewProps} style Optional style overrides.
* @param {ViewProps} rest Optional props.
*
* @returns A styled View component with the inner content.
*/
export const OuterView: FC<ViewProps> = ({ children, style, ...rest }) => {
return (
<View
style={[g.rounded_xs, { width: 200, height: 200, backgroundColor: "#cfe3d0ff" }, style]}
{...rest}
>
<View style={[g.flex_1]}>{children}</View>
</View>
);
};
import Box from "@components/bases/Box";
import { Ionicons } from "@expo/vector-icons";
const NotificationIconButton = () => {
return (
<Box width={40} height={40} position={"relative"}>
<Box
backgroundColor={"white"}
opacity={0.5}
p={"s"}
borderRadius={50}
style={{ height: "100%", width: "100%", position: "absolute" }}
/>
<Box
style={{
height: "100%",
width: "100%",
borderRadius: 50,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons
name="notifications-outline"
size={24}
color="white"
style={{ opacity: 1 }}
/>
</Box>
</Box>
);
};
export default NotificationIconButton;
import type { PaymentCode } from "@/utils/requests/types";
import { images } from "@styles/Commons";
import { Image, TouchableOpacity } from "react-native";
import Box from "./bases/Box";
type PaymentOptions = "OM" | "MTN" | "FLOOZ" | "WAVE" | "CB";
type Props = {
onPress: () => void;
paymentMethod: PaymentCode;
};
const PaymentOptionContainer = ({ children }: { children: React.ReactNode }) => {
return (
<Box
height={50}
// p={"s"}
style={{ width: "100%", height: "100%" }}
overflow={"hidden"}
backgroundColor={"yellow"}
borderRadius={30}
>
{children}
</Box>
);
};
const Orange = () => {
return (
<Image source={require("../../assets/operators/orange_money.png")} style={images.cover} />
);
};
const Mtn = () => {
return <Image source={require("../../assets/operators/mtn_money.png")} style={images.cover} />;
};
const Flooz = () => {
return <Image source={require("../../assets/operators/moov_money.png")} style={images.cover} />;
};
const Wave = () => {
return <Image source={require("../../assets/operators/wave_money.png")} style={images.cover} />;
};
const Cb = () => {
return <Image source={require("../../assets/operators/visa_card.png")} style={images.cover} />;
};
const PaymentOption = ({ onPress, paymentMethod }: Props) => {
return (
<TouchableOpacity style={{ width: "100%", height: "100%" }} onPress={onPress}>
{paymentMethod === "OM" && <Orange />}
{paymentMethod === "MTN" && <Mtn />}
{paymentMethod === "FLOOZ" && <Flooz />}
{paymentMethod === "WAVE" && <Wave />}
{paymentMethod === "CB" && <Cb />}
</TouchableOpacity>
);
};
export default PaymentOption;
import type React from "react";
import { Dimensions } from "react-native";
import Box from "./bases/Box";
// const PaymentsOptionsRendererTwoColumn = () => {
// return (
// <View>
// <Box flexDirection={"row"} justifyContent={"space-between"} mb={"s"}>
// <PaymentOptionContainer>
// <PaymentOption
// onPress={() => navigation.navigate("paymentAmountInputScreen")}
// paymentMethod={"OrangeMoney"}
// />
// </PaymentOptionContainer>
// <PaymentOptionContainer>
// <PaymentOption
// onPress={() => navigation.navigate("paymentAmountInputScreen")}
// paymentMethod={"MtnMoney"}
// />
// </PaymentOptionContainer>
// </Box>
// <Box flexDirection={"row"} justifyContent={"space-between"} mb={"s"}>
// <PaymentOptionContainer>
// <PaymentOption
// onPress={() => navigation.navigate("paymentAmountInputScreen")}
// paymentMethod={"MoovMoney"}
// />
// </PaymentOptionContainer>
// <PaymentOptionContainer>
// <PaymentOption
// onPress={() => navigation.navigate("paymentAmountInputScreen")}
// paymentMethod={"WaveMoney"}
// />
// </PaymentOptionContainer>
// </Box>
// <PaymentOptionContainer>
// <PaymentOption
// onPress={() => navigation.navigate("paymentAmountInputScreen")}
// paymentMethod={"VisaCard"}
// />
// </PaymentOptionContainer>
// </View>
// );
// };
const screenWidth = Dimensions.get("window").width;
const paymentOptionCardWidth = screenWidth / 2 - 30;
const paymentOptionCardHeight = paymentOptionCardWidth * 0.65;
const PaymentOptionContainer = ({ children }: { children: React.ReactNode }) => {
return (
<Box width={paymentOptionCardWidth} height={paymentOptionCardHeight}>
{children}
</Box>
);
};
// export default PaymentsOptionsRendererTwoColumn;
import type { PaymentCode } from "@/utils/requests/types";
import moment from "moment";
import "moment/locale/fr";
import PaymentOption from "./PaymentOption";
import Box from "./bases/Box";
import Text from "./bases/Text";
interface Props {
paymentType: PaymentCode;
reference: string;
date: string;
amount: number;
status: "SUCCESS" | "INITIATED" | "FAILED";
}
moment.locale("fr");
const TransactionInformationsItem = ({ paymentType, reference, date, amount, status }: Props) => {
const dateObject = Date.parse(date);
return (
<Box
width={"100%"}
py={"s"}
px={"s"}
flexDirection={"row"}
gap={"s"}
borderRadius={10}
borderColor={"lightGray"}
shadowColor={"black"}
shadowOffset={{ width: 0, height: 0 }}
shadowOpacity={0.5}
backgroundColor={"white"}
justifyContent={"space-between"}
>
<Box flexDirection={"row"} flex={1} gap={"s"}>
<Box width={50} height={50} borderRadius={10} overflow={"hidden"}>
<PaymentOption paymentMethod={paymentType} onPress={() => {}} />
</Box>
<Box height={50} flex={1}>
<Text variant={"black"}>{reference}</Text>
<Text>{moment(dateObject).fromNow()}</Text>
</Box>
</Box>
<Box height={50}>
<AmountColorRenderer status={status} amount={amount} />
</Box>
</Box>
);
};
const AmountColorRenderer = ({ status, amount }: { status: string; amount: number }) => {
if (status === "SUCCESS") {
return <AmountWrapper color="secondary">{amount}</AmountWrapper>;
}
if (status === "INITIATED") {
return <AmountWrapper color="softYellow">{amount}</AmountWrapper>;
}
return <AmountWrapper color="softRed">{amount}</AmountWrapper>;
};
const AmountWrapper = ({
color,
children,
}: { color: "secondary" | "softYellow" | "softRed"; children: React.ReactNode }) => {
return (
<Box backgroundColor={color} px={"m"} py={"xs"} borderRadius={7}>
<Text variant={"white"} fontWeight={"bold"}>
{children} F
</Text>
</Box>
);
};
export default TransactionInformationsItem;
import type React from "react";
import Box from "./bases/Box";
const WrapperTopEdgeCurved = ({ children }: { children?: React.ReactElement }) => {
return (
<Box
flex={1}
backgroundColor={"white"}
borderTopLeftRadius={20}
borderTopRightRadius={20}
// p={"l"}
flexDirection={"column"}
>
{children}
</Box>
);
};
export default WrapperTopEdgeCurved;
import { containers, images } from "@styles/Commons";
import { ImageBackground, View } from "react-native";
type Props = { children: React.ReactNode };
const BackgroundGreenWhiteContentArea = ({ children }: Props) => {
return (
<View style={containers.containerFull}>
<ImageBackground
source={require("../../../assets/background_content_white2.png")}
resizeMode="cover"
style={images.cover}
>
{children}
</ImageBackground>
</View>
);
};
export default BackgroundGreenWhiteContentArea;
import BeasyLogoIcon from "@components/BeasyLogoIcon";
import GoBackIconButton from "@components/GoBackIconButton";
import WrapperTopEdgeCurved from "@components/WrapperTopEdgeCurved";
import Box from "@components/bases/Box";
import { useNavigation } from "@react-navigation/native";
import type { ReactElement } from "react";
import BackgroundDefault from "./BeasyDefaultBackground";
const BackgroundWithBeasyIconAndWhiteContentArea = ({
children,
goBack = false,
}: { children?: ReactElement; goBack?: boolean }) => {
const navigation = useNavigation();
return (
<BackgroundDefault>
<Box
px={"l"}
flexDirection={"row"}
justifyContent={"space-between"}
alignItems={"center"}
mb={"m"}
>
<BeasyLogoIcon />
{goBack && <GoBackIconButton onPress={() => navigation.goBack()} />}
</Box>
<WrapperTopEdgeCurved>{children}</WrapperTopEdgeCurved>
</BackgroundDefault>
);
};
export default BackgroundWithBeasyIconAndWhiteContentArea;
import { images } from "@styles/Commons";
import { ImageBackground } from "react-native";
type Props = { children: React.ReactNode };
const BeasyDefaultBackground = ({ children }: Props) => {
return (
<ImageBackground source={require("../../../assets/background.png")} style={images.cover}>
{children}
</ImageBackground>
);
};
export default BeasyDefaultBackground;
import { type BoxProps, type VariantProps, createRestyleComponent } from "@shopify/restyle";
import type { Theme } from "@themes/Theme";
import { buttonVariants } from "@themes/Variants";
import Box from "./Box";
const ButtonBase = createRestyleComponent<
VariantProps<Theme, "buttonVariants"> &
BoxProps<Theme> & {
children: React.ReactNode;
},
Theme
>([buttonVariants], Box);
export default ButtonBase;
import type { Theme } from "@/themes/Theme";
import Box from "@re-box";
import { type VariantProps, createRestyleComponent, createVariant } from "@shopify/restyle";
const Card = createRestyleComponent<
VariantProps<Theme, "cardVariants"> & React.ComponentProps<typeof Box>,
Theme
>(
[
createVariant({
themeKey: "cardVariants",
defaults: {
margin: {
phone: "s",
tablet: "m",
},
backgroundColor: "red",
},
}),
],
Box,
);
export default Card;
import type { Theme } from "@/themes/Theme";
import { createText } from "@shopify/restyle";
const Text = createText<Theme>();
export default Text;
import Box from "@components/bases/Box";
import type React from "react";
import {} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
const SafeAreaViewTopLeftRightFull = ({ children }: { children: React.ReactNode }) => {
return (
<SafeAreaView edges={["top", "left", "right"]}>
<Box
style={{
height: "100%",
width: "100%",
}}
>
{children}
</Box>
</SafeAreaView>
);
};
export default SafeAreaViewTopLeftRightFull;
import { asp as g } from "@asp/asp";
import Box from "@components/bases/Box";
import { images } from "@styles/Commons";
import { Image } from "react-native";
const CheckIcon = () => {
......@@ -7,7 +7,8 @@ const CheckIcon = () => {
<Box width={50} height={50}>
<Image
source={require("../../../assets/icon_check_success.png")}
style={images.contain}
style={[g.flex_1]}
resizeMode="contain"
/>
</Box>
);
......
import { asp as g } from "@asp/asp";
import Box from "@components/bases/Box";
import { images } from "@styles/Commons";
import { Image } from "react-native";
const ErrorIcon = () => {
......@@ -7,7 +7,8 @@ const ErrorIcon = () => {
<Box width={50} height={50}>
<Image
source={require("../../../assets/icon_close_failure.png")}
style={images.contain}
style={[g.flex_1]}
resizeMode="contain"
/>
</Box>
);
......
import { asp as g } from "@asp/asp";
import Box from "@components/bases/Box";
import { images } from "@styles/Commons";
import { Image } from "react-native";
const InformationIcon = () => {
......@@ -7,7 +7,8 @@ const InformationIcon = () => {
<Box width={50} height={50}>
<Image
source={require("../../../assets/icon_alert_information.png")}
style={images.contain}
style={[g.flex_1]}
resizeMode="contain"
/>
</Box>
);
......
import { useModalsManagerContext } from "@/contexts/ModalsManagerContext";
import Button from "@components/Button";
import ErrorIcon from "@components/icons/ErrorIcon";
import Box from "@re-box";
import Card from "@re-card";
import Text from "@re-text";
interface Props {
message?: string;
// onPress?: () => void;
}
const ErrorModal = ({ message = "Une erreur s'est produite" }: Props) => {
const { closeModal } = useModalsManagerContext();
return (
<Card variant={"modal"}>
<ErrorIcon />
<Text variant={"gray"} fontWeight={"bold"}>
{message}
</Text>
<Box style={{ width: "80%" }}>
<Button
variant={"fullError"}
textVariants={"white"}
label="Fermer"
onPress={closeModal}
/>
</Box>
</Card>
);
};
export default ErrorModal;
import { useModalsManagerContext } from "@/contexts/ModalsManagerContext";
import Button from "@components/Button";
import Box from "@components/bases/Box";
import InformationIcon from "@components/icons/InformationIcon";
import { Text } from "react-native";
interface Props {
message?: string;
onPress?: () => void;
actionLabel?: string;
}
const InformationModal = ({
message = "Une erreur s'est produite",
onPress = undefined,
actionLabel = "Ok",
}: Props) => {
const { closeModal } = useModalsManagerContext();
return (
<Box
width={300}
// height={200}
backgroundColor={"white"}
alignItems={"center"}
justifyContent={"center"}
alignSelf={"center"}
marginTop={"x240"}
position={"absolute"}
zIndex={10}
borderRadius={20}
gap={"m"}
shadowColor={"black"}
shadowOffset={{ width: 0, height: 0 }}
shadowOpacity={0.5}
p={"m"}
>
<InformationIcon />
<Text>{message}</Text>
<Box style={{ width: "80%" }}>
{onPress && (
<Button
variant={"fullInformation"}
textVariants={"white"}
label={actionLabel}
onPress={onPress}
/>
)}
<Button
variant={"noMargin"}
textVariants={"error"}
label="Fermer"
onPress={closeModal}
/>
</Box>
</Box>
);
};
export default InformationModal;
import Box from "@components/bases/Box";
import { ActivityIndicator, Text } from "react-native";
interface Props {
message?: string;
}
const LoadingModal = ({ message = "Veuillez patienter..." }: Props) => {
return (
<Box
width={300}
height={200}
backgroundColor={"white"}
alignItems={"center"}
justifyContent={"center"}
alignSelf={"center"}
marginTop={"x240"}
position={"absolute"}
zIndex={10}
borderRadius={20}
gap={"m"}
shadowColor={"black"}
shadowOffset={{ width: 0, height: 0 }}
shadowOpacity={0.5}
>
<ActivityIndicator size={"large"} />
<Text>{message}</Text>
</Box>
);
};
export default LoadingModal;
import Button from "@components/Button";
import Box from "@components/bases/Box";
import InformationIcon from "@components/icons/InformationIcon";
import { create } from "react-modal-promise";
import { Text } from "react-native";
interface Props {
isOpen: boolean;
onResolve: () => void;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
onReject: any;
}
const MyModal = ({ isOpen, onResolve, onReject }: Props) => {
return (
<Box
width={300}
// height={200}
backgroundColor={"white"}
alignItems={"center"}
justifyContent={"center"}
alignSelf={"center"}
marginTop={"x240"}
position={"absolute"}
zIndex={10}
borderRadius={20}
gap={"m"}
shadowColor={"black"}
shadowOffset={{ width: 0, height: 0 }}
shadowOpacity={0.5}
p={"m"}
visible={isOpen}
>
<InformationIcon />
<Text>Modal</Text>
<Box style={{ width: "80%" }}>
<Button
variant={"fullInformation"}
textVariants={"white"}
label={"Ok"}
onPress={onResolve}
/>
<Button
variant={"noMargin"}
textVariants={"error"}
label="Fermer"
onPress={() => onReject("Fermer")}
/>
</Box>
</Box>
);
};
const myPromiseModal = create(MyModal);
export { myPromiseModal };
export default MyModal;
import BeasyDefaultBackground from "@components/backgrounds/BeasyDefaultBackground";
import type React from "react";
import { SafeAreaView } from "react-native-safe-area-context";
type Props = { children: React.ReactNode };
const WrapperWithDefaultBeasyBackgroundAndSafeAreaFull = ({ children }: Props) => {
return (
<BeasyDefaultBackground>
<SafeAreaView edges={["top", "bottom", "left", "right"]}>{children}</SafeAreaView>
</BeasyDefaultBackground>
);
};
export default WrapperWithDefaultBeasyBackgroundAndSafeAreaFull;
import BeasyDefaultBackground from "@components/backgrounds/BeasyDefaultBackground";
import type React from "react";
import { SafeAreaView } from "react-native-safe-area-context";
type Props = { children: React.ReactNode };
const WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight = ({ children }: Props) => {
return (
<BeasyDefaultBackground>
<SafeAreaView edges={["top", "left", "right"]}>{children}</SafeAreaView>
</BeasyDefaultBackground>
);
};
export default WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight;
import { createContext, useContext, useEffect, useState } from "react";
import { View } from "react-native";
export interface ImodalsManagerContext {
showModal(element: React.ReactElement): void;
closeModal(): void;
}
export const ModalsManagerContext = createContext<ImodalsManagerContext>({
showModal: () => {},
closeModal: () => {},
});
export const ModalsManagerProvider = ({ children }: { children: React.ReactNode }) => {
const [showBackdrop, setShowBackdrop] = useState(false);
const [modalElement, setModalElement] = useState<React.ReactElement | null>(null);
const showModal = (element: React.ReactElement) => {
setModalElement(element);
setShowBackdrop(true);
};
const closeModal = () => {
setModalElement(null);
setShowBackdrop(false);
};
useEffect(() => {
return () => {
setShowBackdrop(false);
setModalElement(null);
};
}, []);
return (
<ModalsManagerContext.Provider
value={{
showModal,
closeModal,
}}
>
{children}
{modalElement && (
<>
<OverlayBackdrop />
{modalElement}
</>
)}
</ModalsManagerContext.Provider>
);
};
export const useModalsManagerContext = () => {
return useContext(ModalsManagerContext);
};
export const OVERLAY_BACKDROP_Z_INDEX = 10;
const OverlayBackdrop = () => {
return (
<View
style={{
flex: 1,
width: "100%",
height: "100%",
// justifyContent: "center",
// alignItems: "center",
zIndex: OVERLAY_BACKDROP_Z_INDEX,
backgroundColor: "black",
opacity: 0.5,
position: "absolute",
}}
/>
);
};
export interface IauthenticationData {
access: string;
refresh: string;
}
import cacheAssetsAsync from "@/utils/assetsCache";
import authenticateUser, { parseAuthicationErrors } from "@/utils/requests/authenticateUser";
import type { IuserInformations } from "@/utils/requests/types";
import getUserInformations, {
parseUserInformationsErrors,
} from "@/utils/requests/userInformations";
import ErrorModal from "@components/modals/ErrorModal";
import { LOG } from "@logger";
import AsyncStorage from "@react-native-async-storage/async-storage";
// import { type NavigationProp, useNavigation } from "@react-navigation/native";
import { useMutation } from "@tanstack/react-query";
import * as SplashScreen from "expo-splash-screen";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { useModalsManagerContext } from "./ModalsManagerContext";
import type { IauthenticationData } from "./Types";
const log = LOG.extend("UserAuthenticationContext");
SplashScreen.preventAutoHideAsync();
export interface UserAuthenticationContextProps {
isAuthenticated: boolean;
setIsAuthenticated: React.Dispatch<React.SetStateAction<boolean>>;
setAuthenticationData: React.Dispatch<React.SetStateAction<IauthenticationData>>;
userInformations: IuserInformations;
setUserInformations: React.Dispatch<React.SetStateAction<IuserInformations>>;
login: (email: string, password: string) => void;
isAuthenticating: boolean;
logout: () => void;
}
export const UserAuthenticationContext = createContext<UserAuthenticationContextProps>({
isAuthenticated: false,
setIsAuthenticated: () => {},
setAuthenticationData: () => {},
userInformations: {
username: "",
email: "",
// biome-ignore lint/style/useNamingConvention: <Api response>
first_name: "",
// biome-ignore lint/style/useNamingConvention: <Api response>
last_name: "",
marchand: {
// biome-ignore lint/style/useNamingConvention: <Api response>
marchand_id: "",
nom: "",
code: "",
adresse: "",
// biome-ignore lint/style/useNamingConvention: <Api response>
url_succes: "",
// biome-ignore lint/style/useNamingConvention: <Api response>
url_echec: "",
entreprise: 0,
user: 0,
},
},
setUserInformations: () => {},
login: () => {},
isAuthenticating: false,
logout: () => {},
});
export const UserAuthenticationContextProvider = ({ children }: { children: React.ReactNode }) => {
// States
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [authenticationData, setAuthenticationData] = useState<IauthenticationData>({
access: "",
refresh: "",
});
const [error, setError] = useState("");
const [userInformations, setUserInformations] = useState<IuserInformations>({
username: "JohnDoe",
email: "JohnDoe@example.com",
// biome-ignore lint/style/useNamingConvention: <Api response>
first_name: "John",
// biome-ignore lint/style/useNamingConvention: <Api response>
last_name: "Doe",
marchand: {
// biome-ignore lint/style/useNamingConvention: <Api response>
marchand_id: "id123",
nom: "Beasy",
code: "BEASY-EXAMPLE-1",
adresse: "Plateau 2, 1023, Immeuble Chardy",
// biome-ignore lint/style/useNamingConvention: <Api response>
url_succes: "https://example.com/success",
// biome-ignore lint/style/useNamingConvention: <Api response>
url_echec: "https://example.com/echec",
entreprise: 0,
user: 0,
},
});
// Hoooks
const { showModal } = useModalsManagerContext();
// const navigation = useNavigation<NavigationProp<ImainStackNavigator>>();
// Mutations
const authenticationMutation = useMutation({
mutationFn: authenticateUser,
onMutate: () => {
setIsAuthenticating(true);
setError("");
},
onSuccess: (data) => {
setAuthenticationData(data);
log.info("Receive data from authenticateUser, running getUserInformations...");
userInformationsMutation.mutate(data.access);
},
onError: (error: unknown) => {
const errorString = parseAuthicationErrors(error);
showModal(<ErrorModal message={errorString} />);
},
onSettled: () => {
setIsAuthenticating(false);
},
});
const userInformationsMutation = useMutation({
mutationFn: (userAccessToken: string) => getUserInformations(userAccessToken),
onMutate: () => {
setIsAuthenticating(true);
setError("");
},
onSettled: () => {
setIsAuthenticating(false);
},
onSuccess: (userInformations) => {
log.info("getUserInformations request was a success, navigating to homepage");
setUserInformations(userInformations);
setIsAuthenticated(true);
storeAuthenticationData(authenticationData);
storeUserInformations(userInformations);
// navigation.navigate("appBottomTabsNavigator");
},
onError: (error) => {
log.error("userInformationsMutation", error);
const errorString = parseUserInformationsErrors(error);
showModal(<ErrorModal message={errorString} />);
clearStorages();
},
});
// Methods
const login = useCallback(
(email: string, password: string) => {
authenticationMutation.mutate({
username: email,
password: password,
});
},
[authenticationMutation],
);
const logout = useCallback(() => {
(async () => {
setIsAuthenticated(false);
setAuthenticationData({
access: "",
refresh: "",
});
setUserInformations({
username: "",
email: "",
// biome-ignore lint/style/useNamingConvention: <explanation>
first_name: "",
// biome-ignore lint/style/useNamingConvention: <explanation>
last_name: "",
marchand: {
// biome-ignore lint/style/useNamingConvention: <explanation>
marchand_id: "",
nom: "",
code: "",
adresse: "",
// biome-ignore lint/style/useNamingConvention: <explanation>
url_succes: "",
// biome-ignore lint/style/useNamingConvention: <explanation>
url_echec: "",
entreprise: 0,
user: 0,
},
});
await clearStorages();
})();
}, []);
// Storages
const storeAuthenticationData = async (authenticationData: IauthenticationData) => {
try {
await AsyncStorage.setItem("authenticationData", JSON.stringify(authenticationData));
} catch (error) {
log.error("storeAuthenticationData |", JSON.stringify(error, null, 2));
// saving error
}
};
const storeUserInformations = async (userInformations: IuserInformations) => {
try {
await AsyncStorage.setItem("userInformations", JSON.stringify(userInformations));
} catch (error) {
log.error("storeUserInformations |", JSON.stringify(error, null, 2));
// saving error
}
};
const clearStorages = async () => {
try {
await AsyncStorage.clear();
} catch (error) {
log.error("clearStorages |", JSON.stringify(error, null, 2));
// saving error
}
};
const loadAuthenticationData = async () => {
log.debug("loadAuthenticationData | Loading authentication data");
const jsonRepresentation = await AsyncStorage.getItem("authenticationData");
return jsonRepresentation ? JSON.parse(jsonRepresentation) : null;
};
const loadUserInformations = async () => {
log.debug("loadUserInformations | Loading user informations");
const jsonRepresentation = await AsyncStorage.getItem("userInformations");
return jsonRepresentation ? JSON.parse(jsonRepresentation) : null;
};
// biome-ignore lint/correctness/useExhaustiveDependencies: <This should only be executed once. At startup.>
useEffect(() => {
log.debug("UserAuthenticationContext | App Startup | loading saved user data.");
(async () => {
try {
// await loadAssetsAsync();
await cacheAssetsAsync({
images: [
"../assets/background_default.png",
"../assets/beasy_default.png",
"../assets/beasy_background.png",
"../assets/background_content_white2.png",
"../../assets/background.png",
],
});
const authenticationData = await loadAuthenticationData();
const userInformations = await loadUserInformations();
if (authenticationData && userInformations) {
setAuthenticationData(authenticationData);
setUserInformations(userInformations);
setIsAuthenticated(true);
}
} catch (error) {
log.error(
"UserAuthenticationContext | App Startup | error during retrieval of stored data |",
JSON.stringify(error, null, 2),
);
} finally {
setTimeout(async () => await SplashScreen.hideAsync(), 500);
}
})();
}, []);
return (
<UserAuthenticationContext.Provider
value={{
isAuthenticated: isAuthenticated,
isAuthenticating: isAuthenticating,
setIsAuthenticated,
setAuthenticationData,
userInformations,
setUserInformations,
login,
logout,
}}
>
{children}
</UserAuthenticationContext.Provider>
);
};
export const useUserAuthenticationContext = () => {
return useContext(UserAuthenticationContext);
};
import { axiosInstance } from "@/axios";
import type { LoginPayload, Token, UserData } from "./types";
export const login = (data: LoginPayload) => {
return axiosInstance.post<Token>("/login/token/", data);
};
// On this one you just provide the bear token and its should return the userData.
export const getUserData = () => {
return axiosInstance.get<UserData>("/user-info/");
};
import { asp as g } from "@asp/asp";
import * as Button from "@components/Button";
import * as Input from "@components/Input";
import * as Modal from "@components/Modal";
import { MaterialIcons } from "@expo/vector-icons";
import Feather from "@expo/vector-icons/Feather";
import { useMutation } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { useRef, useState } from "react";
import { Text, View } from "react-native";
import { useDispatch } from "react-redux";
import { getUserData, login } from "../api";
import { setToken, setUserData } from "../slice";
export const LoginForm = () => {
const dispatch = useDispatch();
const usernameRef = useRef<string>("");
const passwordRef = useRef<string>("");
const loginMutation = useMutation({
mutationFn: () => login({ username: usernameRef.current, password: passwordRef.current }),
onSuccess: (data) => {
dispatch(setToken(data.data));
useDataMutation.mutate();
},
onError: (error: AxiosError) => {
setError(JSON.stringify(error.response?.data) || error.message);
},
});
const useDataMutation = useMutation({
mutationFn: getUserData,
onSuccess: (data) => {
dispatch(setUserData(data.data));
},
});
const [error, setError] = useState<string>("");
return (
<View
style={[
g.p_lg,
g.gap_lg,
{
backgroundColor: "white",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
]}
>
<Text style={[g.text_5xl, g.font_bold]}>Connexion</Text>
<Text style={{ color: "gray" }}>Bienvenue, vous nous avez manqué !</Text>
<Input.Container>
<Input.Header>Addresse e-mail</Input.Header>
<Input.FieldContainer>
<Feather name="smartphone" size={24} color="black" />
<Input.Field onChangeText={(v) => (usernameRef.current = v)} />
</Input.FieldContainer>
</Input.Container>
<Input.Container>
<Input.Header>Mot de passe</Input.Header>
<Input.FieldContainer>
<MaterialIcons name="lock-outline" size={24} color="black" />
<Input.Field secureTextEntry onChangeText={(v) => (passwordRef.current = v)} />
</Input.FieldContainer>
</Input.Container>
<Button.Container
style={[g.mt_5xl]}
isLoading={loginMutation.isPending}
onPress={() => loginMutation.mutate()}
>
<Button.Label style={{ color: "white" }}>Se connecter</Button.Label>
</Button.Container>
<Button.Container style={[{ backgroundColor: "#e8e8e9aa" }]}>
<Button.Label style={{ color: "black" }}>Créer un compte</Button.Label>
</Button.Container>
{/* MODAL */}
<Modal.SafeView>
<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.SafeView>
</View>
);
};
import type { PayloadAction } from "@reduxjs/toolkit";
import { createSlice } from "@reduxjs/toolkit";
import type { Token, UserData, UserDataAndToken } from "./types";
export interface AuthState {
isAuthenticated: boolean;
user: UserData;
token: Token;
}
const initialState: AuthState = {
isAuthenticated: false,
user: {} as UserData,
token: {} as Token,
};
export const authSlice = createSlice({
name: "auth",
initialState: initialState,
reducers: {
setToken: (state, action: PayloadAction<Token>) => {
state.token = action.payload;
},
setUserData: (state, action: PayloadAction<UserData>) => {
state.user = action.payload;
state.isAuthenticated = true;
},
login: (state, action: PayloadAction<UserDataAndToken>) => {
state.isAuthenticated = true;
state.user = action.payload.user;
state.token = action.payload.tokens;
},
logout: (state) => {
state.isAuthenticated = false;
state.token = {} as Token;
state.user = {} as UserData;
},
},
});
export const { login, logout, setToken, setUserData } = authSlice.actions;
export const authReducer = authSlice.reducer;
export interface Merchant {
marchand_id: string;
nom: string;
code: string;
adresse: string;
url_succes: string;
url_echec: string;
entreprise: number;
user: number;
}
export type UserData = {
id: number;
username: string;
email: string;
first_name: string;
last_name: string;
role: string;
marchand: Merchant;
};
export type Token = {
access: string;
refresh: string;
};
export type LoginPayload = {
username: string;
password: string;
};
export type UserDataAndToken = {
user: UserData;
tokens: Token;
};
import { axiosInstance } from "@/axios";
import type {
DjangoPaginated,
OmTransaction,
OmInitializationPayload as OmTransactionInitializationPayload,
OmInitializationResponse as OmTransactionInitializationResponse,
PaymentType,
Transaction,
WaveInitializationPayload,
WaveTransactionInitilizationResponse,
} 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);
}
};
export const waveInitializeTransaction = (payload: WaveInitializationPayload) => {
return axiosInstance.post<WaveTransactionInitilizationResponse>("/transactions/", payload);
};
export const waveGetTransactionStatus = (id: string) => {
return axiosInstance.get<WaveTransactionInitilizationResponse>(`/wave-session/${id}/`);
};
export const getTransactions = () => {
return axiosInstance.get<DjangoPaginated<Transaction[]>>("/transactions/");
};
/** biome-ignore-all lint/style/useNamingConvention: <The codes are expected as is..> */
import { asp as g } from "@asp/asp";
import { Image } from "expo-image";
import type { FC } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
import type { PaymentTypeCode } from "../types";
const Images = {
OM: require("@assets/operators/orange_money.png"),
MTN: require("@assets/operators/mtn_money.png"),
WAVE: require("@assets/operators/wave_money.png"),
FLOOZ: require("@assets/operators/moov_money.png"),
CB: require("@assets/operators/visa_card.png"),
};
type PaymentTypeProps = Omit<TouchableOpacityProps, "children"> & { type: PaymentTypeCode };
const PaymentType: FC<PaymentTypeProps> = ({ style, type, ...rest }) => {
return (
<TouchableOpacity
style={[g.rounded_md, g.overflow_hidden, { height: 70, width: 100 }, style]}
{...rest}
>
<Image source={Images[type]} style={[g.flex_1]} cachePolicy={"memory-disk"} />
</TouchableOpacity>
);
};
export default PaymentType;
import moment from "moment";
import "moment/locale/fr";
import { asp as g } from "@asp/asp";
import type { FC } from "react";
import { Text, View } from "react-native";
import type { Transaction } from "../types";
import PaymentType from "./PaymentType";
moment.locale("fr");
export const TransactionItem: FC<{ transaction: Transaction }> = ({ transaction }) => {
const { type_paiement_label, reference, date, montant, status } = transaction;
const dateObject = Date.parse(date);
const color = status === "SUCCESS" ? "green" : status === "INITIATED" ? "orange" : "red";
return (
<View
style={[
g.w_full,
g.p_sm,
g.flex_row,
g.gap_sm,
g.rounded_sm,
g.shadow_elevated,
g.justify_between,
{ backgroundColor: "white" },
]}
>
<View style={[g.flex_row, g.flex_1, g.gap_sm]}>
<View style={[g.rounded_md, g.overflow_hidden, { height: 50, width: 50 }]}>
<PaymentType
style={{ height: "100%", aspectRatio: 1 }}
type={type_paiement_label}
/>
</View>
<View style={[g.flex_1, { height: 50 }]}>
<Text>{reference}</Text>
<Text>{moment(dateObject).fromNow()}</Text>
</View>
</View>
<View style={{ height: 50 }}>
<Text style={{ color: color }}>{montant}</Text>
</View>
</View>
);
};
export type PaymentTypeCode = "OM" | "FLOOZ" | "MTN" | "WAVE" | "CB";
export interface PaymentType {
reference: number;
nom: string;
description: string;
code: PaymentTypeCode;
etat: boolean;
type_operateur: string;
}
export interface DjangoPaginated<T> {
count: number;
next: string | null;
previous: string | null;
results: T;
}
export interface Merchant {
marchand_id: string;
nom: string;
code: string;
adresse: string;
url_succes: string;
url_echec: string;
entreprise: number;
user: number;
}
export interface IuserInformations {
username: string;
email: string;
first_name: string;
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;
};
}
// WAVE
export type WaveInitializationPayload = {
type_paiement: number;
marchand: string;
service: string;
montant: number;
};
export interface WaveTransactionInitilizationResponse {
id: string;
amount: string;
checkout_status: string;
client_reference: unknown;
currenfy: string;
error_url: string;
last_payment_errror: unknown;
business_name: string;
payment_status: string;
succes_url: string;
wave_launch_url: string;
when_completed: unknown;
when_created: string;
when_expires: string;
}
export type Transaction = {
type_paiement: number;
type_paiement_label: PaymentTypeCode;
marchand: string;
marchand_name: string;
service: string;
montant: number;
date: string;
commentaire: string;
etat: boolean;
status: "SUCCESS" | "INITIATED" | "FAILED";
reference: string;
transaction_id: number;
marchand_code: string;
};
import { useModalsManagerContext } from "@/contexts/ModalsManagerContext";
import type { IpaymentStackNavigator } from "@/navigations/Types";
import {
type IorangePaymentStarter,
getTransactionStatus,
getTransactionsData,
} from "@/utils/requests/orangePayment";
import ErrorModal from "@components/modals/ErrorModal";
import LoadingModal from "@components/modals/LoadingModal";
import { LOG } from "@logger";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios";
import * as WebBrowser from "expo-web-browser";
import { useRef, useState } from "react";
import { AppState, Platform } from "react-native";
const log = LOG.extend("useOrangeMoney");
const paymentObjectDefault: IorangePaymentStarter = {
// biome-ignore lint/style/useNamingConvention: <api expect type_paiement>
type_paiement: 1,
marchand: "1",
service: "2",
montant: 0,
numero: "0707070707",
commentaire: "Un commentaire",
};
const useOrangeMoney = (
navigation?: NativeStackNavigationProp<
IpaymentStackNavigator,
"paymentAmountInputScreen",
"IpaymentStackNavigator"
>,
) => {
const queryClient = useQueryClient();
const [isBrowserOpen, setIsBrowserOpen] = useState(false);
const { showModal, closeModal } = useModalsManagerContext();
const appState = useRef(AppState.currentState);
const [appStateVisible, setAppStateVisible] = useState(appState.current);
const handlePaymentUsingBrowser = async (url: string) => {
setIsBrowserOpen(true);
const result = await WebBrowser.openBrowserAsync(url);
// setResult(result);
log.debug("handlePaymentUsingBrowser | Result ::", result);
setIsBrowserOpen(false);
};
const orangeTransactionInitializerMutation = useMutation({
mutationFn: (amount: number) =>
getTransactionsData({
...paymentObjectDefault,
montant: amount,
}),
onSuccess: (data) => {
// return data.payment_url
log.debug("orangeTransactionInitializerMutation request success, opening browser...");
queryClient.invalidateQueries({ queryKey: ["transactionsHistory"] });
// await handlePaymentUsingBrowser(data.payment_url);
// await transactionsStatusMutation.mutate(data.order_id);
// setResult(result);
},
onError: (err) => {
log.error("orangeTransactionInitializerMutation |", err);
},
});
const maxRetry = 3;
const retryDelay = 5000;
const transactionsStatusMutation = useMutation({
mutationFn: (orderId: string) => getTransactionStatus(orderId),
onSuccess: (data) => {
log.debug("transactionsStatusMutation request success");
queryClient.invalidateQueries({ queryKey: ["transactionsHistory"] });
return data.status;
},
onError: (err) => {
log.error("transactionsStatusMutation |", err);
queryClient.invalidateQueries({ queryKey: ["transactionsHistory"] });
},
// retry: (failureCount, error) => {
// log.warn("transactionsStatusMutation | retrying", failureCount, error);
// return failureCount < maxRetry;
// },
// retryDelay(_failureCount, _error) {
// return retryDelay;
// },
});
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <Necessarry evil>
const checkStatus = async (orderId: string, retry = 0): Promise<string | undefined> => {
// Check the transactions 'retry' time.
let numberOfTries = 0;
while (numberOfTries <= retry) {
log.verbose("useOrangePayment | checkStatus | Try No.", numberOfTries + 1);
try {
await transactionsStatusMutation.mutateAsync(orderId);
return;
} catch (error) {
// errors are throwned from getTransactions status when the staus is either initiated or failed.
if (error instanceof AxiosError && error.response?.status === 404) {
log.error("useOrangeMoney | checkStatus | Transaction not found");
return error.response.data.error || `Transaction ${orderId} not found !!`;
}
if (error instanceof AxiosError && error.name === "ORANGE_PAYMENT_FAILED") {
return "La transaction à échoué.";
}
if (error instanceof AxiosError && error.name === "ORANGE_UNKNOWN_PAYMENT_ERROR") {
return "Une erreur est survenue.";
}
}
numberOfTries += 1;
}
return "La transaction est toujours en cours.";
};
const openBrowserThenCheckStatus = async (paymentUrl: string, orderId: string) => {
try {
await handlePaymentUsingBrowser(paymentUrl);
if (Platform.OS === "android") {
log.debug(
"useOrangeMoney | openBrowserThenCheckStatus | Android device. Setup listener for browser close event.",
);
const sub = AppState.addEventListener("change", async (nextAppState) => {
log.debug(
"useOrangeMoney | openBrowserThenCheckStatus | Android device. Browser state :",
nextAppState,
);
if (nextAppState === "active") {
log.debug(
"useOrangeMoney | openBrowserThenCheckStatus | Android device. Browser is closed. Removing listener. Checking for transaction State.",
);
sub.remove();
log.info("openBrowserThenCheckStatus | Verifying transaction status...");
showModal(
<LoadingModal message="Vérification du statut de la transaction..." />,
);
// await transactionsStatusMutation.mutateAsync(orderId);
const message = await checkStatus(orderId);
if (message) {
showModal(<ErrorModal message={message} />);
} else {
navigation?.getParent()?.navigate("paymentResultScreen");
}
}
});
} else {
log.info("openBrowserThenCheckStatus | Verifying transaction status...");
showModal(<LoadingModal message="Vérification du statut de la transaction..." />);
// await transactionsStatusMutation.mutateAsync(orderId);
const message = await checkStatus(orderId);
if (message) {
showModal(<ErrorModal message={message} />);
} else {
navigation?.getParent()?.navigate("paymentResultScreen");
}
}
// closeModal();
} catch (error: unknown) {
log.verbose("Error catching");
if (error instanceof AxiosError) {
log.error("openBrowserThenCheckStatus Catch Block|", error);
showModal(
<ErrorModal
message={error.response?.data.error || "Une erreur Axios est survenue."}
/>,
);
} else {
showModal(<ErrorModal message="Une erreur est survenue." />);
}
// log.error("openBrowserThenCheckStatus Catch Block|", error);
// if (error instanceof Error) {
// log.debug("1");
// if (error.name === "ORANGE_PAYMENT_IN_PROGRESS") {
// log.debug("2");
// log.warn("openBrowserThenCheckStatus | ORANGE_PAYMENT_IN_PROGRESS");
// await showModal(
// <InformationModal
// message="Le payment est toujours en cours."
// actionLabel="Rééssayer"
// onPress={() => openBrowserThenCheckStatus(paymentUrl, orderId)}
// />,
// );
// log.debug("3");
// } else if (error.name === "ORANGE_PAYMENT_FAILED") {
// showModal(<ErrorModal message="Le paiment à échoué." />);
// log.error("openBrowserThenCheckStatus | ORANGE_PAYMENT_FAILED");
// }
// log.debug("4 --", error.name);
// } else {
// log.error("openBrowserThenCheckStatus Else Block|", error);
// closeModal();
// throw error;
// }
}
};
const orangePaymentTransactionHandler = async (amount: number) => {
try {
showModal(<LoadingModal message="Initialization de la transaction." />);
const { payment_url, order_id } =
await orangeTransactionInitializerMutation.mutateAsync(amount);
log.info("orangePaymentTransactionHandler |", payment_url, order_id);
log.info("Opening browser for payment...");
await openBrowserThenCheckStatus(payment_url, order_id);
// biome-ignore lint/suspicious/noExplicitAny: <TODO: Change this later>
} catch (error: any) {
log.error("makePayment |", error);
showModal(
<ErrorModal message={error.response?.data?.error || "Une erreur est survenue."} />,
);
throw error;
} finally {
//closeModal(); // just to be ultra sure that the modal is closed
}
};
return {
orangeTransactionInitializerMutation: orangeTransactionInitializerMutation,
handlePaymentUsingBrowser,
isBrowserOpen,
isWaitingForOmPaymentUrl: orangeTransactionInitializerMutation.isPending,
isCheckingForTransactionStatus: transactionsStatusMutation.isPending,
transactionsStatusMutation,
orangePaymentTransactionHandler,
};
};
export default useOrangeMoney;
import { type Transaction, getTransactionsHistory } from "@/utils/requests/transactions";
import type { PaymentCode } from "@/utils/requests/types";
import { LOG } from "@logger";
import { useQuery } from "@tanstack/react-query";
import { useCallback, useMemo, useState } from "react";
const log = LOG.extend("useTransactionsHistory");
// biome-ignore lint/style/useNamingConvention: <explanation>
export type OperatorsFilter = { [key in PaymentCode]: boolean };
const useTransactionsHistory = () => {
log.verbose("useTransactionsHistory");
const [referenceFilter, setReferenceFilter] = useState<string>("");
// biome-ignore lint/style/useNamingConvention: <Types values are in uppercase>
const [operatorsFilter, setOperatorsFilter] = useState<OperatorsFilter>({
// biome-ignore lint/style/useNamingConvention: <Types values are in uppercase>
OM: true,
// biome-ignore lint/style/useNamingConvention: <Types values are in uppercase>
MTN: true,
// biome-ignore lint/style/useNamingConvention: <Types values are in uppercase>
FLOOZ: true,
// biome-ignore lint/style/useNamingConvention: <Types values are in uppercase>
WAVE: true,
// biome-ignore lint/style/useNamingConvention: <Types values are in uppercase>
CB: true,
});
const { data, isLoading, error, refetch } = useQuery({
queryKey: ["transactionsHistory"],
queryFn: getTransactionsHistory,
});
const filterByReference = useCallback(
(reference: string) => {
if (!data?.length) return [];
return data.filter(
(transaction) => transaction.reference.includes(reference) && transaction.reference,
);
},
[data],
);
const filterDataByReference = (data: Transaction[], reference: string) => {
if (!data?.length) return [];
return data.filter(
(transaction) => transaction.reference.includes(reference) && transaction.reference,
);
};
const filterByOperators = (data: Transaction[]) => {
if (!data?.length) return [];
// create a set
const set = new Set<PaymentCode>();
for (const key of Object.keys(operatorsFilter)) {
if (operatorsFilter[key as keyof OperatorsFilter]) {
set.add(key as PaymentCode);
}
}
return data.filter((transaction) => {
// return true if the set is empty, as there is no need to check
if (set.size === 0) return true;
// return true if the set contains the value
return set.has(transaction.type_paiement_label);
});
};
const transactionsHistory: Transaction[] = useMemo(() => {
if (!data) return [];
const filteredByOperators = filterByOperators(data);
const filteredByReference = filterDataByReference(filteredByOperators, referenceFilter);
return filteredByReference;
// return filterByReference(referenceFilter);
}, [data, referenceFilter, filterByOperators, filterDataByReference]);
return {
transactionsHistory,
isLoading,
error,
refetch,
setReferenceFilter,
operatorsFilter,
setOperatorsFilter,
};
};
export default useTransactionsHistory;
import { useModalsManagerContext } from "@/contexts/ModalsManagerContext";
import type { IpaymentStackNavigator } from "@/navigations/Types";
import {
type IwavePaymentStarter,
getTransactionStatus,
initTransaction,
} from "@/utils/requests/wavePayment";
import ErrorModal from "@components/modals/ErrorModal";
import LoadingModal from "@components/modals/LoadingModal";
import { LOG } from "@logger";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import * as WebBrowser from "expo-web-browser";
import { useState } from "react";
const log = LOG.extend("useWave");
const paymentObjectDefault: IwavePaymentStarter = {
// biome-ignore lint/style/useNamingConvention: <api>
type_paiement: 2,
marchand: "1",
service: "2",
montant: 0,
};
const useWave = (
navigation?: NativeStackNavigationProp<
IpaymentStackNavigator,
"paymentAmountInputScreen",
"IpaymentStackNavigator"
>,
) => {
const { showModal, closeModal } = useModalsManagerContext();
const [isBrowserOpen, setIsBrowserOpen] = useState(false);
const queryClient = useQueryClient();
// Mutations
const waveTransactionInitializerMutation = useMutation({
mutationFn: (amount: number) =>
initTransaction({
...paymentObjectDefault,
montant: amount,
}),
onSuccess: (data) => {},
onError: (err) => {
log.error("waveTransactionInitializerMutation |", err);
},
});
const waveTransactionStatusMutation = useMutation({
mutationFn: (orderId: string) => getTransactionStatus(orderId),
onSuccess: (data) => {
log.debug("waveTransactionStatusMutation request success");
},
onError: (err) => {
log.error("waveTransactionStatusMutation |", err);
},
});
// Browser stuff
const handlePaymentUsingBrowser = async (url: string) => {
log.debug("handlePaymentUsingBrowser | Opening the browser at url :: ", url);
setIsBrowserOpen(true);
const result = await WebBrowser.openBrowserAsync(url);
// setResult(result);
log.debug("handlePaymentUsingBrowser | Result ::", result);
setIsBrowserOpen(false);
};
const openBrowserThenCheckStatus = async (paymentUrl: string, orderId: string) => {
try {
await handlePaymentUsingBrowser(paymentUrl);
log.info("openBrowserThenCheckStatus | Verifying transaction status...");
showModal(<LoadingModal message="Vérification du statut de la transaction..." />);
await waveTransactionStatusMutation.mutateAsync(orderId);
closeModal();
// navigation?.getParent()?.navigate("paymentResultScreen");
} catch (error) {
log.error("openBrowserThenCheckStatus |", error);
// if (error instanceof Error) {
// if (error.name === "ORANGE_PAYMENT_IN_PROGRESS") {
// log.warn("openBrowserThenCheckStatus | ORANGE_PAYMENT_IN_PROGRESS");
// await showModal(
// <InformationModal
// message="Le payment est toujours en cours."
// actionLabel="Rééssayer"
// onPress={() => openBrowserThenCheckStatus(paymentUrl, orderId)}
// />,
// );
// } else if (error.name === "ORANGE_PAYMENT_FAILED") {
// showModal(<ErrorModal message="Le paiment à échoué." />);
// log.error("openBrowserThenCheckStatus | ORANGE_PAYMENT_FAILED");
// }
// } else {
// log.error("openBrowserThenCheckStatus |", error);
// closeModal();
// throw error;
// }
}
};
// Handlers
const waveTransactionHandler = async (amount: number) => {
try {
showModal(<LoadingModal message="Initialization de la transaction." />);
const response = await waveTransactionInitializerMutation.mutateAsync(amount);
log.info("waveTransactionHandler payment url received :: ", response.wave_launch_url);
// log.info("Opening browser for payment...");
log.info("Navigating to the qr code screen...");
closeModal();
navigation?.getParent()?.navigate("waveQrCodePaymentScreen", { data: response });
// await openBrowserThenCheckStatus(response.wave_launch_url, response.id);
} catch (error) {
log.error("waveTransactionHandler |", error);
showModal(<ErrorModal message="Une erreur s'est produite." />);
throw error;
}
};
const handlePaymentVerification = async (id: string) => {
log.info("handlePaymentVerification |", id);
try {
showModal(<LoadingModal message="Vérification du statut de la transaction..." />);
const response = await waveTransactionStatusMutation.mutateAsync(id);
closeModal();
} catch (error) {
log.error("handlePaymentVerification |", error);
// closeModal();
// showModal(<ErrorModal message="Le paiment a été echoué." />);
} finally {
// TODO : remove this finally block once a proper implementation workflow is set. currently we close the modal after logging whatever response we get from the request
closeModal();
}
};
return {
waveTransactionInitializerMutation,
waveTransactionHandler,
handlePaymentVerification,
};
};
export default useWave;
......@@ -3,10 +3,9 @@ import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import TransactionHistoryScreen from "@screens/TransactionHistoryScreen";
import UserProfileScreen from "@screens/UserProfileScreen";
import { useTheme } from "@shopify/restyle";
// import palette
import type { Theme } from "@themes/Theme";
import { View } from "react-native";
import Text from "../components/bases/Text";
import { Text, View } from "react-native";
import PaymentStackNavigator from "./PaymentStackNavigation";
const Tab = createBottomTabNavigator();
......@@ -63,22 +62,6 @@ export const AppBottomTabsNavigator = () => {
);
};
const HomeScreen = () => {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Home!</Text>
</View>
);
};
const Transactions = () => {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Transactions!</Text>
</View>
);
};
const SettingsScreen = () => {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
......@@ -86,11 +69,3 @@ const SettingsScreen = () => {
</View>
);
};
const ProfileScreen = () => {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Profile!</Text>
</View>
);
};
import { useUserAuthenticationContext } from "@/contexts/UserAuthenticationContext";
import { LOG } from "@logger";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomeUserNotLoggedIn from "@screens/HomeUserNotLoggedIn";
......@@ -6,8 +5,10 @@ import PaymentResultScreen from "@screens/PaymentResultScreen";
import UserLoginScreen from "@screens/UserLoginScreen";
import WaveQrCodePaymentScreen from "@screens/WaveQrCodePaymentScreen";
import { memo } from "react";
import { useSelector } from "react-redux";
import type { RootState } from "@/redux";
import { AppBottomTabsNavigator } from "./AppBottomTabsNavigator";
import type { ImainStackNavigator } from "./Types";
import type { ImainStackNavigator } from "./types";
const Stack = createNativeStackNavigator<ImainStackNavigator>();
......@@ -50,6 +51,6 @@ const AppMainStackNavigator: React.FC<IappMainStackNavigatorProps> = ({ isAuthen
export default memo(AppMainStackNavigator);
export const AppMainStackNavigatorAuthWrapper = () => {
const { isAuthenticated } = useUserAuthenticationContext();
const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated);
return <AppMainStackNavigator isAuthenticated={isAuthenticated} />;
};
......@@ -2,7 +2,7 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomePageWithPaymentOptions from "@screens/HomePageWithPaymentOptions";
import NumberAndOtpForPaymentScreen from "@screens/NumberAndOtpForPaymentScreen";
import PaymentAmountInputScreen from "@screens/PaymentAmountInputScreen";
import type { IpaymentStackNavigator } from "./Types";
import type { IpaymentStackNavigator } from "./types";
const Stack = createNativeStackNavigator<IpaymentStackNavigator>();
......
import type { PaymentCode } from "@/utils/requests/types";
import type { IwaveStarterRespone } from "@/utils/requests/wavePayment";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import type { PaymentType, WaveTransactionInitilizationResponse } from "@/features/pay/types";
export type IpaymentStackNavigator = {
homePageWithPaymentOptions: undefined;
numberAndOtpForPaymentScreen: undefined;
numberAndOtpForPaymentScreen: {
paymentType: PaymentType;
amount: number;
};
paymentAmountInputScreen: {
paymentType: PaymentCode;
paymentType: PaymentType;
};
};
......@@ -23,7 +25,7 @@ export type ImainStackNavigator = {
appBottomTabsNavigator: undefined;
paymentResultScreen: undefined;
waveQrCodePaymentScreen: {
data: IwaveStarterRespone;
data: WaveTransactionInitilizationResponse;
};
};
......
import { configureStore } from "@reduxjs/toolkit";
import { authReducer } from "../features/auth/slice";
export const store = configureStore({
reducer: {
auth: authReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export type AppReduxStore = typeof store;
import type { PaymentStackScreenComponentProps } from "@/navigations/Types";
import getPaymentTypes from "@/utils/requests/getPaymentTypes";
import BalanceContainer from "@components/BalanceContainer";
import BarWithBeasyAndNotificationsIcon from "@components/BarWithBeasyAndNotificationsIcon";
import PaymentOption from "@components/PaymentOption";
import Box from "@components/bases/Box";
import WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight from "@components/wrappers/WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight";
import { asp as g } from "@asp/asp";
import { Balance } from "@components/Balance";
import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BeasyLogoIcon from "@components/BeasyLogoIcon";
import Ionicons from "@expo/vector-icons/Ionicons";
import { LOG } from "@logger";
import Card from "@re-card";
import Text from "@re-text";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { Dimensions } from "react-native";
import { Text, View } from "react-native";
import { getPaymentTypes } from "@/features/pay/api";
import PaymentType from "@/features/pay/components/PaymentType";
import type { PaymentStackScreenComponentProps } from "@/navigations/types";
const log = LOG.extend("HomePageWithPaymentOptions");
const HomePageWithPaymentOptions: PaymentStackScreenComponentProps<"homePageWithPaymentOptions"> =
({ navigation }) => {
const HomePageWithPaymentOptions: PaymentStackScreenComponentProps<
"homePageWithPaymentOptions"
> = ({ navigation }) => {
log.debug("HomePageWithPaymentOptions");
const { data, isLoading, error } = useQuery({
const paymentTypesQuery = useQuery({
queryKey: ["paymentTypes"],
queryFn: getPaymentTypes,
enabled: true,
});
// getting valid payments supported
const paymentTypesWithActiveStatus = useMemo(() => {
log.info("Filtering payment types");
const paymentTypes = data?.results || [];
return paymentTypes.filter((paymentType) => paymentType.etat === true);
}, [data]);
log.info(
"payment types to render",
paymentTypesWithActiveStatus.map((paymentType) => paymentType.code),
);
const paymentTypes = paymentTypesQuery.isSuccess
? Object.keys(paymentTypesQuery.data.data.results)
.map((key) => paymentTypesQuery.data.data.results[key])
.filter((item) => item.etat === true)
: [];
return (
<WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight>
<Box style={{ height: "100%" }} flexDirection={"column"}>
<BarWithBeasyAndNotificationsIcon />
<Card
variant={"curvedTopContainer"}
height={Dimensions.get("window").height / 2 + 150}
style={{ marginTop: "auto" }}
padding={"l"}
>
<Box position={"relative"} top={-120}>
<Box alignSelf={"center"}>
<BalanceContainer balance={78000} label="Total des ventes" />
</Box>
<Box marginVertical={"l"}>
<Text fontSize={20} fontWeight={"bold"}>
Types de paiement
</Text>
</Box>
<BarnoinPayBackground style={[g.flex_col, g.relative]}>
<View style={[g.px_lg, g.flex_row, g.justify_between]}>
<BeasyLogoIcon />
<Ionicons name="notifications" size={24} color="black" />
</View>
<Box
flex={1}
flexDirection={"row"}
justifyContent={"space-between"}
flexWrap={"wrap"}
rowGap={"m"}
<View
style={[
g.absolute,
g.z_10,
g.align_center,
g.justify_center,
g.w_full,
{ top: 120 },
]}
>
{isLoading && (
<Box flex={1}>
<Text textAlign={"center"}>
Chargement des méthodes de paiement...
</Text>
</Box>
)}
{!isLoading &&
!error &&
paymentTypesWithActiveStatus.map((paymentType) => (
<PaymentOptionContainer key={paymentType.id}>
<PaymentOption
// key={paymentType.id}
onPress={() =>
navigation.navigate(
"paymentAmountInputScreen",
<Balance amount={0} />
</View>
<View
style={[
g.px_lg,
g.pb_lg,
g.gap_lg,
{
paymentType: paymentType.code,
backgroundColor: "white",
height: "80%",
marginTop: "auto",
borderTopRightRadius: 30,
borderTopLeftRadius: 30,
paddingTop: 150,
},
)
}
paymentMethod={paymentType.code}
]}
>
<Text style={[g.text_2xl, g.font_bold]}>Types de paiement</Text>
<View style={[g.flex_1, g.flex_wrap, g.flex_row, g.gap_lg, g.justify_between]}>
{paymentTypes.map((paymentType) => {
return (
<PaymentType
onPress={() => {
navigation.navigate("paymentAmountInputScreen", {
paymentType,
});
}}
key={paymentType.reference}
style={[{ width: "47%" }]}
type={paymentType.code}
/>
</PaymentOptionContainer>
))}
</Box>
</Box>
</Card>
</Box>
</WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight>
);
};
export default HomePageWithPaymentOptions;
const screenWidth = Dimensions.get("window").width;
const paymentOptionCardWidth = screenWidth / 2 - 30;
const paymentOptionCardHeight = paymentOptionCardWidth * 0.65;
const PaymentOptionContainer = ({ children }: { children: React.ReactNode }) => {
return (
<Box
width={paymentOptionCardWidth}
height={paymentOptionCardHeight}
borderRadius={30}
overflow={"hidden"}
>
{children}
</Box>
})}
</View>
</View>
</BarnoinPayBackground>
);
};
export default HomePageWithPaymentOptions;
import type { MainStackScreenComponentProps } from "@/navigations/Types";
import Button from "@components/Button";
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import { Image, StyleSheet } from "react-native";
import { asp as g } from "@asp/asp";
import * as Button from "@components/Button";
import { Image } from "expo-image";
import { Text, View } from "react-native";
import type { MainStackScreenComponentProps } from "@/navigations/types";
const HomeUserNotLoggedIn: MainStackScreenComponentProps<"homeUserNotLoggedIn"> = ({
navigation,
}) => {
return (
<>
<Box style={style.container} p={"xl"} backgroundColor={"white"}>
<Box mt={"s"}>
<Box
height={300}
width={300}
backgroundColor={"primary"}
mb={"l"}
alignItems={"center"}
justifyContent={"center"}
>
<View style={[g.flex_1, g.p_5xl, g.gap_lg]}>
<View style={[{ height: 400 }]}>
<Image
source={require("../../assets/payment_processing.png")}
style={style.image}
source={require("@assets/payment_processing.png")}
style={{ width: "100%", height: "100%" }}
contentFit="contain"
cachePolicy={"memory-disk"}
/>
</Box>
<Text color={"gray"} fontSize={16} textAlign={"center"}>
Acceptez des paiements en magasin et en ligne et recevez votre argent en
quelques secondes
</View>
<Text style={[g.text_center, { color: "gray" }]}>
Acceptez des paiements en magasin et en ligne et recevez votre argent en quelques
secondes
</Text>
<Box mt={"xl"}>
<Button
variant={"full"}
textVariants={"primary"}
label="Se connecter"
<Button.Container
onPress={() => navigation.navigate("userLoginScreen")}
/>
<Button
variant={"clean"}
textVariants={"secondary"}
label="Creer un compte"
onPress={() => {}}
/>
</Box>
</Box>
</Box>
</>
style={[g.mt_5xl]}
>
<Button.Label style={{ color: "white" }}>Se connecter</Button.Label>
</Button.Container>
<Button.Container style={[{ backgroundColor: "transparent" }]}>
<Button.Label style={{ color: "green" }}>Créer un compte</Button.Label>
</Button.Container>
</View>
);
};
const style = StyleSheet.create({
container: {
width: "100%",
height: "100%",
flex: 1,
alignItems: "center",
justifyContent: "center",
},
image: {
width: "100%",
height: "100%",
},
});
export default HomeUserNotLoggedIn;
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 BackgroundGreenWhiteContentArea from "@components/backgrounds/BackgroundGreenWhiteContentArea";
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import * as Button from "@components/Button";
import * as Input from "@components/Input";
import AntDesign from "@expo/vector-icons/AntDesign";
import { LOG } from "@logger";
import { SafeAreaView } from "react-native-safe-area-context";
import { Text, View } from "react-native";
import PaymentType from "@/features/pay/components/PaymentType";
import type { PaymentStackScreenComponentProps } from "@/navigations/types";
const log = LOG.extend("NumberAndOtpForPaymentScreen");
const _log = LOG.extend("NumberAndOtpForPaymentScreen");
const NumberAndOtpForPaymentScreen: PaymentStackScreenComponentProps<
"numberAndOtpForPaymentScreen"
> = ({ navigation }) => {
console.debug("NumberAndOtpForPaymentScreen");
> = ({ navigation, route }) => {
return (
<BackgroundGreenWhiteContentArea>
<SafeAreaView>
<Box style={{ height: "100%", width: "100%" }}>
<Box
px={"l"}
flexDirection={"row"}
justifyContent={"space-between"}
alignItems={"center"}
>
<BarnoinPayBackground style={[g.flex_col, g.gap_lg, g.relative]}>
<View style={[g.px_lg, g.flex_row, g.align_center, g.justify_between]}>
<BeasyLogoIcon />
<GoBackIconButton onPress={() => navigation.goBack()} />
</Box>
<Box height={122} alignItems={"center"} justifyContent={"center"}>
<Text color={"white"}>Montant à payer</Text>
<Text color={"white"} fontSize={30}>
78000
</Text>
</Box>
<Box p={"l"}>
<Box width={100} height={70} mb={"l"}>
<PaymentOption onPress={() => {}} paymentMethod={"OM"} />
</Box>
<Box mb={"l"}>
<InputWithTopLabel
label="Entrez le numero"
keyboardType="numeric"
textVariants={"black"}
/>
<InputWithTopLabel
label="Code OTP"
keyboardType="numeric"
textVariants={"black"}
<AntDesign
name="arrowleft"
size={24}
color="black"
onPress={() => navigation.goBack()}
/>
</Box>
<Button
onPress={() => {}}
variant={"full"}
textVariants={"primary"}
label="Confirmer"
/>
</Box>
</Box>
</SafeAreaView>
</BackgroundGreenWhiteContentArea>
</View>
<View style={[g.px_lg, g.align_center, g.justify_center]}>
<Text style={[g.font_bold, g.text_2xl, { color: "white" }]}>Montant à payé </Text>
<Text style={[g.font_bold, g.text_2xl, { color: "white" }]}>
{route.params.amount}
</Text>
</View>
<View
style={[
g.flex_1,
g.p_5xl,
g.gap_5xl,
{ backgroundColor: "white", borderTopLeftRadius: 20, borderTopRightRadius: 20 },
]}
>
<PaymentType type={route.params.paymentType.code} />
<Input.Container>
<Input.Header>Entrer le numéro</Input.Header>
<Input.FieldContainer>
<Input.Field keyboardType="number-pad" />
</Input.FieldContainer>
</Input.Container>
<Input.Container>
<Input.Header>Code OTP</Input.Header>
<Input.FieldContainer>
<Input.Field keyboardType="number-pad" />
</Input.FieldContainer>
</Input.Container>
<Button.Container>
<Button.Label>Confirmer</Button.Label>
</Button.Container>
</View>
</BarnoinPayBackground>
);
};
......
import type { MainStackScreenComponentProps } 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 BeasyDefaultBackgroundWrapper from "@components/backgrounds/BeasyDefaultBackground";
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import * as Button from "@components/Button";
import CheckIcon from "@components/icons/CheckIcon";
import { AntDesign } from "@expo/vector-icons";
import { LOG } from "@logger";
import { CommonActions } from "@react-navigation/native";
// import { Text } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { MainStackScreenComponentProps } from "@/navigations/types";
const log = LOG.extend("PaymentResultScreen");
......@@ -20,122 +18,109 @@ const PaymentResultScreen: MainStackScreenComponentProps<"paymentResultScreen">
const insets = useSafeAreaInsets();
log.debug("insets", insets);
return (
<BeasyDefaultBackgroundWrapper>
<SafeAreaView edges={["top", "left", "right"]}>
<Box
style={{
height: "100%",
width: "100%",
// marginTop: insets.top,
}}
>
<Box
px={"l"}
flexDirection={"row"}
justifyContent={"space-between"}
alignItems={"center"}
mb={"m"}
>
<BarnoinPayBackground style={[g.gap_lg]}>
<View style={[g.px_lg, g.flex_row, g.align_center, g.justify_between]}>
<BeasyLogoIcon />
<GoBackIconButton
onPress={() => {
navigation.dispatch(
CommonActions.reset({
index: 1,
routes: [{ name: "appBottomTabsNavigator" }],
}),
);
}}
<AntDesign
name="arrowleft"
size={24}
color="black"
onPress={() => navigation.goBack()}
/>
</Box>
<Box
flex={1}
backgroundColor={"white"}
borderRadius={20}
p={"l"}
flexDirection={"column"}
gap={"l"}
</View>
<View style={[g.flex_1]}>
<View
style={[
g.flex_1,
g.gap_lg,
g.p_lg,
{
backgroundColor: "white",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
]}
>
<Box alignItems={"center"}>
<View style={[g.self_center]}>
<CheckIcon />
</Box>
<Text variant={"secondary"} fontWeight={"bold"} textAlign={"center"}>
Transactions effectué avec succès !
</Text>
<Button
width={200}
alignSelf={"center"}
variant={"full"}
textVariants={"white"}
label={"Imprimer le réçu"}
onPress={() => {}}
/>
<Box backgroundColor={"lightGray"} flex={1} borderRadius={20} p={"l"}>
<Box
flexDirection={"row"}
justifyContent={"space-between"}
borderBottomColor={"gray"}
borderBottomWidth={1}
py={"m"}
</View>
<Text style={[g.text_center]}>Transactions effectué avec succès !</Text>
<Button.Container style={[g.self_center, { width: 200 }]}>
<Button.Label>Imprimer le réçu</Button.Label>
</Button.Container>
<View style={[g.rounded_lg, g.p_lg, { backgroundColor: "#eeeef1ff" }]}>
<View
style={[
g.flex_row,
g.justify_between,
g.p_md,
{ borderBottomColor: "#000", borderBottomWidth: 1 },
]}
>
<Text fontWeight={"bold"}>Caisse</Text>
<Text variant={"black"}>00147C</Text>
</Box>
<Box
flexDirection={"row"}
justifyContent={"space-between"}
borderBottomColor={"gray"}
borderBottomWidth={1}
py={"m"}
<Text style={[g.font_bold]}>Caisse</Text>
<Text>00147C</Text>
</View>
<View
style={[
g.flex_row,
g.justify_between,
g.p_md,
{ borderBottomColor: "#000", borderBottomWidth: 1 },
]}
>
<Text fontWeight={"bold"}>Reference</Text>
<Text variant={"black"}>CP...</Text>
</Box>
<Box
flexDirection={"row"}
justifyContent={"space-between"}
borderBottomColor={"gray"}
borderBottomWidth={1}
py={"m"}
<Text style={[g.font_bold]}>Reference</Text>
<Text>CP...</Text>
</View>
<View
style={[
g.flex_row,
g.justify_between,
g.p_md,
{ borderBottomColor: "#000", borderBottomWidth: 1 },
]}
>
<Text fontWeight={"bold"}>Mode de paiement</Text>
<Text variant={"black"}>Orange</Text>
</Box>
<Box
flexDirection={"row"}
justifyContent={"space-between"}
borderBottomColor={"gray"}
borderBottomWidth={1}
py={"m"}
<Text style={[g.font_bold]}>Mode de paiement</Text>
<Text>Orange</Text>
</View>
<View
style={[
g.flex_row,
g.justify_between,
g.p_md,
{ borderBottomColor: "#000", borderBottomWidth: 1 },
]}
>
<Text fontWeight={"bold"}>Infos client</Text>
<Text variant={"black"}>Dogeless Miso</Text>
</Box>
<Box
flexDirection={"row"}
justifyContent={"space-between"}
borderBottomColor={"gray"}
borderBottomWidth={1}
py={"m"}
<Text style={[g.font_bold]}>Infos client</Text>
<Text>Dogeless Miso</Text>
</View>
<View
style={[
g.flex_row,
g.justify_between,
g.p_md,
{ borderBottomColor: "#000", borderBottomWidth: 1 },
]}
>
<Text fontWeight={"bold"}>Montant</Text>
<Text variant={"black"}>10</Text>
</Box>
<Box
flexDirection={"row"}
justifyContent={"space-between"}
borderBottomColor={"gray"}
borderBottomWidth={1}
py={"m"}
<Text style={[g.font_bold]}>Montant</Text>
<Text>10</Text>
</View>
<View
style={[
g.flex_row,
g.justify_between,
g.p_md,
{ borderBottomColor: "#000" },
]}
>
<Text fontWeight={"bold"}>N° Client</Text>
<Text variant={"black"}>Dogeless Misso</Text>
</Box>
</Box>
</Box>
</Box>
</SafeAreaView>
</BeasyDefaultBackgroundWrapper>
<Text style={[g.font_bold]}>N° Client</Text>
<Text>Dogeless Misso</Text>
</View>
</View>
</View>
</View>
</BarnoinPayBackground>
);
};
......
import { useUserAuthenticationContext } from "@/contexts/UserAuthenticationContext";
import type { MainStackScreenComponentProps } from "@/navigations/Types";
import Button from "@components/Button";
import InputWithTopLabel from "@components/InputWithTopLabel";
import Box from "@components/bases/Box";
import WrapperWithDefaultBeasyBackgroundAndSafeAreaFull from "@components/wrappers/WrapperWithDefaultBeasyBackgroundAndSafeAreaFull";
import { Fontisto } from "@expo/vector-icons";
import { asp as g } from "@asp/asp";
import { LOG } from "@logger";
import Card from "@re-card";
import Text from "@re-text";
import { useCallback, useState } from "react";
import { TouchableOpacity } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ImageBackground, View } from "react-native";
import { LoginForm } from "@/features/auth/components/LoginForm";
import type { MainStackScreenComponentProps } from "@/navigations/types";
const log = LOG.extend("UserLoginScreen");
const UserLoginScreen: MainStackScreenComponentProps<"userLoginScreen"> = ({ navigation }) => {
log.debug("UserLoginScreen");
const { login, isAuthenticating } = useUserAuthenticationContext();
// TODO : Remove default value for email and password
const [email, setEmail] = useState("admin");
const [password, setPassword] = useState("admin");
const insets = useSafeAreaInsets();
const submit = useCallback(() => {
login(email, password);
}, [email, password, login]);
return (
<WrapperWithDefaultBeasyBackgroundAndSafeAreaFull>
<Box height={"100%"}>
<Box style={{ height: "20%" }} px={"l"}>
<Box
px={"m"}
justifyContent={"space-between"}
flexDirection={"row"}
alignItems={"center"}
<View style={[g.flex_1]}>
<ImageBackground
source={require("@assets/background.png")}
style={[g.flex_1, g.flex_col, g.justify_end]}
>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Fontisto name="close-a" size={12} color="black" />
</TouchableOpacity>
<TouchableOpacity>
<Text>Mot de passe oublie ?</Text>
</TouchableOpacity>
</Box>
</Box>
<Card variant={"curvedTopContainer"} style={{ marginTop: "auto" }}>
<KeyboardAwareScrollView
// extraScrollHeight={-125}
extraHeight={10}
keyboardOpeningTime={Number.MAX_SAFE_INTEGER}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps={"handled"}
>
<Box p={"l"} paddingTop={"x100"} mb={"s"}>
<Box mb={"l"}>
<Text fontSize={40} fontWeight={"bold"} variant={"black"}>
Connexion
</Text>
<Text color={"gray"}>Bienvenue, vous nous avez manqué !</Text>
</Box>
<Box gap={"m"}>
<InputWithTopLabel
label="Email"
// value={email}
autoCorrect={false}
textContentType="emailAddress"
onChangeText={(email) => setEmail(email)}
/>
<InputWithTopLabel
label="Mot de passe"
secureTextEntry={true}
textContentType="oneTimeCode"
// value={password}
onChangeText={(text) => setPassword(text)}
/>
</Box>
</Box>
<Box p={"s"}>
<Button
variant={"full"}
textVariants={"primary"}
label="Se connecter"
onPress={() => {
submit();
}}
isLoading={isAuthenticating}
/>
<Button
variant={"lightGray"}
textVariants={"black"}
label="Creer un compte"
onPress={() => {}}
/>
</Box>
</KeyboardAwareScrollView>
</Card>
</Box>
<Box
position={"absolute"}
bottom={0}
height={insets.bottom}
backgroundColor={"white"}
width={"100%"}
/>
</WrapperWithDefaultBeasyBackgroundAndSafeAreaFull>
<LoginForm />
</ImageBackground>
</View>
);
};
......
import { useUserAuthenticationContext } from "@/contexts/UserAuthenticationContext";
import BarWithBeasyAndNotificationsIcon from "@components/BarWithBeasyAndNotificationsIcon";
import Button from "@components/Button";
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight from "@components/wrappers/WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight";
import { asp as g } from "@asp/asp";
import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BeasyLogoIcon from "@components/BeasyLogoIcon";
import * as Button from "@components/Button";
import Ionicons from "@expo/vector-icons/Ionicons";
import { LOG } from "@logger";
import Card from "@re-card";
import { Text, View } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { logout } from "@/features/auth/slice";
import type { RootState } from "@/redux";
const log = LOG.extend("UserProfileScreen");
const UserProfileScreen = () => {
log.verbose("UserProfileScreen");
const { logout } = useUserAuthenticationContext();
const { userInformations } = useUserAuthenticationContext();
const dispatch = useDispatch();
const userInformations = useSelector((state: RootState) => state.auth.user);
return (
<WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight>
<>
<BarWithBeasyAndNotificationsIcon />
<Card
variant="curvedTopContainer"
height={"100%"}
padding={"m"}
gap={"m"}
marginTop={"m"}
>
<Box
width={"100%"}
// height={200}
backgroundColor={"lightGray"}
borderRadius={10}
p={"s"}
>
<Box
width={"100%"}
// height={"100%"}
flexDirection={"row"}
gap={"s"}
py={"m"}
borderBottomColor={"white"}
borderBottomWidth={1}
>
<Box
height={70}
aspectRatio={1}
borderRadius={50}
backgroundColor={"secondary"}
<BarnoinPayBackground style={[g.flex_col, g.justify_between]}>
<View style={[g.px_lg, g.flex_row, g.justify_between]}>
<BeasyLogoIcon />
<Ionicons name="notifications" size={24} color="black" />
</View>
<View
style={[
g.gap_md,
g.p_md,
{
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
backgroundColor: "white",
height: "90%",
},
]}
>
<Text />
</Box>
<View style={[g.w_full, g.rounded_md, g.p_md, { backgroundColor: "#F4F4F4" }]}>
<View style={[g.flex_row, g.gap_sm, g.py_sm]}>
<View
style={[
g.rounded_full,
{ height: 70, aspectRatio: 1, backgroundColor: "green" },
]}
/>
<Box height={"100%"} flex={1} flexDirection={"column"}>
<Text fontWeight={"bold"} variant={"black"} fontSize={20}>
<View style={[g.flex_1, g.flex_col]}>
<Text style={[g.font_bold]}>
{userInformations.first_name} {userInformations.last_name}
</Text>
<Text>{userInformations.email}</Text>
</Box>
</Box>
<Box
width={"100%"}
flexDirection={"row"}
justifyContent={"space-between"}
py={"m"}
>
<Text variant={"black"} fontWeight={"bold"}>
Utilisateur
</Text>
<Text textAlign={"center"}>{userInformations.username}</Text>
</Box>
</Box>
<Button
label={"Deconnexion"}
onPress={logout}
variant={"danger"}
textVariants={"white"}
/>
</View>
</View>
<View style={[g.w_full, g.flex_row, g.justify_between, g.py_md]}>
<Text style={[g.font_bold]}>Utilisateur</Text>
<Text style={[g.text_center]}>{userInformations.username}</Text>
</View>
</View>
<Text fontWeight={"bold"}>Informations sur le marchand</Text>
<Box
width={"100%"}
backgroundColor={"lightGray"}
borderRadius={10}
p={"s"}
flexDirection={"column"}
// gap={"m"}
<Button.Container
style={[g.rounded_lg, { backgroundColor: "#960101ff" }]}
onPress={() => dispatch(logout())}
>
<Box
width={"100%"}
flexDirection={"row"}
borderBottomWidth={1}
borderBottomColor={"white"}
justifyContent={"space-between"}
py={"m"}
<Button.Label>Deconnexion</Button.Label>
</Button.Container>
<Text style={[g.font_bold]}>Informations sur le marchand</Text>
<View
style={[
g.w_full,
g.rounded_md,
g.p_sm,
g.flex_col,
{ backgroundColor: "#F4F4F4" },
]}
>
<Text variant={"black"} fontWeight={"bold"}>
Identifiant
</Text>
<Text textAlign={"center"}>
{userInformations.marchand.marchand_id}
</Text>
</Box>
<Box
width={"100%"}
flexDirection={"row"}
borderBottomWidth={1}
borderBottomColor={"white"}
justifyContent={"space-between"}
py={"m"}
<View
style={[
g.w_full,
g.flex_row,
g.justify_between,
g.py_md,
{ borderBottomColor: "white", borderBottomWidth: 1 },
]}
>
<Text variant={"black"} fontWeight={"bold"}>
Entreprise
</Text>
<Text textAlign={"center"}>{userInformations.marchand.nom}</Text>
</Box>
<Box
width={"100%"}
flexDirection={"row"}
borderBottomWidth={1}
borderBottomColor={"white"}
justifyContent={"space-between"}
py={"m"}
<Text style={[g.font_bold]}>Identifiant</Text>
<Text style={[g.text_center]}>{userInformations.marchand.marchand_id}</Text>
</View>
<View
style={[
g.w_full,
g.flex_row,
g.justify_between,
g.py_md,
{ borderBottomColor: "white", borderBottomWidth: 1 },
]}
>
<Text variant={"black"} fontWeight={"bold"}>
Code
</Text>
<Text textAlign={"center"}>{userInformations.marchand.code}</Text>
</Box>
<Box
width={"100%"}
flexDirection={"row"}
borderBottomWidth={1}
borderBottomColor={"white"}
justifyContent={"space-between"}
py={"m"}
<Text style={[g.font_bold]}>Entreprise</Text>
<Text style={[g.text_center]}>{userInformations.marchand.nom}</Text>
</View>
<View
style={[
g.w_full,
g.flex_row,
g.justify_between,
g.py_md,
{ borderBottomColor: "white", borderBottomWidth: 1 },
]}
>
<Text variant={"black"} fontWeight={"bold"}>
Addresse
</Text>
<Text textAlign={"center"}>{userInformations.marchand.adresse}</Text>
</Box>
<Box
width={"100%"}
flexDirection={"row"}
borderBottomWidth={1}
borderBottomColor={"white"}
justifyContent={"space-between"}
py={"m"}
alignItems={"center"}
<Text style={[g.font_bold]}>Code</Text>
<Text style={[g.text_center]}>{userInformations.marchand.code}</Text>
</View>
<View
style={[
g.w_full,
g.flex_row,
g.justify_between,
g.py_md,
{ borderBottomColor: "white", borderBottomWidth: 1 },
]}
>
<Text variant={"black"} fontWeight={"bold"}>
Url succès
</Text>
<Text textAlign={"center"}>{userInformations.marchand.url_succes}</Text>
</Box>
<Box
width={"100%"}
flexDirection={"row"}
justifyContent={"space-between"}
// alignItems={"center"}
py={"m"}
<Text style={[g.font_bold]}>Addresse</Text>
<Text style={[g.text_center]}>{userInformations.marchand.adresse}</Text>
</View>
<View
style={[
g.w_full,
g.flex_row,
g.justify_between,
g.py_md,
{ borderBottomColor: "white", borderBottomWidth: 1 },
]}
>
<Text variant={"black"} fontWeight={"bold"}>
Url échec
</Text>
<Text textAlign={"center"}>{userInformations.marchand.url_echec}</Text>
</Box>
</Box>
</Card>
</>
</WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight>
<Text style={[g.font_bold]}>Url succès</Text>
<Text style={[g.text_center]}>{userInformations.marchand.url_succes}</Text>
</View>
<View style={[g.w_full, g.flex_row, g.py_md, g.justify_between]}>
<Text style={[g.font_bold]}>Url échec</Text>
<Text style={[g.text_center]}>{userInformations.marchand.url_echec}</Text>
</View>
</View>
</View>
</BarnoinPayBackground>
);
};
......
import type { MainStackScreenComponentProps } from "@/navigations/Types";
import Button from "@components/Button";
import BackgroundWithBeasyIconAndWhiteContentArea from "@components/backgrounds/BackgroundWithBeasyIconAndWhiteContentArea";
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import useWave from "@hooks/useWave";
import { asp as g } from "@asp/asp";
import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BeasyLogoIcon from "@components/BeasyLogoIcon";
import * as Button from "@components/Button";
import * as Modal from "@components/Modal";
import AntDesign from "@expo/vector-icons/AntDesign";
import { LOG } from "@logger";
import { Dimensions } from "react-native";
// biome-ignore lint/style/useNamingConvention: <explanation>
import QRCode from "react-native-qrcode-svg";
import { useMutation } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { useState } from "react";
import { Dimensions, ScrollView, Text, View } from "react-native";
import QrCode from "react-native-qrcode-svg";
import { waveGetTransactionStatus } from "@/features/pay/api";
import type { MainStackScreenComponentProps } from "@/navigations/types";
const log = LOG.extend("WaveQrCodePaymentScreen");
......@@ -16,62 +20,108 @@ const WaveQrCodePaymentScreen: MainStackScreenComponentProps<"waveQrCodePaymentS
navigation,
}) => {
log.verbose("WaveQrCodePaymentScreen");
const [error, setError] = useState("");
const data = route.params.data;
const windowWidth = Dimensions.get("window").width;
const { handlePaymentVerification } = useWave();
const qrSize = windowWidth * 0.7;
const [isSuccess, setIsSuccess] = useState(false);
const query = useMutation({
mutationKey: ["waveTransactionStatus", data.id],
mutationFn: () => waveGetTransactionStatus(data.id),
onError: (err: AxiosError) => {
setError(JSON.stringify(err.response?.data) || err.message);
},
onSuccess: (_data) => {
setIsSuccess(true);
},
});
return (
<BackgroundWithBeasyIconAndWhiteContentArea goBack={true}>
<Box
style={{
height: "100%",
width: "100%",
}}
p={"m"}
alignItems={"center"}
>
<Text
variant={"black"}
mb={"l"}
fontSize={20}
textAlign={"center"}
fontWeight={"bold"}
<BarnoinPayBackground style={[g.justify_between]}>
<View style={[g.px_lg, g.flex_row, g.align_center, g.justify_between]}>
<BeasyLogoIcon />
<AntDesign
name="arrowleft"
size={24}
color="black"
onPress={() => navigation.goBack()}
/>
</View>
<View
style={[
g.w_full,
g.p_5xl,
g.gap_xl,
{
backgroundColor: "white",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
height: "80%",
},
]}
>
Votre QR Code
</Text>
<Text style={[g.text_3xl, g.font_bold]}>Votre QR Code</Text>
<Box
style={{ height: windowWidth - 80, aspectRatio: 1 }}
backgroundColor={"primary"}
alignItems={"center"}
justifyContent={"center"}
borderRadius={20}
borderWidth={2}
borderColor={"secondary"}
shadowColor={"black"}
shadowOffset={{ width: 0, height: 10 }}
shadowOpacity={0.5}
shadowRadius={13.16}
elevation={20}
mb={"l"}
<View
style={[
g.w_full,
g.align_center,
g.justify_center,
g.p_md,
g.rounded_lg,
g.shadow_elevated,
{ aspectRatio: 1, backgroundColor: "white" },
]}
>
<QRCode
value={data.wave_launch_url}
size={windowWidth - 120}
// backgroundColor={theme.colors.secondary}
/>
</Box>
<QrCode value={data.wave_launch_url} size={qrSize} />
</View>
<Text>Veuillez scanner le QR Code pour terminer le paiement</Text>
<Box width={"100%"} mt={"x100"}>
<Button
variant={"full"}
textVariants={"white"}
label="Verification"
onPress={() => handlePaymentVerification(data.id)}
/>
</Box>
</Box>
</BackgroundWithBeasyIconAndWhiteContentArea>
<Button.Container onPress={() => query.mutate()} isLoading={query.isPending}>
<Button.Label>Verification</Button.Label>
</Button.Container>
</View>
<Modal.RnModal visible={!!error} style={{ backgroundColor: "rgba(0, 0, 0, 0.25)" }}>
<Modal.OuterView
style={[
g.p_md,
g.shadow_elevated,
{ backgroundColor: "white", width: "80%", height: "40%" },
]}
>
<View style={[g.flex_1, g.gap_md]}>
<ScrollView style={[g.flex_1]}>
<Text>{error}</Text>
</ScrollView>
<Button.Container onPress={() => setError("")}>
<Button.Label>OK</Button.Label>
</Button.Container>
</View>
</Modal.OuterView>
</Modal.RnModal>
<Modal.RnModal visible={isSuccess} style={{ backgroundColor: "rgba(0, 0, 0, 0.25)" }}>
<Modal.OuterView
style={[
g.p_md,
g.shadow_elevated,
{ backgroundColor: "white", width: "80%", height: "40%" },
]}
>
<View style={[g.flex_1, g.gap_md]}>
<Text style={[g.text_center]}>Transaction réussie</Text>
<Button.Container
onPress={() => navigation.getParent()?.navigate("paymentResultScreen")}
>
<Button.Label>OK</Button.Label>
</Button.Container>
</View>
</Modal.OuterView>
</Modal.RnModal>
</BarnoinPayBackground>
);
};
......
import { StyleSheet } from "react-native";
export const containers = StyleSheet.create({
fullScreenContentCentered: {
width: "100%",
height: "100%",
flex: 1,
alignItems: "center",
justifyContent: "center",
},
containerFull: {
width: "100%",
height: "100%",
flex: 1,
},
containerFlexUno: {
flex: 1,
},
});
export const images = StyleSheet.create({
cover: {
flex: 1,
width: "100%",
height: "100%",
resizeMode: "cover",
},
background: {
flex: 1,
resizeMode: "cover",
justifyContent: "center",
},
contain: {
flex: 1,
width: "100%",
height: "100%",
resizeMode: "contain",
},
});
import { OVERLAY_BACKDROP_Z_INDEX } from "@/contexts/ModalsManagerContext";
const OVERLAY_BACKDROP_Z_INDEX = 999;
import { Dimensions } from "react-native";
export const cardVariants = {
......
// To load assets asynchronously
import { Asset } from "expo-asset";
import { LOG } from "@logger";
const log = LOG.extend("assetsCache");
export default function cacheAssetsAsync({
images = [],
fonts = [],
videos = [],
}: { images?: string[]; fonts?: string[]; videos?: string[] }) {
return Promise.all([...cacheImages(images)]);
}
function cacheImages(images: string[]) {
return images.map((image) => Asset.fromModule(image).downloadAsync());
}
// function cacheVideos(videos) {
// return videos.map((video) => Asset.fromModule(video).downloadAsync());
// }
// function cacheFonts(fonts) {
// return fonts.map((font) => Font.loadAsync(font));
// }
import { LOG } from "@logger";
import axios, { type AxiosError, type AxiosResponse } from "axios";
const log = LOG.extend("AxiosRequest");
const baseUrl = process.env.EXPO_PUBLIC_API_URL;
// biome-ignore lint/style/useNamingConvention: <baseURL is not for me to change.>
const client = axios.create({ baseURL: baseUrl });
const axiosRequest = async <T>({ ...options }): Promise<T> => {
// console.log("base Url", baseUrl);
// client.defaults.headers.common.Authorization = `Bearer ${""}`;
// client.defaults.headers.common['Content-Type'] = 'application/json';
// console.log("client default", client.defaults);
// console.log("client datas", client.defaults.data);
log.debug("Base url :: ", client.getUri());
log.debug("RequestOptions :: ", options);
const onSuccess = (response: T) => {
return response;
};
const onError = (error: AxiosError) => {
log.error(error);
throw error;
};
try {
const response: AxiosResponse<T> = await client({ ...options });
return onSuccess(response.data);
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
log.error("axiosRequest | Response :: ", JSON.stringify(error.response, null, 2));
// log.error("Axios RequestError Reponse message:: ", error.message);
// log.error("Axios RequestError Reponse name:: ", error.response?.data);
} else {
log.error("axiosRequest | General RequestError :: ", JSON.stringify(error, null, 2));
}
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
return onError(error as any);
}
};
export default axiosRequest;
import type { IauthenticationData } from "@/contexts/Types";
import { LOG } from "@logger";
import { AxiosError } from "axios";
import axiosRequest from "../axiosRequest";
const log = LOG.extend("authenticateUser");
const authenticateUser = async ({ username, password }: { username: string; password: string }) => {
log.http({ username, password });
const response = await axiosRequest<IauthenticationData>({
url: "/login/token/",
method: "POST",
data: {
username: username,
password: password,
},
});
log.http(JSON.stringify(response, null, 2));
return response;
};
export default authenticateUser;
export const parseAuthicationErrors = (error: unknown): string => {
if (error instanceof AxiosError && error.response) {
switch (error.response.status) {
case 401:
return "Wrong username or password";
default:
return "Unknown Error. Please try again.";
}
}
if (error instanceof AxiosError && error.request) {
return "Network error";
}
return "Unknown error";
};
import { LOG } from "@logger";
import base64 from "react-native-base64";
import axiosRequest from "../axiosRequest";
import type { IpaginatedResponse, IpaymentType } from "./types";
const basictoken = base64.encode("admin:admin");
const log = LOG.extend("getPaymentTypes");
const getPaymentTypes = async () => {
log.http("getPaymentTypes");
const response = await axiosRequest<IpaginatedResponse<IpaymentType[]>>({
url: "/operateur/",
method: "GET",
headers: {
// biome-ignore lint/style/useNamingConvention: <explanation>
Authorization: `Basic ${basictoken}`,
},
});
log.http(JSON.stringify(response, null, 2));
return response;
};
export default getPaymentTypes;
import { LOG } from "@logger";
import base64 from "react-native-base64";
import axiosRequest from "../axiosRequest";
export interface IorangePaymentStarter {
// biome-ignore lint/style/useNamingConvention: <api expect type_paiement>
type_paiement: number;
marchand: "1";
service: string;
montant: number;
commentaire: string;
numero: string;
}
export interface IorangeResponse {
status: number;
message: string;
// biome-ignore lint/style/useNamingConvention: <api expect pay_token>
pay_token: string;
// biome-ignore lint/style/useNamingConvention: <api expect payment_url>
payment_url: string;
// biome-ignore lint/style/useNamingConvention: <api expect notif_token>
notif_token: string;
// biome-ignore lint/style/useNamingConvention: <api expect order_id>
order_id: string;
}
const basictoken = base64.encode("admin:admin");
export type OrangeStatus = "INITIATED" | "SUCCESS" | "FAILED";
export interface IorangePaymentStatus {
status: OrangeStatus;
code: number;
message: {
status: OrangeStatus;
// biome-ignore lint/style/useNamingConvention: <api expect order_id>
order_id: string;
txnid?: string;
};
}
const log = LOG.extend("orangePayment");
export const getTransactionsData = async (payload: IorangePaymentStarter) => {
log.http("getTransactionsData", payload);
// const basictoken = base64.encode("admin:admin");
const response = await axiosRequest<IorangeResponse>({
url: "/transactions/",
method: "POST",
headers: {
// biome-ignore lint/style/useNamingConvention: <explanation>
Authorization: `Basic ${basictoken}`,
},
data: payload,
});
log.http("getTransactionsData |", JSON.stringify(response, null, 2));
return response;
};
export const getTransactionStatus = async (orderId: string) => {
log.http("getTransactionStatus |", { orderId });
try {
const response = await axiosRequest<IorangePaymentStatus>({
url: `/api/TransactionCheckStatus/${orderId}/`,
method: "GET",
headers: {
// biome-ignore lint/style/useNamingConvention: <explanation>
Authorization: `Basic ${basictoken}`,
},
});
log.http("getTransactionStatus |", JSON.stringify(response, null, 2));
switch (response.status) {
case "SUCCESS": {
log.http("getTransactionStatus |", JSON.stringify(response, null, 2));
return response;
}
case "INITIATED": {
log.warn("Payment is still in progress, throwing error for mutation to catch");
const error = new Error("Payment is still in progress");
error.name = "ORANGE_PAYMENT_IN_PROGRESS";
throw error;
}
case "FAILED": {
log.warn("Payment failed, throwing error for mutation to catch");
const error = new Error("Payment failed");
error.name = "ORANGE_PAYMENT_FAILED";
throw error;
}
default: {
log.warn("An unknown error occured, throwing error for mutation to catch");
const error = new Error("Payment failed");
error.name = "ORANGE_UNKNOWN_PAYMENT_ERROR";
throw error;
}
}
} catch (error) {
log.error(
"getTransactionStatus | An unexpected error occured |",
JSON.stringify(error, null, 2),
);
throw error;
}
};
import { LOG } from "@logger";
import base64 from "react-native-base64";
import axiosRequest from "../axiosRequest";
import type { PaymentCode } from "./types";
const log = LOG.extend("transactions");
export interface Transaction {
// biome-ignore lint/style/useNamingConvention: <api response>
type_paiement: number;
// biome-ignore lint/style/useNamingConvention: <api response>
type_paiement_label: PaymentCode;
marchand: string;
// biome-ignore lint/style/useNamingConvention: <api response>
marchand_name: string;
service: string;
montant: number;
date: string;
commentaire: string;
etat: boolean;
status: "SUCCESS" | "INITIATED" | "FAILED";
reference: string;
// biome-ignore lint/style/useNamingConvention: <api response>
transaction_id: number;
// biome-ignore lint/style/useNamingConvention: <api response>
marchand_code: string;
}
export interface TransactionHistoryResponse {
count: number;
next: string | null;
previous: string | null;
results: Transaction[];
}
export const getTransactionsHistory = async (): Promise<Transaction[]> => {
const basictoken = base64.encode("admin:admin");
log.http("getTransactionsHistory");
try {
const response = await axiosRequest<TransactionHistoryResponse>({
url: "/transactions/",
headers: {
// biome-ignore lint/style/useNamingConvention: <explanation>
Authorization: `Basic ${basictoken}`,
},
});
log.http("getTransactionsHistory |", JSON.stringify(response, null, 2));
// TODO: Update this when the api is fixed, response should not be reversed
return response.results.reverse();
} catch (error) {
log.error("getTransactionsHistory |", error);
throw error;
}
};
export type PaymentCode = "OM" | "FLOOZ" | "MTN" | "WAVE" | "CB";
export interface IpaymentType {
id: number;
code: PaymentCode;
etat: boolean;
}
export interface IpaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T;
}
export interface ImerchandInformations {
// biome-ignore lint/style/useNamingConvention: <Api response>
marchand_id: string;
nom: string;
code: string;
adresse: string;
// biome-ignore lint/style/useNamingConvention: <Api response>
url_succes: string;
// biome-ignore lint/style/useNamingConvention: <Api response>
url_echec: string;
entreprise: number;
user: number;
}
export interface IuserInformations {
username: string;
email: string;
// biome-ignore lint/style/useNamingConvention: <Api response>
first_name: string;
// biome-ignore lint/style/useNamingConvention: <Api response>
last_name: string;
marchand: ImerchandInformations;
}
import { LOG } from "@logger";
import base64 from "react-native-base64";
import axiosRequest from "../axiosRequest";
import type { IuserInformations } from "./types";
const log = LOG.extend("getUserInformations");
const getUserInformations = async (userAccessToken: string) => {
log.http("getUserInformations", userAccessToken);
const basictoken = base64.encode("admin:admin");
log.http("basictoken", basictoken);
const response = await axiosRequest<IuserInformations>({
url: "/user-info/",
method: "GET",
headers: {
// biome-ignore lint/style/useNamingConvention: <Header>
Authorization: `Basic ${basictoken}`,
},
});
log.http(JSON.stringify(response, null, 2));
return response;
};
export default getUserInformations;
export const parseUserInformationsErrors = (error: unknown): string => {
// if (error instanceof AxiosError && error.response) {
// switch (error.response.status) {
// case 401:
// return "Wrong username or password";
// default:
// return "Unknown Error. Please try again.";
// }
// }
// if (error instanceof AxiosError && error.request) {
// return "Network error";
// }
return "Failure to fetch user informations. Please try again.";
};
import { LOG } from "@logger";
import base64 from "react-native-base64";
import axiosRequest from "../axiosRequest";
const log = LOG.extend("wavePayment");
export interface IwavePaymentStarter {
// biome-ignore lint/style/useNamingConvention: <API requirement>
type_paiement: 2; // id 2 is for wave.
marchand: string;
service: string;
montant: number;
}
export interface IwaveStarterRespone {
id: string;
amount: string;
// biome-ignore lint/style/useNamingConvention: <api response>
checkout_status: string;
// biome-ignore lint/style/useNamingConvention: <api response>
client_reference: unknown;
currenfy: string;
// biome-ignore lint/style/useNamingConvention: <api response>
error_url: string;
// biome-ignore lint/style/useNamingConvention: <api response>
last_payment_errror: unknown;
// biome-ignore lint/style/useNamingConvention: <api response>
business_name: string;
// biome-ignore lint/style/useNamingConvention: <api response>
payment_status: string;
// biome-ignore lint/style/useNamingConvention: <api response>
succes_url: string;
// biome-ignore lint/style/useNamingConvention: <api response>
wave_launch_url: string;
// biome-ignore lint/style/useNamingConvention: <api response>
when_completed: unknown;
// biome-ignore lint/style/useNamingConvention: <api response>
when_created: string;
// biome-ignore lint/style/useNamingConvention: <api response>
when_expires: string;
}
export interface IwaveStatusResponse {
status: string;
code: number;
message: IwaveStarterRespone;
}
const basictoken = base64.encode("admin:admin");
export const initTransaction = async (payload: IwavePaymentStarter) => {
log.http("initTransaction", payload);
// const basictoken = base64.encode("admin:admin");
try {
const response = await axiosRequest<IwaveStarterRespone>({
url: "/transactions/",
method: "POST",
headers: {
// biome-ignore lint/style/useNamingConvention: <explanation>
Authorization: `Basic ${basictoken}`,
},
data: payload,
});
log.http("initTransaction |", JSON.stringify(response, null, 2));
return response;
} catch (error) {
log.error("initTransaction |", error);
throw error;
}
};
export const getTransactionStatus = async (id: string) => {
log.http("getTransactionStatus", id);
try {
const response = await axiosRequest<IwaveStarterRespone>({
url: `/wave-session/${id}/`,
headers: {
// biome-ignore lint/style/useNamingConvention: <explanation>
Authorization: `Basic ${basictoken}`,
},
});
log.http("getTransactionStatus |", JSON.stringify(response, null, 2));
return response;
} catch (error) {
log.error("getTransactionStatus |", error);
throw error;
}
};
......@@ -14,6 +14,8 @@
"@styles/*": ["./src/styles/*"],
"@hooks/*": ["./src/hooks/*"],
"@logger": ["./src/utils/logger"],
"@asp/*": ["./src/appStylingPrimitives/*"],
"@assets/*": ["./assets/*"],
"@/*": ["./src/*"]
}
}
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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