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 @@ ...@@ -2,7 +2,8 @@
"editor.tabSize": 4, "editor.tabSize": 4,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": "explicit" "source.organizeImports.biome": "explicit"
// "source.fixAll.biome": "explicit"
}, },
"npm.packageManager": "yarn", "npm.packageManager": "yarn",
"editor.defaultFormatter": "biomejs.biome", "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 { LOG } from "@logger";
import { NavigationContainer } from "@react-navigation/native"; import { DefaultTheme, NavigationContainer } from "@react-navigation/native";
import { ThemeProvider } from "@shopify/restyle"; import { ThemeProvider } from "@shopify/restyle";
import ModalContainer from "react-modal-promise"; import { injectStoreIntoAxiosInstance } from "@/axios";
import { SafeAreaProvider } from "react-native-safe-area-context"; import ProvideQueryClient from "@/contexts/ProvideQueryClient";
import { AppMainStackNavigatorAuthWrapper } from "@/navigations/AppMainStackNavigator";
import theme from "@/themes/Theme";
import "react-native-gesture-handler"; import "react-native-gesture-handler";
import "react-native-reanimated"; 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"); const log = LOG.extend("App");
injectStoreIntoAxiosInstance(store);
export default function App() { export default function App() {
log.verbose("App started..."); log.verbose("App started...");
return ( return (
<Provider store={store}>
<GestureHandlerRootView style={{ flex: 1 }}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<ModalsManagerProvider>
<SafeAreaProvider> <SafeAreaProvider>
<ProvideQueryClient> <ProvideQueryClient>
<UserAuthenticationContextProvider> <NavigationContainer
<NavigationContainer> theme={{
...DefaultTheme,
colors: {
...DefaultTheme.colors,
background: "white",
},
}}
>
<AppMainStackNavigatorAuthWrapper /> <AppMainStackNavigatorAuthWrapper />
<StatusBar translucent backgroundColor="transparent" style="dark" />
</NavigationContainer> </NavigationContainer>
</UserAuthenticationContextProvider>
</ProvideQueryClient> </ProvideQueryClient>
</SafeAreaProvider> </SafeAreaProvider>
</ModalsManagerProvider>
<ModalContainer />
</ThemeProvider> </ThemeProvider>
</GestureHandlerRootView>
</Provider>
); );
} }
...@@ -15,9 +15,21 @@ ...@@ -15,9 +15,21 @@
"assetBundlePatterns": ["**/*"], "assetBundlePatterns": ["**/*"],
"ios": { "ios": {
"supportsTablet": true, "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": { "android": {
"softwareKeyboardLayoutMode": "pan",
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/beasy_icon.png", "foregroundImage": "./assets/beasy_icon.png",
"backgroundColor": "#00875A" "backgroundColor": "#00875A"
...@@ -26,7 +38,7 @@ ...@@ -26,7 +38,7 @@
"android.permission.USE_BIOMETRIC", "android.permission.USE_BIOMETRIC",
"android.permission.USE_FINGERPRINT" "android.permission.USE_FINGERPRINT"
], ],
"package": "com.idrissouattara.beasymobile" "package": "com.idrissouattara.barnoinpaymobile"
}, },
"web": { "web": {
"favicon": "./assets/favicon.png" "favicon": "./assets/favicon.png"
...@@ -35,13 +47,13 @@ ...@@ -35,13 +47,13 @@
[ [
"expo-local-authentication", "expo-local-authentication",
{ {
"faceIDPermission": "Allow B-Easy to use Face ID." "faceIDPermission": "Allow BarnoinPay to use Face ID."
} }
], ],
[ [
"expo-dev-launcher", "expo-dev-launcher",
{ {
"launchMode": "most-recent" "launchMode": "launcher"
} }
], ],
[ [
......
module.exports = function(api) { module.exports = function (api) {
api.cache(true); api.cache(true);
return { return {
presets: ['babel-preset-expo'], presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin']
}; };
}; };
{ {
"$schema": "https://biomejs.dev/schemas/1.7.1/schema.json", "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
"organizeImports": { "assist": { "actions": { "source": { "organizeImports": "on" } } },
"enabled": true
},
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"formatWithErrors": false, "formatWithErrors": false,
"ignore": [], "includes": ["**"],
"attributePosition": "auto", "attributePosition": "auto",
"indentStyle": "tab", "indentStyle": "tab",
"indentWidth": 4, "indentWidth": 4,
...@@ -19,10 +17,12 @@ ...@@ -19,10 +17,12 @@
"recommended": true, "recommended": true,
"correctness": { "correctness": {
"noUnusedImports": "warn", "noUnusedImports": "warn",
"noUnusedVariables": "warn" "noUnusedVariables": "warn",
"useUniqueElementIds": "warn"
}, },
"complexity": { "complexity": {
"noExcessiveCognitiveComplexity": "error" "noExcessiveCognitiveComplexity": "error",
"noUselessFragments": "off"
}, },
"style": { "style": {
"useConsistentArrayType": "warn", "useConsistentArrayType": "warn",
...@@ -36,23 +36,49 @@ ...@@ -36,23 +36,49 @@
"useNamingConvention": { "useNamingConvention": {
"level": "error", "level": "error",
"options": { "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" "useShorthandAssign": "warn"
}, },
"suspicious": { "suspicious": {
"noConsoleLog": "warn", "useAwait": "warn",
"useAwait": "warn" "noConsole": { "level": "warn", "options": { "allow": ["log"] } },
"noDuplicateElseIf": "warn",
"noAssignInExpressions": "off"
}, },
"nursery": { "nursery": {}
"noDuplicateElseIf": "warn"
}
} }
}, },
"overrides": [ "overrides": [
{ {
"include": ["src/utils/*"], "includes": [
"**/src/utils/**/*",
"**/src/api/**/*",
"**/types.ts",
"**/*.ts",
"**/hooks/**/*"
],
"linter": { "linter": {
"rules": { "rules": {
"style": { "style": {
...@@ -65,6 +91,16 @@ ...@@ -65,6 +91,16 @@
} }
} }
} }
},
{
"includes": ["**/src/appStylingPrimitives/**/*"],
"linter": {
"rules": {
"style": {
"useNamingConvention": "off"
}
}
}
} }
], ],
"vcs": { "vcs": {
...@@ -72,6 +108,16 @@ ...@@ -72,6 +108,16 @@
"clientKind": "git" "clientKind": "git"
}, },
"files": { "files": {
"ignore": ["babel.config.js", "eas.json"] "includes": [
"**",
"!**/*.config.js",
"!**/*.d.ts",
"!**/dist",
"!**/android",
"!**/ios",
"!**/node_modules",
"!**/.expo",
"!**/.vscode"
]
} }
} }
...@@ -5,16 +5,19 @@ ...@@ -5,16 +5,19 @@
}, },
"build": { "build": {
"development": { "development": {
"env": {
"EXPO_PUBLIC_API_URL": "http://192.168.1.223:8001"
},
"developmentClient": true, "developmentClient": true,
"distribution": "internal", "distribution": "internal",
"channel": "development" "channel": "development",
"android": {
"buildType": "apk"
},
"env": {
"EXPO_PUBLIC_API_URL": "http://51.77.152.180:8000"
}
}, },
"preview": { "preview": {
"env": { "env": {
"EXPO_PUBLIC_API_URL": "http://192.168.1.223:8001" "EXPO_PUBLIC_API_URL": "http://51.77.152.180:8000"
}, },
"distribution": "internal", "distribution": "internal",
"android": { "android": {
......
{ {
"name": "beasy-mobile", "name": "barnoinpay-mobile",
"version": "1.0.0", "version": "1.0.0",
"main": "node_modules/expo/AppEntry.js", "main": "node_modules/expo/AppEntry.js",
"scripts": { "scripts": {
...@@ -11,19 +11,26 @@ ...@@ -11,19 +11,26 @@
"format": "biome format --no-errors-on-unmatched --write .", "format": "biome format --no-errors-on-unmatched --write .",
"lint": "biome lint --no-errors-on-unmatched --apply .", "lint": "biome lint --no-errors-on-unmatched --apply .",
"biome-check": "biome check --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": { "dependencies": {
"@gorhom/bottom-sheet": "^5",
"@react-native-async-storage/async-storage": "2.1.2", "@react-native-async-storage/async-storage": "2.1.2",
"@react-navigation/bottom-tabs": "^7.0.0-alpha.22", "@react-navigation/bottom-tabs": "^7.4.6",
"@react-navigation/native": "^7.0.0-alpha.18", "@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.0.0-alpha.20", "@react-navigation/native-stack": "^7.3.25",
"@reduxjs/toolkit": "^2.8.2",
"@shopify/flash-list": "1.7.6",
"@shopify/restyle": "^2.4.4", "@shopify/restyle": "^2.4.4",
"@tanstack/react-query": "^5.35.1", "@tanstack/react-query": "^5.35.1",
"axios": "^1.6.8", "axios": "^1.6.8",
"expo": "53.0.22", "expo": "53.0.22",
"expo-build-properties": "~0.14.8", "expo-build-properties": "~0.14.8",
"expo-checkbox": "~4.1.4",
"expo-contacts": "~14.2.5",
"expo-dev-client": "~5.2.4", "expo-dev-client": "~5.2.4",
"expo-image": "~2.4.0",
"expo-local-authentication": "~16.0.5", "expo-local-authentication": "~16.0.5",
"expo-splash-screen": "~0.30.10", "expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~2.2.3",
...@@ -32,28 +39,35 @@ ...@@ -32,28 +39,35 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"moti": "^0.29.0", "moti": "^0.29.0",
"react": "19.0.0", "react": "19.0.0",
"react-modal-promise": "^1.0.2",
"react-native": "0.79.5", "react-native": "0.79.5",
"react-native-base64": "^0.2.1", "react-native-base64": "^0.2.1",
"react-native-date-picker": "5.0.12",
"react-native-gesture-handler": "~2.24.0", "react-native-gesture-handler": "~2.24.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-logs": "^5.1.0", "react-native-keyboard-controller": "^1.18.5",
"react-native-qrcode-svg": "^6.3.1", "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-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
"react-native-svg": "15.11.2", "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": { "devDependencies": {
"@babel/core": "^7.20.0", "@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": "~19.0.10",
"@types/react-native-base64": "^0.2.2", "@types/react-native-base64": "^0.2.2",
"@types/react-native-vector-icons": "^6.4.18", "@types/react-native-vector-icons": "^6.4.18",
"conventional-changelog": "^7.1.1",
"husky": "^9.0.11", "husky": "^9.0.11",
"lint-staged": "^15.2.2", "lint-staged": "^15.2.2",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react-native-svg-transformer": "^1.5.0",
"typescript": "~5.8.3" "typescript": "~5.8.3"
}, },
"private": true "private": true
......
# A set of primitives to make styling components faster through the use of tailwind type anotations.
import { Platform, StyleSheet } from "react-native";
import * as tokens from "./tokens";
export const asp = {
debug: {
borderColor: "red",
borderWidth: 1,
},
/*
* Positioning
*/
fixed: {
position: Platform.select({ web: "fixed", native: "absolute" }) as "absolute",
},
absolute: {
position: "absolute",
},
relative: {
position: "relative",
},
static: {
position: "static",
},
inset_0: {
top: 0,
left: 0,
right: 0,
bottom: 0,
},
top_0: {
top: 0,
},
right_0: {
right: 0,
},
bottom_0: {
bottom: 0,
},
left_0: {
left: 0,
},
z_10: {
zIndex: 10,
},
z_20: {
zIndex: 20,
},
z_30: {
zIndex: 30,
},
z_40: {
zIndex: 40,
},
z_50: {
zIndex: 50,
},
overflow_hidden: {
overflow: "hidden",
},
/*
* Width & Height
*/
w_full: {
width: "100%",
},
h_full: {
height: "100%",
},
max_w_full: {
maxWidth: "100%",
},
max_h_full: {
maxHeight: "100%",
},
/*
* Theme-independent bg colors
*/
bg_transparent: {
backgroundColor: "transparent",
},
/*
* Border radius
*/
rounded_0: {
borderRadius: 0,
},
rounded_2xs: {
borderRadius: tokens.borderRadius._2xs,
},
rounded_xs: {
borderRadius: tokens.borderRadius.xs,
},
rounded_sm: {
borderRadius: tokens.borderRadius.sm,
},
rounded_md: {
borderRadius: tokens.borderRadius.md,
},
rounded_lg: {
borderRadius: tokens.borderRadius.lg,
},
rounded_full: {
borderRadius: tokens.borderRadius.full,
},
/*
* Flex
*/
gap_0: {
gap: 0,
},
gap_2xs: {
gap: tokens.space._2xs,
},
gap_xs: {
gap: tokens.space.xs,
},
gap_sm: {
gap: tokens.space.sm,
},
gap_md: {
gap: tokens.space.md,
},
gap_lg: {
gap: tokens.space.lg,
},
gap_xl: {
gap: tokens.space.xl,
},
gap_2xl: {
gap: tokens.space._2xl,
},
gap_3xl: {
gap: tokens.space._3xl,
},
gap_4xl: {
gap: tokens.space._4xl,
},
gap_5xl: {
gap: tokens.space._5xl,
},
flex: {
display: "flex",
},
flex_col: {
flexDirection: "column",
},
flex_row: {
flexDirection: "row",
},
flex_col_reverse: {
flexDirection: "column-reverse",
},
flex_row_reverse: {
flexDirection: "row-reverse",
},
flex_wrap: {
flexWrap: "wrap",
},
flex_nowrap: {
flexWrap: "nowrap",
},
flex_0: {
flex: 0,
},
flex_1: {
flex: 1,
},
flex_grow: {
flexGrow: 1,
},
flex_grow_0: {
flexGrow: 0,
},
flex_shrink: {
flexShrink: 1,
},
flex_shrink_0: {
flexShrink: 0,
},
justify_start: {
justifyContent: "flex-start",
},
justify_center: {
justifyContent: "center",
},
justify_between: {
justifyContent: "space-between",
},
justify_around: {
justifyContent: "space-around",
},
justify_end: {
justifyContent: "flex-end",
},
align_center: {
alignItems: "center",
},
align_start: {
alignItems: "flex-start",
},
align_end: {
alignItems: "flex-end",
},
align_baseline: {
alignItems: "baseline",
},
align_stretch: {
alignItems: "stretch",
},
self_auto: {
alignSelf: "auto",
},
self_start: {
alignSelf: "flex-start",
},
self_end: {
alignSelf: "flex-end",
},
self_center: {
alignSelf: "center",
},
self_stretch: {
alignSelf: "stretch",
},
self_baseline: {
alignSelf: "baseline",
},
/*
* Text
*/
text_left: {
textAlign: "left",
},
text_center: {
textAlign: "center",
},
text_right: {
textAlign: "right",
},
text_2xs: {
fontSize: tokens.fontSize._2xs,
letterSpacing: tokens.TRACKING,
},
text_xs: {
fontSize: tokens.fontSize.xs,
letterSpacing: tokens.TRACKING,
},
text_sm: {
fontSize: tokens.fontSize.sm,
letterSpacing: tokens.TRACKING,
},
text_md: {
fontSize: tokens.fontSize.md,
letterSpacing: tokens.TRACKING,
},
text_lg: {
fontSize: tokens.fontSize.lg,
letterSpacing: tokens.TRACKING,
},
text_xl: {
fontSize: tokens.fontSize.xl,
letterSpacing: tokens.TRACKING,
},
text_2xl: {
fontSize: tokens.fontSize._2xl,
letterSpacing: tokens.TRACKING,
},
text_3xl: {
fontSize: tokens.fontSize._3xl,
letterSpacing: tokens.TRACKING,
},
text_4xl: {
fontSize: tokens.fontSize._4xl,
letterSpacing: tokens.TRACKING,
},
text_5xl: {
fontSize: tokens.fontSize._5xl,
letterSpacing: tokens.TRACKING,
},
leading_tight: {
lineHeight: 1.15,
},
leading_snug: {
lineHeight: 1.3,
},
leading_normal: {
lineHeight: 1.5,
},
tracking_normal: {
letterSpacing: tokens.TRACKING,
},
font_normal: {
fontWeight: tokens.fontWeight.normal,
},
font_bold: {
fontWeight: tokens.fontWeight.bold,
},
font_heavy: {
fontWeight: tokens.fontWeight.black,
},
italic: {
fontStyle: "italic",
},
/*
* Border
*/
border_0: {
borderWidth: 0,
},
border_t_0: {
borderTopWidth: 0,
},
border_b_0: {
borderBottomWidth: 0,
},
border_l_0: {
borderLeftWidth: 0,
},
border_r_0: {
borderRightWidth: 0,
},
border: {
borderWidth: StyleSheet.hairlineWidth,
},
border_t: {
borderTopWidth: StyleSheet.hairlineWidth,
},
border_b: {
borderBottomWidth: StyleSheet.hairlineWidth,
},
border_l: {
borderLeftWidth: StyleSheet.hairlineWidth,
},
border_r: {
borderRightWidth: StyleSheet.hairlineWidth,
},
border_transparent: {
borderColor: "transparent",
},
/*
* Shadow
*/
shadow_sm: {
shadowRadius: 8,
shadowOpacity: 0.1,
elevation: 8,
},
shadow_md: {
shadowRadius: 16,
shadowOpacity: 0.1,
elevation: 16,
},
shadow_lg: {
shadowRadius: 32,
shadowOpacity: 0.1,
elevation: 24,
},
shadow_elevated: {
shadowColor: "black",
shadowOpacity: 0.25,
shadowOffset: { width: 0, height: 3 },
shadowRadius: 3.84,
elevation: 5,
},
/*
* Padding
*/
p_0: {
padding: 0,
},
p_2xs: {
padding: tokens.space._2xs,
},
p_xs: {
padding: tokens.space.xs,
},
p_sm: {
padding: tokens.space.sm,
},
p_md: {
padding: tokens.space.md,
},
p_lg: {
padding: tokens.space.lg,
},
p_xl: {
padding: tokens.space.xl,
},
p_2xl: {
padding: tokens.space._2xl,
},
p_3xl: {
padding: tokens.space._3xl,
},
p_4xl: {
padding: tokens.space._4xl,
},
p_5xl: {
padding: tokens.space._5xl,
},
px_0: {
paddingLeft: 0,
paddingRight: 0,
},
px_2xs: {
paddingLeft: tokens.space._2xs,
paddingRight: tokens.space._2xs,
},
px_xs: {
paddingLeft: tokens.space.xs,
paddingRight: tokens.space.xs,
},
px_sm: {
paddingLeft: tokens.space.sm,
paddingRight: tokens.space.sm,
},
px_md: {
paddingLeft: tokens.space.md,
paddingRight: tokens.space.md,
},
px_lg: {
paddingLeft: tokens.space.lg,
paddingRight: tokens.space.lg,
},
px_xl: {
paddingLeft: tokens.space.xl,
paddingRight: tokens.space.xl,
},
px_2xl: {
paddingLeft: tokens.space._2xl,
paddingRight: tokens.space._2xl,
},
px_3xl: {
paddingLeft: tokens.space._3xl,
paddingRight: tokens.space._3xl,
},
px_4xl: {
paddingLeft: tokens.space._4xl,
paddingRight: tokens.space._4xl,
},
px_5xl: {
paddingLeft: tokens.space._5xl,
paddingRight: tokens.space._5xl,
},
py_0: {
paddingTop: 0,
paddingBottom: 0,
},
py_2xs: {
paddingTop: tokens.space._2xs,
paddingBottom: tokens.space._2xs,
},
py_xs: {
paddingTop: tokens.space.xs,
paddingBottom: tokens.space.xs,
},
py_sm: {
paddingTop: tokens.space.sm,
paddingBottom: tokens.space.sm,
},
py_md: {
paddingTop: tokens.space.md,
paddingBottom: tokens.space.md,
},
py_lg: {
paddingTop: tokens.space.lg,
paddingBottom: tokens.space.lg,
},
py_xl: {
paddingTop: tokens.space.xl,
paddingBottom: tokens.space.xl,
},
py_2xl: {
paddingTop: tokens.space._2xl,
paddingBottom: tokens.space._2xl,
},
py_3xl: {
paddingTop: tokens.space._3xl,
paddingBottom: tokens.space._3xl,
},
py_4xl: {
paddingTop: tokens.space._4xl,
paddingBottom: tokens.space._4xl,
},
py_5xl: {
paddingTop: tokens.space._5xl,
paddingBottom: tokens.space._5xl,
},
pt_0: {
paddingTop: 0,
},
pt_2xs: {
paddingTop: tokens.space._2xs,
},
pt_xs: {
paddingTop: tokens.space.xs,
},
pt_sm: {
paddingTop: tokens.space.sm,
},
pt_md: {
paddingTop: tokens.space.md,
},
pt_lg: {
paddingTop: tokens.space.lg,
},
pt_xl: {
paddingTop: tokens.space.xl,
},
pt_2xl: {
paddingTop: tokens.space._2xl,
},
pt_3xl: {
paddingTop: tokens.space._3xl,
},
pt_4xl: {
paddingTop: tokens.space._4xl,
},
pt_5xl: {
paddingTop: tokens.space._5xl,
},
pb_0: {
paddingBottom: 0,
},
pb_2xs: {
paddingBottom: tokens.space._2xs,
},
pb_xs: {
paddingBottom: tokens.space.xs,
},
pb_sm: {
paddingBottom: tokens.space.sm,
},
pb_md: {
paddingBottom: tokens.space.md,
},
pb_lg: {
paddingBottom: tokens.space.lg,
},
pb_xl: {
paddingBottom: tokens.space.xl,
},
pb_2xl: {
paddingBottom: tokens.space._2xl,
},
pb_3xl: {
paddingBottom: tokens.space._3xl,
},
pb_4xl: {
paddingBottom: tokens.space._4xl,
},
pb_5xl: {
paddingBottom: tokens.space._5xl,
},
pl_0: {
paddingLeft: 0,
},
pl_2xs: {
paddingLeft: tokens.space._2xs,
},
pl_xs: {
paddingLeft: tokens.space.xs,
},
pl_sm: {
paddingLeft: tokens.space.sm,
},
pl_md: {
paddingLeft: tokens.space.md,
},
pl_lg: {
paddingLeft: tokens.space.lg,
},
pl_xl: {
paddingLeft: tokens.space.xl,
},
pl_2xl: {
paddingLeft: tokens.space._2xl,
},
pl_3xl: {
paddingLeft: tokens.space._3xl,
},
pl_4xl: {
paddingLeft: tokens.space._4xl,
},
pl_5xl: {
paddingLeft: tokens.space._5xl,
},
pr_0: {
paddingRight: 0,
},
pr_2xs: {
paddingRight: tokens.space._2xs,
},
pr_xs: {
paddingRight: tokens.space.xs,
},
pr_sm: {
paddingRight: tokens.space.sm,
},
pr_md: {
paddingRight: tokens.space.md,
},
pr_lg: {
paddingRight: tokens.space.lg,
},
pr_xl: {
paddingRight: tokens.space.xl,
},
pr_2xl: {
paddingRight: tokens.space._2xl,
},
pr_3xl: {
paddingRight: tokens.space._3xl,
},
pr_4xl: {
paddingRight: tokens.space._4xl,
},
pr_5xl: {
paddingRight: tokens.space._5xl,
},
/*
* Margin
*/
m_0: {
margin: 0,
},
m_2xs: {
margin: tokens.space._2xs,
},
m_xs: {
margin: tokens.space.xs,
},
m_sm: {
margin: tokens.space.sm,
},
m_md: {
margin: tokens.space.md,
},
m_lg: {
margin: tokens.space.lg,
},
m_xl: {
margin: tokens.space.xl,
},
m_2xl: {
margin: tokens.space._2xl,
},
m_3xl: {
margin: tokens.space._3xl,
},
m_4xl: {
margin: tokens.space._4xl,
},
m_5xl: {
margin: tokens.space._5xl,
},
m_auto: {
margin: "auto",
},
mx_0: {
marginLeft: 0,
marginRight: 0,
},
mx_2xs: {
marginLeft: tokens.space._2xs,
marginRight: tokens.space._2xs,
},
mx_xs: {
marginLeft: tokens.space.xs,
marginRight: tokens.space.xs,
},
mx_sm: {
marginLeft: tokens.space.sm,
marginRight: tokens.space.sm,
},
mx_md: {
marginLeft: tokens.space.md,
marginRight: tokens.space.md,
},
mx_lg: {
marginLeft: tokens.space.lg,
marginRight: tokens.space.lg,
},
mx_xl: {
marginLeft: tokens.space.xl,
marginRight: tokens.space.xl,
},
mx_2xl: {
marginLeft: tokens.space._2xl,
marginRight: tokens.space._2xl,
},
mx_3xl: {
marginLeft: tokens.space._3xl,
marginRight: tokens.space._3xl,
},
mx_4xl: {
marginLeft: tokens.space._4xl,
marginRight: tokens.space._4xl,
},
mx_5xl: {
marginLeft: tokens.space._5xl,
marginRight: tokens.space._5xl,
},
mx_auto: {
marginLeft: "auto",
marginRight: "auto",
},
my_0: {
marginTop: 0,
marginBottom: 0,
},
my_2xs: {
marginTop: tokens.space._2xs,
marginBottom: tokens.space._2xs,
},
my_xs: {
marginTop: tokens.space.xs,
marginBottom: tokens.space.xs,
},
my_sm: {
marginTop: tokens.space.sm,
marginBottom: tokens.space.sm,
},
my_md: {
marginTop: tokens.space.md,
marginBottom: tokens.space.md,
},
my_lg: {
marginTop: tokens.space.lg,
marginBottom: tokens.space.lg,
},
my_xl: {
marginTop: tokens.space.xl,
marginBottom: tokens.space.xl,
},
my_2xl: {
marginTop: tokens.space._2xl,
marginBottom: tokens.space._2xl,
},
my_3xl: {
marginTop: tokens.space._3xl,
marginBottom: tokens.space._3xl,
},
my_4xl: {
marginTop: tokens.space._4xl,
marginBottom: tokens.space._4xl,
},
my_5xl: {
marginTop: tokens.space._5xl,
marginBottom: tokens.space._5xl,
},
my_auto: {
marginTop: "auto",
marginBottom: "auto",
},
mt_0: {
marginTop: 0,
},
mt_2xs: {
marginTop: tokens.space._2xs,
},
mt_xs: {
marginTop: tokens.space.xs,
},
mt_sm: {
marginTop: tokens.space.sm,
},
mt_md: {
marginTop: tokens.space.md,
},
mt_lg: {
marginTop: tokens.space.lg,
},
mt_xl: {
marginTop: tokens.space.xl,
},
mt_2xl: {
marginTop: tokens.space._2xl,
},
mt_3xl: {
marginTop: tokens.space._3xl,
},
mt_4xl: {
marginTop: tokens.space._4xl,
},
mt_5xl: {
marginTop: tokens.space._5xl,
},
mt_auto: {
marginTop: "auto",
},
mb_0: {
marginBottom: 0,
},
mb_2xs: {
marginBottom: tokens.space._2xs,
},
mb_xs: {
marginBottom: tokens.space.xs,
},
mb_sm: {
marginBottom: tokens.space.sm,
},
mb_md: {
marginBottom: tokens.space.md,
},
mb_lg: {
marginBottom: tokens.space.lg,
},
mb_xl: {
marginBottom: tokens.space.xl,
},
mb_2xl: {
marginBottom: tokens.space._2xl,
},
mb_3xl: {
marginBottom: tokens.space._3xl,
},
mb_4xl: {
marginBottom: tokens.space._4xl,
},
mb_5xl: {
marginBottom: tokens.space._5xl,
},
mb_auto: {
marginBottom: "auto",
},
ml_0: {
marginLeft: 0,
},
ml_2xs: {
marginLeft: tokens.space._2xs,
},
ml_xs: {
marginLeft: tokens.space.xs,
},
ml_sm: {
marginLeft: tokens.space.sm,
},
ml_md: {
marginLeft: tokens.space.md,
},
ml_lg: {
marginLeft: tokens.space.lg,
},
ml_xl: {
marginLeft: tokens.space.xl,
},
ml_2xl: {
marginLeft: tokens.space._2xl,
},
ml_3xl: {
marginLeft: tokens.space._3xl,
},
ml_4xl: {
marginLeft: tokens.space._4xl,
},
ml_5xl: {
marginLeft: tokens.space._5xl,
},
ml_auto: {
marginLeft: "auto",
},
mr_0: {
marginRight: 0,
},
mr_2xs: {
marginRight: tokens.space._2xs,
},
mr_xs: {
marginRight: tokens.space.xs,
},
mr_sm: {
marginRight: tokens.space.sm,
},
mr_md: {
marginRight: tokens.space.md,
},
mr_lg: {
marginRight: tokens.space.lg,
},
mr_xl: {
marginRight: tokens.space.xl,
},
mr_2xl: {
marginRight: tokens.space._2xl,
},
mr_3xl: {
marginRight: tokens.space._3xl,
},
mr_4xl: {
marginRight: tokens.space._4xl,
},
mr_5xl: {
marginRight: tokens.space._5xl,
},
mr_auto: {
marginRight: "auto",
},
/*
* Pointer events & user select
*/
pointer_events_none: {
pointerEvents: "none",
},
pointer_events_auto: {
pointerEvents: "auto",
},
user_select_none: {
userSelect: "none",
},
user_select_text: {
userSelect: "text",
},
user_select_all: {
userSelect: "all",
},
outline_inset_1: {
outlineOffset: -1,
},
/*
* Text decoration
*/
underline: {
textDecorationLine: "underline",
},
strike_through: {
textDecorationLine: "line-through",
},
/*
* Display
*/
hidden: {
display: "none",
},
} as const;
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 Box from "@components/bases/Box";
import { images } from "@styles/Commons";
import { Image } from "react-native"; import { Image } from "react-native";
const BeasyLogoIcon = () => { const BeasyLogoIcon = () => {
return ( return (
<Box width={100} height={30}> <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> </Box>
); );
}; };
......
import type { BoxProps, VariantProps } from "@shopify/restyle"; import { asp as g } from "@asp/asp";
import type { Theme } from "@themes/Theme"; import type { FC } from "react";
import { ActivityIndicator, TouchableOpacity } from "react-native"; import {
import ButtonBase from "./bases/ButtonBase"; ActivityIndicator,
import Text from "./bases/Text"; Text,
type TextProps,
TouchableOpacity,
type TouchableOpacityProps,
} from "react-native";
type Props = BoxProps<Theme> & const DEFAULT_HEIGHT = 50;
VariantProps<Theme, "buttonVariants"> &
VariantProps<Theme, "textVariants", "textVariants"> & { type ContainerProps = TouchableOpacityProps & {
label: string;
onPress: () => void;
isLoading?: boolean; isLoading?: boolean;
}; };
const Button = ({ onPress, label, isLoading, textVariants, variant, ...rest }: Props) => { export const Container: FC<ContainerProps> = ({ children, style, isLoading, onPress, ...rest }) => {
return ( return (
<TouchableOpacity onPress={onPress}> <TouchableOpacity
<ButtonBase onPress={onPress}
variant={variant} style={[
justifyContent="center" g.p_md,
alignItems="center" g.rounded_xs,
flexDirection={"row"} g.align_center,
g.justify_center,
{ height: DEFAULT_HEIGHT, backgroundColor: "#03875b" },
style,
]}
{...rest} {...rest}
gap={"m"}
> >
{isLoading ? ( {isLoading ? <ActivityIndicator /> : children}
<ActivityIndicator color="white" />
) : (
<Text variant={textVariants}>{label}</Text>
)}
</ButtonBase>
</TouchableOpacity> </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 { asp as g } from "@asp/asp";
import type { Theme } from "@themes/Theme"; import type { FC } from "react";
import type { TextInputProps } from "react-native"; import {
import { TextInput } from "react-native"; Text,
import Box from "./bases/Box"; 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 ( return (
<Box> <View style={[g.gap_md, style]} {...rest}>
<Box backgroundColor={"lightGray"} height={50} borderRadius={10} my={"m"} p={"s"}> {children}
<TextInput style={{ height: "100%", width: "100%" }} {...rest} /> </View>
</Box>
</Box>
); );
}; };
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 Box from "@components/bases/Box";
import { images } from "@styles/Commons";
import { Image } from "react-native"; import { Image } from "react-native";
const CheckIcon = () => { const CheckIcon = () => {
...@@ -7,7 +7,8 @@ const CheckIcon = () => { ...@@ -7,7 +7,8 @@ const CheckIcon = () => {
<Box width={50} height={50}> <Box width={50} height={50}>
<Image <Image
source={require("../../../assets/icon_check_success.png")} source={require("../../../assets/icon_check_success.png")}
style={images.contain} style={[g.flex_1]}
resizeMode="contain"
/> />
</Box> </Box>
); );
......
import { asp as g } from "@asp/asp";
import Box from "@components/bases/Box"; import Box from "@components/bases/Box";
import { images } from "@styles/Commons";
import { Image } from "react-native"; import { Image } from "react-native";
const ErrorIcon = () => { const ErrorIcon = () => {
...@@ -7,7 +7,8 @@ const ErrorIcon = () => { ...@@ -7,7 +7,8 @@ const ErrorIcon = () => {
<Box width={50} height={50}> <Box width={50} height={50}>
<Image <Image
source={require("../../../assets/icon_close_failure.png")} source={require("../../../assets/icon_close_failure.png")}
style={images.contain} style={[g.flex_1]}
resizeMode="contain"
/> />
</Box> </Box>
); );
......
import { asp as g } from "@asp/asp";
import Box from "@components/bases/Box"; import Box from "@components/bases/Box";
import { images } from "@styles/Commons";
import { Image } from "react-native"; import { Image } from "react-native";
const InformationIcon = () => { const InformationIcon = () => {
...@@ -7,7 +7,8 @@ const InformationIcon = () => { ...@@ -7,7 +7,8 @@ const InformationIcon = () => {
<Box width={50} height={50}> <Box width={50} height={50}>
<Image <Image
source={require("../../../assets/icon_alert_information.png")} source={require("../../../assets/icon_alert_information.png")}
style={images.contain} style={[g.flex_1]}
resizeMode="contain"
/> />
</Box> </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"; ...@@ -3,10 +3,9 @@ import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import TransactionHistoryScreen from "@screens/TransactionHistoryScreen"; import TransactionHistoryScreen from "@screens/TransactionHistoryScreen";
import UserProfileScreen from "@screens/UserProfileScreen"; import UserProfileScreen from "@screens/UserProfileScreen";
import { useTheme } from "@shopify/restyle"; import { useTheme } from "@shopify/restyle";
// import palette
import type { Theme } from "@themes/Theme"; import type { Theme } from "@themes/Theme";
import { View } from "react-native"; import { Text, View } from "react-native";
import Text from "../components/bases/Text";
import PaymentStackNavigator from "./PaymentStackNavigation"; import PaymentStackNavigator from "./PaymentStackNavigation";
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
...@@ -63,22 +62,6 @@ export const AppBottomTabsNavigator = () => { ...@@ -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 = () => { const SettingsScreen = () => {
return ( return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
...@@ -86,11 +69,3 @@ const SettingsScreen = () => { ...@@ -86,11 +69,3 @@ const SettingsScreen = () => {
</View> </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 { LOG } from "@logger";
import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomeUserNotLoggedIn from "@screens/HomeUserNotLoggedIn"; import HomeUserNotLoggedIn from "@screens/HomeUserNotLoggedIn";
...@@ -6,8 +5,10 @@ import PaymentResultScreen from "@screens/PaymentResultScreen"; ...@@ -6,8 +5,10 @@ import PaymentResultScreen from "@screens/PaymentResultScreen";
import UserLoginScreen from "@screens/UserLoginScreen"; import UserLoginScreen from "@screens/UserLoginScreen";
import WaveQrCodePaymentScreen from "@screens/WaveQrCodePaymentScreen"; import WaveQrCodePaymentScreen from "@screens/WaveQrCodePaymentScreen";
import { memo } from "react"; import { memo } from "react";
import { useSelector } from "react-redux";
import type { RootState } from "@/redux";
import { AppBottomTabsNavigator } from "./AppBottomTabsNavigator"; import { AppBottomTabsNavigator } from "./AppBottomTabsNavigator";
import type { ImainStackNavigator } from "./Types"; import type { ImainStackNavigator } from "./types";
const Stack = createNativeStackNavigator<ImainStackNavigator>(); const Stack = createNativeStackNavigator<ImainStackNavigator>();
...@@ -50,6 +51,6 @@ const AppMainStackNavigator: React.FC<IappMainStackNavigatorProps> = ({ isAuthen ...@@ -50,6 +51,6 @@ const AppMainStackNavigator: React.FC<IappMainStackNavigatorProps> = ({ isAuthen
export default memo(AppMainStackNavigator); export default memo(AppMainStackNavigator);
export const AppMainStackNavigatorAuthWrapper = () => { export const AppMainStackNavigatorAuthWrapper = () => {
const { isAuthenticated } = useUserAuthenticationContext(); const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated);
return <AppMainStackNavigator isAuthenticated={isAuthenticated} />; return <AppMainStackNavigator isAuthenticated={isAuthenticated} />;
}; };
...@@ -2,7 +2,7 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack"; ...@@ -2,7 +2,7 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomePageWithPaymentOptions from "@screens/HomePageWithPaymentOptions"; import HomePageWithPaymentOptions from "@screens/HomePageWithPaymentOptions";
import NumberAndOtpForPaymentScreen from "@screens/NumberAndOtpForPaymentScreen"; import NumberAndOtpForPaymentScreen from "@screens/NumberAndOtpForPaymentScreen";
import PaymentAmountInputScreen from "@screens/PaymentAmountInputScreen"; import PaymentAmountInputScreen from "@screens/PaymentAmountInputScreen";
import type { IpaymentStackNavigator } from "./Types"; import type { IpaymentStackNavigator } from "./types";
const Stack = createNativeStackNavigator<IpaymentStackNavigator>(); 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 { NativeStackScreenProps } from "@react-navigation/native-stack";
import type { PaymentType, WaveTransactionInitilizationResponse } from "@/features/pay/types";
export type IpaymentStackNavigator = { export type IpaymentStackNavigator = {
homePageWithPaymentOptions: undefined; homePageWithPaymentOptions: undefined;
numberAndOtpForPaymentScreen: undefined; numberAndOtpForPaymentScreen: {
paymentType: PaymentType;
amount: number;
};
paymentAmountInputScreen: { paymentAmountInputScreen: {
paymentType: PaymentCode; paymentType: PaymentType;
}; };
}; };
...@@ -23,7 +25,7 @@ export type ImainStackNavigator = { ...@@ -23,7 +25,7 @@ export type ImainStackNavigator = {
appBottomTabsNavigator: undefined; appBottomTabsNavigator: undefined;
paymentResultScreen: undefined; paymentResultScreen: undefined;
waveQrCodePaymentScreen: { 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 { asp as g } from "@asp/asp";
import getPaymentTypes from "@/utils/requests/getPaymentTypes"; import { Balance } from "@components/Balance";
import BalanceContainer from "@components/BalanceContainer"; import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BarWithBeasyAndNotificationsIcon from "@components/BarWithBeasyAndNotificationsIcon"; import BeasyLogoIcon from "@components/BeasyLogoIcon";
import PaymentOption from "@components/PaymentOption"; import Ionicons from "@expo/vector-icons/Ionicons";
import Box from "@components/bases/Box";
import WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight from "@components/wrappers/WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight";
import { LOG } from "@logger"; import { LOG } from "@logger";
import Card from "@re-card";
import Text from "@re-text";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react"; import { Text, View } from "react-native";
import { Dimensions } 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 log = LOG.extend("HomePageWithPaymentOptions");
const HomePageWithPaymentOptions: PaymentStackScreenComponentProps<"homePageWithPaymentOptions"> = const HomePageWithPaymentOptions: PaymentStackScreenComponentProps<
({ navigation }) => { "homePageWithPaymentOptions"
> = ({ navigation }) => {
log.debug("HomePageWithPaymentOptions"); log.debug("HomePageWithPaymentOptions");
const { data, isLoading, error } = useQuery({ const paymentTypesQuery = useQuery({
queryKey: ["paymentTypes"], queryKey: ["paymentTypes"],
queryFn: getPaymentTypes, queryFn: getPaymentTypes,
enabled: true,
}); });
// getting valid payments supported const paymentTypes = paymentTypesQuery.isSuccess
const paymentTypesWithActiveStatus = useMemo(() => { ? Object.keys(paymentTypesQuery.data.data.results)
log.info("Filtering payment types"); .map((key) => paymentTypesQuery.data.data.results[key])
const paymentTypes = data?.results || []; .filter((item) => item.etat === true)
return paymentTypes.filter((paymentType) => paymentType.etat === true); : [];
}, [data]);
log.info(
"payment types to render",
paymentTypesWithActiveStatus.map((paymentType) => paymentType.code),
);
return ( return (
<WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight> <BarnoinPayBackground style={[g.flex_col, g.relative]}>
<Box style={{ height: "100%" }} flexDirection={"column"}> <View style={[g.px_lg, g.flex_row, g.justify_between]}>
<BarWithBeasyAndNotificationsIcon /> <BeasyLogoIcon />
<Card <Ionicons name="notifications" size={24} color="black" />
variant={"curvedTopContainer"} </View>
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>
<Box <View
flex={1} style={[
flexDirection={"row"} g.absolute,
justifyContent={"space-between"} g.z_10,
flexWrap={"wrap"} g.align_center,
rowGap={"m"} g.justify_center,
g.w_full,
{ top: 120 },
]}
> >
{isLoading && ( <Balance amount={0} />
<Box flex={1}> </View>
<Text textAlign={"center"}>
Chargement des méthodes de paiement... <View
</Text> style={[
</Box> g.px_lg,
)} g.pb_lg,
{!isLoading && g.gap_lg,
!error &&
paymentTypesWithActiveStatus.map((paymentType) => (
<PaymentOptionContainer key={paymentType.id}>
<PaymentOption
// key={paymentType.id}
onPress={() =>
navigation.navigate(
"paymentAmountInputScreen",
{ {
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>
); );
}; })}
</View>
export default HomePageWithPaymentOptions; </View>
</BarnoinPayBackground>
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>
); );
}; };
export default HomePageWithPaymentOptions;
import type { MainStackScreenComponentProps } from "@/navigations/Types"; import { asp as g } from "@asp/asp";
import Button from "@components/Button"; import * as Button from "@components/Button";
import Box from "@components/bases/Box"; import { Image } from "expo-image";
import Text from "@components/bases/Text"; import { Text, View } from "react-native";
import { Image, StyleSheet } from "react-native"; import type { MainStackScreenComponentProps } from "@/navigations/types";
const HomeUserNotLoggedIn: MainStackScreenComponentProps<"homeUserNotLoggedIn"> = ({ const HomeUserNotLoggedIn: MainStackScreenComponentProps<"homeUserNotLoggedIn"> = ({
navigation, navigation,
}) => { }) => {
return ( return (
<> <View style={[g.flex_1, g.p_5xl, g.gap_lg]}>
<Box style={style.container} p={"xl"} backgroundColor={"white"}> <View style={[{ height: 400 }]}>
<Box mt={"s"}>
<Box
height={300}
width={300}
backgroundColor={"primary"}
mb={"l"}
alignItems={"center"}
justifyContent={"center"}
>
<Image <Image
source={require("../../assets/payment_processing.png")} source={require("@assets/payment_processing.png")}
style={style.image} style={{ width: "100%", height: "100%" }}
contentFit="contain"
cachePolicy={"memory-disk"}
/> />
</Box> </View>
<Text color={"gray"} fontSize={16} textAlign={"center"}> <Text style={[g.text_center, { color: "gray" }]}>
Acceptez des paiements en magasin et en ligne et recevez votre argent en Acceptez des paiements en magasin et en ligne et recevez votre argent en quelques
quelques secondes secondes
</Text> </Text>
<Box mt={"xl"}>
<Button <Button.Container
variant={"full"}
textVariants={"primary"}
label="Se connecter"
onPress={() => navigation.navigate("userLoginScreen")} onPress={() => navigation.navigate("userLoginScreen")}
/> style={[g.mt_5xl]}
<Button >
variant={"clean"} <Button.Label style={{ color: "white" }}>Se connecter</Button.Label>
textVariants={"secondary"} </Button.Container>
label="Creer un compte" <Button.Container style={[{ backgroundColor: "transparent" }]}>
onPress={() => {}} <Button.Label style={{ color: "green" }}>Créer un compte</Button.Label>
/> </Button.Container>
</Box> </View>
</Box>
</Box>
</>
); );
}; };
const style = StyleSheet.create({
container: {
width: "100%",
height: "100%",
flex: 1,
alignItems: "center",
justifyContent: "center",
},
image: {
width: "100%",
height: "100%",
},
});
export default HomeUserNotLoggedIn; 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 BeasyLogoIcon from "@components/BeasyLogoIcon";
import Button from "@components/Button"; import * as Button from "@components/Button";
import GoBackIconButton from "@components/GoBackIconButton"; import * as Input from "@components/Input";
import InputWithTopLabel from "@components/InputWithTopLabel"; import AntDesign from "@expo/vector-icons/AntDesign";
import PaymentOption from "@components/PaymentOption";
import BackgroundGreenWhiteContentArea from "@components/backgrounds/BackgroundGreenWhiteContentArea";
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import { LOG } from "@logger"; 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< const NumberAndOtpForPaymentScreen: PaymentStackScreenComponentProps<
"numberAndOtpForPaymentScreen" "numberAndOtpForPaymentScreen"
> = ({ navigation }) => { > = ({ navigation, route }) => {
console.debug("NumberAndOtpForPaymentScreen");
return ( return (
<BackgroundGreenWhiteContentArea> <BarnoinPayBackground style={[g.flex_col, g.gap_lg, g.relative]}>
<SafeAreaView> <View style={[g.px_lg, g.flex_row, g.align_center, g.justify_between]}>
<Box style={{ height: "100%", width: "100%" }}>
<Box
px={"l"}
flexDirection={"row"}
justifyContent={"space-between"}
alignItems={"center"}
>
<BeasyLogoIcon /> <BeasyLogoIcon />
<GoBackIconButton onPress={() => navigation.goBack()} /> <AntDesign
</Box> name="arrowleft"
<Box height={122} alignItems={"center"} justifyContent={"center"}> size={24}
<Text color={"white"}>Montant à payer</Text> color="black"
<Text color={"white"} fontSize={30}> onPress={() => navigation.goBack()}
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"}
/> />
</Box> </View>
<Button
onPress={() => {}} <View style={[g.px_lg, g.align_center, g.justify_center]}>
variant={"full"} <Text style={[g.font_bold, g.text_2xl, { color: "white" }]}>Montant à payé </Text>
textVariants={"primary"} <Text style={[g.font_bold, g.text_2xl, { color: "white" }]}>
label="Confirmer" {route.params.amount}
/> </Text>
</Box> </View>
</Box>
</SafeAreaView> <View
</BackgroundGreenWhiteContentArea> 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 { useModalsManagerContext } from "@/contexts/ModalsManagerContext"; import { asp as g } from "@asp/asp";
import type { PaymentStackScreenComponentProps } from "@/navigations/Types"; import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BeasyLogoIcon from "@components/BeasyLogoIcon"; import BeasyLogoIcon from "@components/BeasyLogoIcon";
import Button from "@components/Button"; import * as Button from "@components/Button";
import GoBackIconButton from "@components/GoBackIconButton"; import * as Input from "@components/Input";
import InputWithTopLabel from "@components/InputWithTopLabel"; import * as Modal from "@components/Modal";
import PaymentOption from "@components/PaymentOption";
import BeasyDefaultBackgroundWrapper from "@components/backgrounds/BeasyDefaultBackground";
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import useOrangeMoney from "@hooks/useOrangeMoney";
import useWave from "@hooks/useWave";
import { LOG } from "@logger"; import { LOG } from "@logger";
import { useCallback, useState } from "react"; import { useMutation } from "@tanstack/react-query";
import { Keyboard, View } from "react-native"; import type { AxiosError } from "axios";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import * as WebBrowser from "expo-web-browser";
import { useEffect, useRef, useState } from "react";
import { AppState, Text, View } from "react-native";
import {
omInitializeTransaction,
omVerifyTransactionStateWithTimeout,
waveInitializeTransaction,
} from "@/features/pay/api";
import PaymentType from "@/features/pay/components/PaymentType";
import type { PaymentStackScreenComponentProps } from "@/navigations/types";
const log = LOG.extend("PaymentAmountInputScreen"); const log = LOG.extend("PaymentAmountInputScreen");
const PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountInputScreen"> = ({ const PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountInputScreen"> = ({
...@@ -21,158 +24,171 @@ const PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountI ...@@ -21,158 +24,171 @@ const PaymentAmountInputScreen: PaymentStackScreenComponentProps<"paymentAmountI
navigation, navigation,
}) => { }) => {
log.debug("PaymentAmountInputScreen"); log.debug("PaymentAmountInputScreen");
const { paymentType } = route.params;
const [amountToPay, setAmountToPay] = useState(0);
const { showModal, closeModal } = useModalsManagerContext();
const {
orangeTransactionInitializerMutation,
handlePaymentUsingBrowser,
isBrowserOpen,
isWaitingForOmPaymentUrl,
isCheckingForTransactionStatus,
transactionsStatusMutation,
orangePaymentTransactionHandler,
} = useOrangeMoney(navigation);
const { waveTransactionHandler } = useWave(navigation);
const insets = useSafeAreaInsets();
log.debug({
isWaitingForOmPaymentUrl,
isCheckingForTransactionStatus,
isBrowserOpen,
});
const updateAmountToPay = (amount: string) => {
const amountParsed = Number.parseInt(amount);
if (!Number.isNaN(amountParsed)) {
return setAmountToPay(amountParsed);
}
return setAmountToPay(0); const [amount, setAmount] = useState(0);
}; const [error, setError] = useState("");
const appState = useRef(AppState.currentState);
const handlePaymentButton = useCallback(async () => { const handlePayment = async () => {
switch (paymentType) {
case "OM": {
Keyboard.dismiss();
log.info("OM so we stays on screen !!");
// await orangePaymentSequence();
try { try {
await orangePaymentTransactionHandler(amountToPay); const code = route.params.paymentType.code;
// navigation.getParent()?.navigate("paymentResultScreen"); if (code === "OM") {
} catch (error) { // TODO: ASK THE BOSS WHY THE PAYLOAD IS MOSTLY USELESS HERE.
log.error("handlePaymentButton |", error); const res = await omInitializeTransaction({
} type_paiement: 1,
marchand: "1",
break; service: "1",
montant: amount,
commentaire: "un commentaire",
numero: "0707070707",
});
WebBrowser.openBrowserAsync(res.data.payment_url);
} else if (code === "WAVE") {
const res = await waveInitializeTransaction({
type_paiement: 2,
marchand: "1",
service: "1",
montant: amount,
});
navigation?.getParent()?.navigate("waveQrCodePaymentScreen", {
data: res,
});
} else {
navigation.navigate("numberAndOtpForPaymentScreen", {
paymentType: route.params.paymentType,
amount: amount,
});
} }
case "WAVE": {
try {
log.info("Wave so we stay on screen.");
await waveTransactionHandler(amountToPay);
} catch (error) { } catch (error) {
log.error("handlePaymentButton Wave|", error); const err = error as AxiosError;
} setError(JSON.stringify(err.response?.data) || err.message);
break;
} }
default: };
log.info("Navigating to numberAndOtpForPaymentScreen");
navigation.navigate("numberAndOtpForPaymentScreen"); const omTransactionVerification = useMutation({
break; mutationFn: () => omVerifyTransactionStateWithTimeout("1", 10000, 2000),
onSuccess: (_data) => {
navigation?.getParent()?.navigate("paymentResultScreen");
},
onError: (err: AxiosError) => {
setError(JSON.stringify(err.response?.data) || err.message);
},
});
useEffect(() => {
if (route.params.paymentType.code !== "OM") return; // only for orange money payment should this effect be run
const subscription = AppState.addEventListener("change", (nextAppState) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active" &&
route.params.paymentType.code === "OM"
) {
console.log("App has come to the foreground!");
omTransactionVerification.mutate();
} }
}, [
paymentType, appState.current = nextAppState;
navigation,
amountToPay, console.log("AppState", appState.current);
orangePaymentTransactionHandler, });
waveTransactionHandler,
]); return () => {
subscription.remove();
};
}, [route, omTransactionVerification]);
// switch (paymentType) {
// case "OM": {
// Keyboard.dismiss();
// log.info("OM so we stays on screen !!");
// // await orangePaymentSequence();
// try {
// await orangePaymentTransactionHandler(amountToPay);
// // navigation.getParent()?.navigate("paymentResultScreen");
// } catch (error) {
// log.error("handlePaymentButton |", error);
// }
// break;
// }
// case "WAVE": {
// try {
// log.info("Wave so we stay on screen.");
// await waveTransactionHandler(amountToPay);
// } catch (error) {
// log.error("handlePaymentButton Wave|", error);
// }
// break;
// }
// default:
// log.info("Navigating to numberAndOtpForPaymentScreen");
// navigation.navigate("numberAndOtpForPaymentScreen");
// break;
// }
// }, [
// paymentType,
// navigation,
// amountToPay,
// orangePaymentTransactionHandler,
// waveTransactionHandler,
// ]);
return ( return (
<BeasyDefaultBackgroundWrapper> <BarnoinPayBackground style={[g.flex_col, g.gap_lg, g.relative]}>
{/* <SafeAreaView> */} <View style={[g.px_lg]}>
{transactionsStatusMutation.isPending && <LoadingScreen />}
<Box
style={{
height: "100%",
width: "100%",
// marginTop: insets.top,
}}
>
<Box
px={"l"}
flexDirection={"row"}
justifyContent={"space-between"}
alignItems={"center"}
mb={"m"}
>
<BeasyLogoIcon /> <BeasyLogoIcon />
<GoBackIconButton onPress={() => navigation.goBack()} /> </View>
</Box>
{/* <Box height={150} alignItems={"center"} justifyContent={"center"}>
<BalanceContainer balance={78000} label="Total des ventes" />
</Box> */}
<Box height={90} padding={"s"} paddingLeft={"l"}>
<Text color={"black"}>Montant à payé</Text>
<Text color={"black"} variant={"header"}>
{amountToPay}
</Text>
</Box>
<Box
p={"l"}
paddingTop={"xl"}
backgroundColor={"white"}
flex={1}
borderTopLeftRadius={20}
borderTopRightRadius={20}
>
<Box width={75} height={50} mb={"l"} borderRadius={10} overflow={"hidden"}>
<PaymentOption onPress={() => {}} paymentMethod={paymentType} />
</Box>
<Box mb={"xl"}>
<InputWithTopLabel
label="Entrez le montant"
autoFocus={true}
keyboardType="numeric"
onChangeText={(e) => updateAmountToPay(e)}
/>
</Box>
<Button
onPress={handlePaymentButton}
variant={"full"}
textVariants={"primary"}
label={`${isWaitingForOmPaymentUrl ? "Chargement..." : "Payer"} `}
/>
</Box>
</Box>
{/* </SafeAreaView> */}
</BeasyDefaultBackgroundWrapper>
);
};
export default PaymentAmountInputScreen; <View style={[g.px_lg]}>
<Text style={[g.font_bold, g.text_2xl]}>Montant à payé </Text>
<Text style={[g.font_bold, g.text_2xl]}>{amount}</Text>
</View>
const LoadingScreen = () => {
return (
<View <View
style={{ style={[
flex: 1, g.flex_1,
width: "100%", g.p_5xl,
height: "100%", g.gap_5xl,
justifyContent: "center", { backgroundColor: "white", borderTopLeftRadius: 20, borderTopRightRadius: 20 },
alignItems: "center", ]}
zIndex: 1000000000000,
backgroundColor: "black",
opacity: 0.5,
position: "absolute",
}}
> >
<Text color={"primary"}>Verification du status de la transaction.</Text> <PaymentType type={route.params.paymentType.code} />
<Text>Veuillez patienter</Text> <Input.Container>
<Input.Header>Entrer le montant</Input.Header>
<Input.FieldContainer>
<Input.Field
keyboardType="number-pad"
value={amount === 0 ? "" : amount.toString()}
onChangeText={(v) => setAmount(Number(v))}
/>
</Input.FieldContainer>
</Input.Container>
<Button.Container onPress={handlePayment}>
<Button.Label>Payer</Button.Label>
</Button.Container>
</View> </View>
<Modal.RnModal visible={!!error} style={{ backgroundColor: "rgba(0, 0, 0, 0.25)" }}>
<Modal.OuterView style={[g.p_md, g.shadow_elevated, { backgroundColor: "white" }]}>
<Text>{error}</Text>
<Button.Container onPress={() => setError("")}>
<Button.Label>OK</Button.Label>
</Button.Container>
</Modal.OuterView>
</Modal.RnModal>
<Modal.RnModal
visible={omTransactionVerification.isPending}
style={{ backgroundColor: "rgba(0, 0, 0, 0.25)" }}
>
<Modal.OuterView style={[g.p_md, g.shadow_elevated, { backgroundColor: "white" }]}>
<Text>Loading...</Text>
</Modal.OuterView>
</Modal.RnModal>
</BarnoinPayBackground>
); );
}; };
export default PaymentAmountInputScreen;
import type { MainStackScreenComponentProps } from "@/navigations/Types"; import { asp as g } from "@asp/asp";
import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BeasyLogoIcon from "@components/BeasyLogoIcon"; import BeasyLogoIcon from "@components/BeasyLogoIcon";
import Button from "@components/Button"; import * as Button from "@components/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 CheckIcon from "@components/icons/CheckIcon"; import CheckIcon from "@components/icons/CheckIcon";
import { AntDesign } from "@expo/vector-icons";
import { LOG } from "@logger"; import { LOG } from "@logger";
import { CommonActions } from "@react-navigation/native"; import { Text, View } from "react-native";
// import { Text } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"; import type { MainStackScreenComponentProps } from "@/navigations/types";
const log = LOG.extend("PaymentResultScreen"); const log = LOG.extend("PaymentResultScreen");
...@@ -20,122 +18,109 @@ const PaymentResultScreen: MainStackScreenComponentProps<"paymentResultScreen"> ...@@ -20,122 +18,109 @@ const PaymentResultScreen: MainStackScreenComponentProps<"paymentResultScreen">
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
log.debug("insets", insets); log.debug("insets", insets);
return ( return (
<BeasyDefaultBackgroundWrapper> <BarnoinPayBackground style={[g.gap_lg]}>
<SafeAreaView edges={["top", "left", "right"]}> <View style={[g.px_lg, g.flex_row, g.align_center, g.justify_between]}>
<Box
style={{
height: "100%",
width: "100%",
// marginTop: insets.top,
}}
>
<Box
px={"l"}
flexDirection={"row"}
justifyContent={"space-between"}
alignItems={"center"}
mb={"m"}
>
<BeasyLogoIcon /> <BeasyLogoIcon />
<GoBackIconButton <AntDesign
onPress={() => { name="arrowleft"
navigation.dispatch( size={24}
CommonActions.reset({ color="black"
index: 1, onPress={() => navigation.goBack()}
routes: [{ name: "appBottomTabsNavigator" }],
}),
);
}}
/> />
</Box> </View>
<Box <View style={[g.flex_1]}>
flex={1} <View
backgroundColor={"white"} style={[
borderRadius={20} g.flex_1,
p={"l"} g.gap_lg,
flexDirection={"column"} g.p_lg,
gap={"l"} {
backgroundColor: "white",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
]}
> >
<Box alignItems={"center"}> <View style={[g.self_center]}>
<CheckIcon /> <CheckIcon />
</Box> </View>
<Text variant={"secondary"} fontWeight={"bold"} textAlign={"center"}> <Text style={[g.text_center]}>Transactions effectué avec succès !</Text>
Transactions effectué avec succès !
</Text> <Button.Container style={[g.self_center, { width: 200 }]}>
<Button <Button.Label>Imprimer le réçu</Button.Label>
width={200} </Button.Container>
alignSelf={"center"}
variant={"full"} <View style={[g.rounded_lg, g.p_lg, { backgroundColor: "#eeeef1ff" }]}>
textVariants={"white"} <View
label={"Imprimer le réçu"} style={[
onPress={() => {}} g.flex_row,
/> g.justify_between,
<Box backgroundColor={"lightGray"} flex={1} borderRadius={20} p={"l"}> g.p_md,
<Box { borderBottomColor: "#000", borderBottomWidth: 1 },
flexDirection={"row"} ]}
justifyContent={"space-between"}
borderBottomColor={"gray"}
borderBottomWidth={1}
py={"m"}
> >
<Text fontWeight={"bold"}>Caisse</Text> <Text style={[g.font_bold]}>Caisse</Text>
<Text variant={"black"}>00147C</Text> <Text>00147C</Text>
</Box> </View>
<Box <View
flexDirection={"row"} style={[
justifyContent={"space-between"} g.flex_row,
borderBottomColor={"gray"} g.justify_between,
borderBottomWidth={1} g.p_md,
py={"m"} { borderBottomColor: "#000", borderBottomWidth: 1 },
]}
> >
<Text fontWeight={"bold"}>Reference</Text> <Text style={[g.font_bold]}>Reference</Text>
<Text variant={"black"}>CP...</Text> <Text>CP...</Text>
</Box> </View>
<Box <View
flexDirection={"row"} style={[
justifyContent={"space-between"} g.flex_row,
borderBottomColor={"gray"} g.justify_between,
borderBottomWidth={1} g.p_md,
py={"m"} { borderBottomColor: "#000", borderBottomWidth: 1 },
]}
> >
<Text fontWeight={"bold"}>Mode de paiement</Text> <Text style={[g.font_bold]}>Mode de paiement</Text>
<Text variant={"black"}>Orange</Text> <Text>Orange</Text>
</Box> </View>
<Box <View
flexDirection={"row"} style={[
justifyContent={"space-between"} g.flex_row,
borderBottomColor={"gray"} g.justify_between,
borderBottomWidth={1} g.p_md,
py={"m"} { borderBottomColor: "#000", borderBottomWidth: 1 },
]}
> >
<Text fontWeight={"bold"}>Infos client</Text> <Text style={[g.font_bold]}>Infos client</Text>
<Text variant={"black"}>Dogeless Miso</Text> <Text>Dogeless Miso</Text>
</Box> </View>
<Box <View
flexDirection={"row"} style={[
justifyContent={"space-between"} g.flex_row,
borderBottomColor={"gray"} g.justify_between,
borderBottomWidth={1} g.p_md,
py={"m"} { borderBottomColor: "#000", borderBottomWidth: 1 },
]}
> >
<Text fontWeight={"bold"}>Montant</Text> <Text style={[g.font_bold]}>Montant</Text>
<Text variant={"black"}>10</Text> <Text>10</Text>
</Box> </View>
<Box <View
flexDirection={"row"} style={[
justifyContent={"space-between"} g.flex_row,
borderBottomColor={"gray"} g.justify_between,
borderBottomWidth={1} g.p_md,
py={"m"} { borderBottomColor: "#000" },
]}
> >
<Text fontWeight={"bold"}>N° Client</Text> <Text style={[g.font_bold]}>N° Client</Text>
<Text variant={"black"}>Dogeless Misso</Text> <Text>Dogeless Misso</Text>
</Box> </View>
</Box> </View>
</Box> </View>
</Box> </View>
</SafeAreaView> </BarnoinPayBackground>
</BeasyDefaultBackgroundWrapper>
); );
}; };
......
import { useModalsManagerContext } from "@/contexts/ModalsManagerContext"; /** biome-ignore-all lint/style/useNamingConvention: <For filters to match the codes> */
import BarWithBeasyAndNotificationsIcon from "@components/BarWithBeasyAndNotificationsIcon";
import Button from "@components/Button"; import { asp as g } from "@asp/asp";
import Input from "@components/Input"; import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import TransactionInformationsItem from "@components/TransactionInformationsItem"; import BeasyLogoIcon from "@components/BeasyLogoIcon";
import Box from "@components/bases/Box"; import * as Button from "@components/Button";
import Text from "@components/bases/Text"; import * as Input from "@components/Input";
import WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight from "@components/wrappers/WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight"; import * as Modal from "@components/Modal";
import useTransactionsHistory, { type OperatorsFilter } from "@hooks/useTransactionsHistory"; import Entypo from "@expo/vector-icons/Entypo";
import Ionicons from "@expo/vector-icons/Ionicons";
import { LOG } from "@logger"; import { LOG } from "@logger";
import Card from "@re-card"; import { FlashList } from "@shopify/flash-list";
import theme from "@themes/Theme"; import { useQuery } from "@tanstack/react-query";
import { useCallback, useState } from "react"; import { useState } from "react";
import { RefreshControl, ScrollView, Switch } from "react-native"; import { Switch, Text, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/Ionicons"; import { getTransactions } from "@/features/pay/api";
import { TransactionItem } from "@/features/pay/components/TransactionItem";
import type { PaymentTypeCode } from "@/features/pay/types";
const log = LOG.extend("TransactionHistoryScreen"); const log = LOG.extend("TransactionHistoryScreen");
const TransactionHistoryScreen = () => { const TransactionHistoryScreen = () => {
log.verbose("TransactionHistoryScreen"); log.verbose("TransactionHistoryScreen");
const [showFilterModal, setShowFilterModal] = useState(false);
const [filters, setFilters] = useState<Record<PaymentTypeCode, boolean>>({
CB: true,
FLOOZ: true,
MTN: true,
WAVE: true,
OM: true,
});
const [referenceFilter, setReferenceFilter] = useState("");
const { const transactionHistoryQuery = useQuery({
transactionsHistory: data, queryKey: ["transactionsHistory"],
isLoading, queryFn: getTransactions,
error, });
refetch,
setReferenceFilter,
operatorsFilter,
setOperatorsFilter,
} = useTransactionsHistory();
const { showModal } = useModalsManagerContext(); const transactions = transactionHistoryQuery.data?.data.results
? transactionHistoryQuery.data?.data?.results.filter(
(transaction) =>
filters[transaction.type_paiement_label] &&
transaction.reference.includes(referenceFilter),
)
: [];
return ( return (
<WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight> <BarnoinPayBackground style={[g.gap_5xl]}>
<BarWithBeasyAndNotificationsIcon /> <View style={[g.px_lg, g.flex_row, g.justify_between]}>
<BeasyLogoIcon />
<Ionicons name="notifications" size={24} color="black" />
</View>
<Card variant={"curvedTopContainer"} padding={"s"} height={"100%"} mt={"m"}> <View
<Box px={"m"} flexDirection={"row"} gap={"s"} alignItems={"center"}> style={[
<Box flex={1}> g.px_xl,
<Input placeholder="Reference" onChangeText={setReferenceFilter} /> g.pt_xl,
</Box> g.flex_1,
<Box g.gap_lg,
height={50} { backgroundColor: "white", borderTopLeftRadius: 20, borderTopRightRadius: 20 },
backgroundColor={"lightGray"} ]}
borderRadius={10}
width={50}
// justifyContent={"center"}
alignItems={"center"}
justifyContent={"center"}
> >
<Icon <View style={[g.flex_row, g.gap_sm, g.align_center, g.w_full]}>
name="filter" <Input.Container style={[g.flex_1]}>
size={30} <Input.FieldContainer>
color="black" <Input.Field
// onPress={() => setShowFiltersModal(true)} placeholder="Recherche par référence"
onPress={() => placeholderTextColor={"gray"}
showModal( onChangeText={setReferenceFilter}
<FiltersModal
setOperatorsFilter={setOperatorsFilter}
operatorsFilter={operatorsFilter}
/>,
)
}
/> />
</Box> </Input.FieldContainer>
</Box> </Input.Container>
<ScrollView <TouchableOpacity
// style={{ backgroundColor: "red" }} onPress={() => setShowFilterModal(true)}
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={refetch} />} style={[
contentContainerStyle={{ g.justify_center,
gap: 10, g.align_center,
padding: 5, g.p_md,
// marginTop: 10, g.rounded_md,
paddingBottom: 30, { backgroundColor: "#e8e8e9ff" },
flexDirection: "column", ]}
}}
showsVerticalScrollIndicator={false}
> >
{data?.map((transaction) => ( <Ionicons name="filter" size={24} color="black" />
<TransactionInformationsItem </TouchableOpacity>
key={transaction.transaction_id} </View>
paymentType={transaction.type_paiement_label}
reference={transaction.reference}
amount={transaction.montant}
date={transaction.date}
status={transaction.status}
/>
))}
</ScrollView>
</Card>
</WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight>
);
};
export default TransactionHistoryScreen;
interface FiltersModalProps {
// biome-ignore lint/style/useNamingConvention: <explanation>
operatorsFilter: OperatorsFilter;
// biome-ignore lint/style/useNamingConvention: <explanation>
setOperatorsFilter: React.Dispatch<React.SetStateAction<OperatorsFilter>>;
}
const FiltersModal: React.FC<FiltersModalProps> = ({ operatorsFilter, setOperatorsFilter }) => {
const [filterOm, setFilterOm] = useState(operatorsFilter.OM);
const [filterMtn, setFilterMtn] = useState(operatorsFilter.MTN);
const [filterFlooz, setFilterFlooz] = useState(operatorsFilter.FLOOZ);
const [filterWave, setFilterWave] = useState(operatorsFilter.WAVE);
const [filterCb, setFilterCb] = useState(operatorsFilter.CB);
const saveOperatorsFilters = useCallback(() => {
setOperatorsFilter({
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
OM: filterOm,
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
MTN: filterMtn,
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
FLOOZ: filterFlooz,
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
WAVE: filterWave,
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
CB: filterCb,
});
}, [filterOm, filterMtn, filterFlooz, filterWave, filterCb, setOperatorsFilter]);
const { closeModal } = useModalsManagerContext(); {!transactions ? (
<Text style={[g.text_center]}>Aucune transaction n'a été trouvée</Text>
) : (
<FlashList
data={transactions}
keyExtractor={(item) => item.reference}
estimatedItemSize={75}
bounces={false}
renderItem={({ item }) => (
<View style={[g.p_sm]} key={item.reference}>
<TransactionItem transaction={item} />
</View>
)}
/>
)}
</View>
const saveFilters = useCallback(() => { {/* FILTER MODAL */}
saveOperatorsFilters();
closeModal();
}, [saveOperatorsFilters, closeModal]);
return ( <Modal.RnModal
<Card variant={"absoluteForegroundScreenSizedTransparentCard"}> visible={showFilterModal}
<Box style={{ backgroundColor: "rgba(0, 0, 0, 0.25)", flex: 1 }}
backgroundColor={"white"}
width={"90%"}
// height={"70%"}
maxHeight={"80%"}
borderRadius={10}
p={"m"}
flexDirection={"column"}
style={{ margin: "auto" }}
> >
<Box <Modal.OuterView
justifyContent={"space-between"} style={[
alignItems={"center"} g.p_md,
flexDirection={"row"} g.shadow_elevated,
width={"100%"} {
backgroundColor: "white",
width: "90%",
height: "60%",
},
]}
> >
<Text fontSize={20} variant={"black"} fontWeight={"bold"}> <View style={[g.p_md, g.flex_1, g.gap_sm]}>
Paramétrage filtre <View style={[g.flex_row, g.justify_between, g.align_center]}>
</Text> <Text style={[g.font_bold, g.text_2xl]}>Paramétrage du filtre</Text>
<Icon <Entypo name="cross" size={24} color="black" />
name="close-outline" </View>
size={30} <Text style={[g.font_bold, g.text_lg]}>Opétareurs</Text>
color="black" <View style={[g.px_md, { backgroundColor: "#e8e8e9ff" }]}>
onPress={() => { <View
closeModal(); style={[
}} g.flex_row,
/> g.justify_between,
</Box> g.align_center,
g.py_md,
<Text fontSize={20} fontWeight={"bold"} my={"s"}> { borderBottomColor: "white", borderBottomWidth: 1 },
Opérateurs ]}
</Text>
<Box
width={"100%"}
backgroundColor={"lightGray"}
borderRadius={10}
p={"s"}
gap={"s"}
> >
<Box <Text style={[g.font_bold]}>Orange Money</Text>
flexDirection={"row"}
alignItems={"center"}
justifyContent={"space-between"}
borderBottomColor={"white"}
borderBottomWidth={1}
py={"s"}
>
<Text fontWeight={"bold"}>Orange Money</Text>
<Switch <Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }} trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filterOm} value={filters.OM}
onValueChange={setFilterOm} onValueChange={(checked) =>
setFilters((prev) => ({ ...prev, OM: checked }))
}
/> />
</Box> </View>
<Box <View
flexDirection={"row"} style={[
alignItems={"center"} g.flex_row,
justifyContent={"space-between"} g.justify_between,
borderBottomColor={"white"} g.align_center,
borderBottomWidth={1} g.py_md,
py={"s"} { borderBottomColor: "white", borderBottomWidth: 1 },
]}
> >
<Text fontWeight={"bold"}>MTN Money</Text> <Text style={[g.font_bold]}>MTN Money</Text>
<Switch <Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }} trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filterMtn} value={filters.MTN}
onValueChange={setFilterMtn} onValueChange={(checked) =>
setFilters((prev) => ({ ...prev, MTN: checked }))
}
/> />
</Box> </View>
<Box <View
flexDirection={"row"} style={[
alignItems={"center"} g.flex_row,
justifyContent={"space-between"} g.justify_between,
borderBottomColor={"white"} g.align_center,
borderBottomWidth={1} g.py_md,
py={"s"} { borderBottomColor: "white", borderBottomWidth: 1 },
]}
> >
<Text fontWeight={"bold"}>Flooz Money</Text> <Text style={[g.font_bold]}>Flooz Money</Text>
<Switch <Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }} trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filterFlooz} value={filters.FLOOZ}
onValueChange={setFilterFlooz} onValueChange={(checked) =>
setFilters((prev) => ({ ...prev, FLOOZ: checked }))
}
/> />
</Box> </View>
<Box <View
flexDirection={"row"} style={[
alignItems={"center"} g.flex_row,
justifyContent={"space-between"} g.justify_between,
borderBottomColor={"white"} g.align_center,
borderBottomWidth={1} g.py_md,
py={"s"} { borderBottomColor: "white", borderBottomWidth: 1 },
]}
> >
<Text fontWeight={"bold"}>Wave</Text> <Text style={[g.font_bold]}>Wave</Text>
<Switch <Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }} trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filterWave} value={filters.WAVE}
onValueChange={setFilterWave} onValueChange={(checked) =>
setFilters((prev) => ({ ...prev, WAVE: checked }))
}
/> />
</Box> </View>
<Box <View style={[g.flex_row, g.justify_between, g.align_center, g.py_md]}>
flexDirection={"row"} <Text style={[g.font_bold]}>Carte Bancaire</Text>
alignItems={"center"}
justifyContent={"space-between"}
borderBottomColor={"white"}
py={"s"}
>
<Text fontWeight={"bold"}>Carte Bancaire</Text>
<Switch <Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }} trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filterCb} value={filters.CB}
onValueChange={setFilterCb} onValueChange={(checked) =>
/> setFilters((prev) => ({ ...prev, CB: checked }))
</Box> }
</Box>
<Box style={{ marginTop: 20 }}>
<Button
variant={"full"}
label={"Valider"}
textVariants={"white"}
onPress={saveFilters}
/> />
</Box> </View>
</Box> </View>
</Card> <Button.Container onPress={() => setShowFilterModal(false)}>
<Button.Label>Valider</Button.Label>
</Button.Container>
</View>
</Modal.OuterView>
</Modal.RnModal>
</BarnoinPayBackground>
); );
}; };
export default TransactionHistoryScreen;
import { useUserAuthenticationContext } from "@/contexts/UserAuthenticationContext"; import { asp as g } from "@asp/asp";
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 { LOG } from "@logger"; import { LOG } from "@logger";
import Card from "@re-card"; import { ImageBackground, View } from "react-native";
import Text from "@re-text"; import { LoginForm } from "@/features/auth/components/LoginForm";
import { useCallback, useState } from "react"; import type { MainStackScreenComponentProps } from "@/navigations/types";
import { TouchableOpacity } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const log = LOG.extend("UserLoginScreen"); const log = LOG.extend("UserLoginScreen");
const UserLoginScreen: MainStackScreenComponentProps<"userLoginScreen"> = ({ navigation }) => { const UserLoginScreen: MainStackScreenComponentProps<"userLoginScreen"> = ({ navigation }) => {
log.debug("UserLoginScreen"); 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 ( return (
<WrapperWithDefaultBeasyBackgroundAndSafeAreaFull> <View style={[g.flex_1]}>
<Box height={"100%"}> <ImageBackground
<Box style={{ height: "20%" }} px={"l"}> source={require("@assets/background.png")}
<Box style={[g.flex_1, g.flex_col, g.justify_end]}
px={"m"}
justifyContent={"space-between"}
flexDirection={"row"}
alignItems={"center"}
> >
<TouchableOpacity onPress={() => navigation.goBack()}> <LoginForm />
<Fontisto name="close-a" size={12} color="black" /> </ImageBackground>
</TouchableOpacity> </View>
<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>
); );
}; };
......
import { useUserAuthenticationContext } from "@/contexts/UserAuthenticationContext"; import { asp as g } from "@asp/asp";
import BarWithBeasyAndNotificationsIcon from "@components/BarWithBeasyAndNotificationsIcon"; import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import Button from "@components/Button"; import BeasyLogoIcon from "@components/BeasyLogoIcon";
import Box from "@components/bases/Box"; import * as Button from "@components/Button";
import Text from "@components/bases/Text"; import Ionicons from "@expo/vector-icons/Ionicons";
import WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight from "@components/wrappers/WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight";
import { LOG } from "@logger"; 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 log = LOG.extend("UserProfileScreen");
const UserProfileScreen = () => { const UserProfileScreen = () => {
log.verbose("UserProfileScreen"); log.verbose("UserProfileScreen");
const { logout } = useUserAuthenticationContext(); const dispatch = useDispatch();
const userInformations = useSelector((state: RootState) => state.auth.user);
const { userInformations } = useUserAuthenticationContext();
return ( return (
<WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight> <BarnoinPayBackground style={[g.flex_col, g.justify_between]}>
<> <View style={[g.px_lg, g.flex_row, g.justify_between]}>
<BarWithBeasyAndNotificationsIcon /> <BeasyLogoIcon />
<Card <Ionicons name="notifications" size={24} color="black" />
variant="curvedTopContainer" </View>
height={"100%"}
padding={"m"} <View
gap={"m"} style={[
marginTop={"m"} g.gap_md,
> g.p_md,
<Box {
width={"100%"} borderTopLeftRadius: 20,
// height={200} borderTopRightRadius: 20,
backgroundColor={"lightGray"} backgroundColor: "white",
borderRadius={10} height: "90%",
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"}
> >
<Text /> <View style={[g.w_full, g.rounded_md, g.p_md, { backgroundColor: "#F4F4F4" }]}>
</Box> <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"}> <View style={[g.flex_1, g.flex_col]}>
<Text fontWeight={"bold"} variant={"black"} fontSize={20}> <Text style={[g.font_bold]}>
{userInformations.first_name} {userInformations.last_name} {userInformations.first_name} {userInformations.last_name}
</Text> </Text>
<Text>{userInformations.email}</Text> <Text>{userInformations.email}</Text>
</Box> </View>
</Box> </View>
<Box <View style={[g.w_full, g.flex_row, g.justify_between, g.py_md]}>
width={"100%"} <Text style={[g.font_bold]}>Utilisateur</Text>
flexDirection={"row"} <Text style={[g.text_center]}>{userInformations.username}</Text>
justifyContent={"space-between"} </View>
py={"m"} </View>
>
<Text variant={"black"} fontWeight={"bold"}>
Utilisateur
</Text>
<Text textAlign={"center"}>{userInformations.username}</Text>
</Box>
</Box>
<Button
label={"Deconnexion"}
onPress={logout}
variant={"danger"}
textVariants={"white"}
/>
<Text fontWeight={"bold"}>Informations sur le marchand</Text> <Button.Container
<Box style={[g.rounded_lg, { backgroundColor: "#960101ff" }]}
width={"100%"} onPress={() => dispatch(logout())}
backgroundColor={"lightGray"}
borderRadius={10}
p={"s"}
flexDirection={"column"}
// gap={"m"}
> >
<Box <Button.Label>Deconnexion</Button.Label>
width={"100%"} </Button.Container>
flexDirection={"row"}
borderBottomWidth={1} <Text style={[g.font_bold]}>Informations sur le marchand</Text>
borderBottomColor={"white"} <View
justifyContent={"space-between"} style={[
py={"m"} g.w_full,
g.rounded_md,
g.p_sm,
g.flex_col,
{ backgroundColor: "#F4F4F4" },
]}
> >
<Text variant={"black"} fontWeight={"bold"}> <View
Identifiant style={[
</Text> g.w_full,
<Text textAlign={"center"}> g.flex_row,
{userInformations.marchand.marchand_id} g.justify_between,
</Text> g.py_md,
</Box> { borderBottomColor: "white", borderBottomWidth: 1 },
<Box ]}
width={"100%"}
flexDirection={"row"}
borderBottomWidth={1}
borderBottomColor={"white"}
justifyContent={"space-between"}
py={"m"}
> >
<Text variant={"black"} fontWeight={"bold"}> <Text style={[g.font_bold]}>Identifiant</Text>
Entreprise <Text style={[g.text_center]}>{userInformations.marchand.marchand_id}</Text>
</Text> </View>
<Text textAlign={"center"}>{userInformations.marchand.nom}</Text> <View
</Box> style={[
<Box g.w_full,
width={"100%"} g.flex_row,
flexDirection={"row"} g.justify_between,
borderBottomWidth={1} g.py_md,
borderBottomColor={"white"} { borderBottomColor: "white", borderBottomWidth: 1 },
justifyContent={"space-between"} ]}
py={"m"}
> >
<Text variant={"black"} fontWeight={"bold"}> <Text style={[g.font_bold]}>Entreprise</Text>
Code <Text style={[g.text_center]}>{userInformations.marchand.nom}</Text>
</Text> </View>
<Text textAlign={"center"}>{userInformations.marchand.code}</Text> <View
</Box> style={[
<Box g.w_full,
width={"100%"} g.flex_row,
flexDirection={"row"} g.justify_between,
borderBottomWidth={1} g.py_md,
borderBottomColor={"white"} { borderBottomColor: "white", borderBottomWidth: 1 },
justifyContent={"space-between"} ]}
py={"m"}
> >
<Text variant={"black"} fontWeight={"bold"}> <Text style={[g.font_bold]}>Code</Text>
Addresse <Text style={[g.text_center]}>{userInformations.marchand.code}</Text>
</Text> </View>
<Text textAlign={"center"}>{userInformations.marchand.adresse}</Text> <View
</Box> style={[
<Box g.w_full,
width={"100%"} g.flex_row,
flexDirection={"row"} g.justify_between,
borderBottomWidth={1} g.py_md,
borderBottomColor={"white"} { borderBottomColor: "white", borderBottomWidth: 1 },
justifyContent={"space-between"} ]}
py={"m"}
alignItems={"center"}
> >
<Text variant={"black"} fontWeight={"bold"}> <Text style={[g.font_bold]}>Addresse</Text>
Url succès <Text style={[g.text_center]}>{userInformations.marchand.adresse}</Text>
</Text> </View>
<Text textAlign={"center"}>{userInformations.marchand.url_succes}</Text> <View
</Box> style={[
<Box g.w_full,
width={"100%"} g.flex_row,
flexDirection={"row"} g.justify_between,
justifyContent={"space-between"} g.py_md,
// alignItems={"center"} { borderBottomColor: "white", borderBottomWidth: 1 },
py={"m"} ]}
> >
<Text variant={"black"} fontWeight={"bold"}> <Text style={[g.font_bold]}>Url succès</Text>
Url échec <Text style={[g.text_center]}>{userInformations.marchand.url_succes}</Text>
</Text> </View>
<Text textAlign={"center"}>{userInformations.marchand.url_echec}</Text> <View style={[g.w_full, g.flex_row, g.py_md, g.justify_between]}>
</Box> <Text style={[g.font_bold]}>Url échec</Text>
</Box> <Text style={[g.text_center]}>{userInformations.marchand.url_echec}</Text>
</Card> </View>
</> </View>
</WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight> </View>
</BarnoinPayBackground>
); );
}; };
......
import type { MainStackScreenComponentProps } from "@/navigations/Types"; import { asp as g } from "@asp/asp";
import Button from "@components/Button"; import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BackgroundWithBeasyIconAndWhiteContentArea from "@components/backgrounds/BackgroundWithBeasyIconAndWhiteContentArea"; import BeasyLogoIcon from "@components/BeasyLogoIcon";
import Box from "@components/bases/Box"; import * as Button from "@components/Button";
import Text from "@components/bases/Text"; import * as Modal from "@components/Modal";
import useWave from "@hooks/useWave"; import AntDesign from "@expo/vector-icons/AntDesign";
import { LOG } from "@logger"; import { LOG } from "@logger";
import { Dimensions } from "react-native"; import { useMutation } from "@tanstack/react-query";
// biome-ignore lint/style/useNamingConvention: <explanation> import type { AxiosError } from "axios";
import QRCode from "react-native-qrcode-svg"; 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"); const log = LOG.extend("WaveQrCodePaymentScreen");
...@@ -16,62 +20,108 @@ const WaveQrCodePaymentScreen: MainStackScreenComponentProps<"waveQrCodePaymentS ...@@ -16,62 +20,108 @@ const WaveQrCodePaymentScreen: MainStackScreenComponentProps<"waveQrCodePaymentS
navigation, navigation,
}) => { }) => {
log.verbose("WaveQrCodePaymentScreen"); log.verbose("WaveQrCodePaymentScreen");
const [error, setError] = useState("");
const data = route.params.data; const data = route.params.data;
const windowWidth = Dimensions.get("window").width; 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 ( return (
<BackgroundWithBeasyIconAndWhiteContentArea goBack={true}> <BarnoinPayBackground style={[g.justify_between]}>
<Box <View style={[g.px_lg, g.flex_row, g.align_center, g.justify_between]}>
style={{ <BeasyLogoIcon />
height: "100%", <AntDesign
width: "100%", name="arrowleft"
}} size={24}
p={"m"} color="black"
alignItems={"center"} onPress={() => navigation.goBack()}
> />
<Text </View>
variant={"black"} <View
mb={"l"} style={[
fontSize={20} g.w_full,
textAlign={"center"} g.p_5xl,
fontWeight={"bold"} g.gap_xl,
{
backgroundColor: "white",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
height: "80%",
},
]}
> >
Votre QR Code <Text style={[g.text_3xl, g.font_bold]}>Votre QR Code</Text>
</Text>
<Box <View
style={{ height: windowWidth - 80, aspectRatio: 1 }} style={[
backgroundColor={"primary"} g.w_full,
alignItems={"center"} g.align_center,
justifyContent={"center"} g.justify_center,
borderRadius={20} g.p_md,
borderWidth={2} g.rounded_lg,
borderColor={"secondary"} g.shadow_elevated,
shadowColor={"black"} { aspectRatio: 1, backgroundColor: "white" },
shadowOffset={{ width: 0, height: 10 }} ]}
shadowOpacity={0.5}
shadowRadius={13.16}
elevation={20}
mb={"l"}
> >
<QRCode <QrCode value={data.wave_launch_url} size={qrSize} />
value={data.wave_launch_url} </View>
size={windowWidth - 120}
// backgroundColor={theme.colors.secondary}
/>
</Box>
<Text>Veuillez scanner le QR Code pour terminer le paiement</Text> <Text>Veuillez scanner le QR Code pour terminer le paiement</Text>
<Box width={"100%"} mt={"x100"}> <Button.Container onPress={() => query.mutate()} isLoading={query.isPending}>
<Button <Button.Label>Verification</Button.Label>
variant={"full"} </Button.Container>
textVariants={"white"} </View>
label="Verification"
onPress={() => handlePaymentVerification(data.id)} <Modal.RnModal visible={!!error} style={{ backgroundColor: "rgba(0, 0, 0, 0.25)" }}>
/> <Modal.OuterView
</Box> style={[
</Box> g.p_md,
</BackgroundWithBeasyIconAndWhiteContentArea> 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"; import { Dimensions } from "react-native";
export const cardVariants = { 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 @@ ...@@ -14,6 +14,8 @@
"@styles/*": ["./src/styles/*"], "@styles/*": ["./src/styles/*"],
"@hooks/*": ["./src/hooks/*"], "@hooks/*": ["./src/hooks/*"],
"@logger": ["./src/utils/logger"], "@logger": ["./src/utils/logger"],
"@asp/*": ["./src/appStylingPrimitives/*"],
"@assets/*": ["./assets/*"],
"@/*": ["./src/*"] "@/*": ["./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