diff --git a/App.tsx b/App.tsx
index 35bd70f..48b4367 100644
--- a/App.tsx
+++ b/App.tsx
@@ -32,10 +32,7 @@ import Cable from './screens/cable';
import Support from './screens/support';
import requestInAppReview from './utils/requestInAppReview';
-// TODO: add autocomplete prop to all text inputs
-// TODO: add autofocus, keyboardAvoidingView and onSubmitediting prop to all forms
// TODO: install why did you render to monitor avoidable re-renders
-// TODO: remove orange cellular icon from splashscreen or redesign it
/**
* Create Stack navigator
diff --git a/android/app/src/main/res/drawable/splashscreen_bg.png b/android/app/src/main/res/drawable/splashscreen_bg.png
index c78dec5..7e7da5d 100644
Binary files a/android/app/src/main/res/drawable/splashscreen_bg.png and b/android/app/src/main/res/drawable/splashscreen_bg.png differ
diff --git a/components/CarrierAndPhoneNumberField.tsx b/components/CarrierAndPhoneNumberField.tsx
index b37a045..d799515 100644
--- a/components/CarrierAndPhoneNumberField.tsx
+++ b/components/CarrierAndPhoneNumberField.tsx
@@ -69,6 +69,8 @@ const CarrierAndPhoneNumberField = ({
textContentType="telephoneNumber"
value={phoneNumber}
onChangeText={value => setPhoneNumber(value)}
+ autoComplete={"tel"}
+ autoFocus
/>
diff --git a/components/FaultyTxModal.tsx b/components/FaultyTxModal.tsx
new file mode 100644
index 0000000..e041073
--- /dev/null
+++ b/components/FaultyTxModal.tsx
@@ -0,0 +1,68 @@
+//import libraries
+import React, {useState} from 'react';
+import Text from '~components/Text';
+import tw from '../lib/tailwind';
+import BottomSheet, {BOTTOMSHEETHEIGHT} from './BottomSheet';
+import Clipboard from '@react-native-clipboard/clipboard';
+import {NavigationProp, useNavigation} from '@react-navigation/native';
+import {StackParamList} from '../navigation/screenParams';
+
+interface FaultyTxModalProps {
+ /**
+ * The transaction ref to display.
+ * This determines the visibility of the component
+ */
+ txRef?: string;
+
+ /**
+ * Function to be called when modal is dismissed
+ */
+ onDismiss: () => void;
+}
+
+const FAULTY_TX_MSG = `Payment was successful but top-up failed due to slow internet connection. Copy below transaction id, then you'll redirected to contact our customer care and complete top-up. Please keep the copied transaction id safe`;
+
+/**
+ * Displays modal for faulty transactions i.e transactions where payment
+ * was successful but top-up completion failed for some reason.
+ * It shows the `transaction ref` to user, so user can copy it and resolve issue
+ * with customer care
+ */
+const FaultyTxModal = ({txRef, onDismiss}: FaultyTxModalProps) => {
+ const [txRefCopied, setTxRefCopied] = useState(false);
+ const {navigate} = useNavigation>();
+
+ const handleCopyId = () => {
+ Clipboard.setString(txRef!);
+ setTxRefCopied(true);
+ setTimeout(() => navigate('Support'), 1000);
+ };
+
+ const noop = () => null;
+
+ return (
+
+ {FAULTY_TX_MSG}
+
+ {txRef}
+
+
+ {txRefCopied ? 'TRANSACTION ID COPIED' : 'COPY'}
+
+
+ );
+};
+
+export default FaultyTxModal;
diff --git a/components/PaymentBottomSheet.tsx b/components/PaymentBottomSheet.tsx
index 5043dd7..da8d2b1 100644
--- a/components/PaymentBottomSheet.tsx
+++ b/components/PaymentBottomSheet.tsx
@@ -9,6 +9,7 @@ import PaymentMethodButton, {PAYMENT_METHODS} from './PaymentMethodButton';
import {FlutterwaveInitOptions} from 'flutterwave-react-native/dist/FlutterwaveInit';
import constants from '../utils/constants';
import {AppServices} from '../global';
+import {decrypt, encrypt} from '../utils/crypto';
interface PaymentBottomSheetProps {
/**
@@ -26,6 +27,14 @@ interface PaymentBottomSheetProps {
*/
amount: number;
+ /**
+ * The unencrypted transaction ref to use for flutterwave.
+ * We delegate tx ref generation to housing component because
+ * we want tx ref to house all top-up details to aid in faulty tx handling.
+ * This component will handle encryption of tx_ref
+ */
+ flutterwaveTxRef: string;
+
/**
* Indicates the service being paid for
*/
@@ -62,9 +71,13 @@ const PaymentBottomSheet = ({
onFlutterwaveInit,
service = 'airtime',
visible = false,
+ flutterwaveTxRef,
}: PaymentBottomSheetProps) => {
const balance = useAppStore(state => state.profile?.wallet_balance);
const profile = useAppStore(state => state.profile);
+ const txRef = useMemo(() => encrypt(
+ flutterwaveTxRef || `service=${service};amt=${amount};dt=${Date.now()}`,
+ ),[flutterwaveTxRef, amount]);
// flutterwave options
const flutterwaveOptions = useMemo<
@@ -73,9 +86,7 @@ const PaymentBottomSheet = ({
() => ({
...constants.INCOMPLETE_STATIC_FLUTTERWAVE_PAYMENT_OPTIONS,
amount: amount,
- tx_ref: `${amount}-${service}-top-up-${
- Date.now() + Math.floor(Math.random() * 100_000).toString(16)
- }`,
+ tx_ref: txRef || `service=${service};amt=${amount};dt=${Date.now()}`,
customer:
profile?.email && profile?.username
? {
@@ -93,7 +104,7 @@ const PaymentBottomSheet = ({
} Payment`,
},
}),
- [amount, profile?.email, profile?.username],
+ [amount, profile?.email, profile?.username, txRef],
);
return (
diff --git a/ios/EasyVtu/Images.xcassets/SplashScreenBackground.imageset/background.png b/ios/EasyVtu/Images.xcassets/SplashScreenBackground.imageset/background.png
index 2a8ebe5..e63c92e 100644
Binary files a/ios/EasyVtu/Images.xcassets/SplashScreenBackground.imageset/background.png and b/ios/EasyVtu/Images.xcassets/SplashScreenBackground.imageset/background.png differ
diff --git a/package-lock.json b/package-lock.json
index c5cb596..3fbd8ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,10 +8,13 @@
"version": "0.0.1",
"dependencies": {
"@hookform/resolvers": "^2.9.8",
+ "@react-native-clipboard/clipboard": "^1.11.1",
"@react-navigation/bottom-tabs": "^6.4.0",
"@react-navigation/native": "^6.0.13",
"@react-navigation/native-stack": "^6.9.0",
+ "@types/crypto-js": "^4.1.1",
"axios": "^1.0.0",
+ "crypto-js": "^3.1.9-1",
"flutterwave-react-native": "^1.0.2",
"lottie-react-native": "^5.1.4",
"react": "18.1.0",
@@ -25,7 +28,7 @@
"react-native-in-app-review": "^4.2.1",
"react-native-linear-gradient": "^2.6.2",
"react-native-mmkv-storage": "^0.8.0",
- "react-native-navigation-bar-color": "^2.0.1",
+ "react-native-navigation-bar-color": "github:stanleyugwu/react-native-navigation-bar-color#stanleyugwu-patch-1",
"react-native-safe-area-context": "^4.4.1",
"react-native-screens": "^3.17.0",
"react-native-select-contact": "^1.6.3",
@@ -2762,6 +2765,15 @@
"node": ">= 8"
}
},
+ "node_modules/@react-native-clipboard/clipboard": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.11.1.tgz",
+ "integrity": "sha512-nvSIIHzybVWqYxcJE5hpT17ekxAAg383Ggzw5WrYHtkKX61N1AwaKSNmXs5xHV7pmKSOe/yWjtSwxIzfW51I5Q==",
+ "peerDependencies": {
+ "react": ">=16.0",
+ "react-native": ">=0.57.0"
+ }
+ },
"node_modules/@react-native-community/cli": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-9.3.2.tgz",
@@ -4821,6 +4833,11 @@
"@babel/types": "^7.3.0"
}
},
+ "node_modules/@types/crypto-js": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz",
+ "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA=="
+ },
"node_modules/@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
@@ -6791,6 +6808,11 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-js": {
+ "version": "3.1.9-1",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz",
+ "integrity": "sha512-W93aKztssqf29OvUlqfikzGyYbD1rpkXvGP9IQ1JchLY3bxaLXZSWYbwrtib2vk8DobrDzX7PIXcDWHp0B6Ymw=="
+ },
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -14403,8 +14425,8 @@
},
"node_modules/react-native-navigation-bar-color": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/react-native-navigation-bar-color/-/react-native-navigation-bar-color-2.0.1.tgz",
- "integrity": "sha512-1kE/oxWt+HYjRxdZdvke9tJ365xaee5n3+euOQA1En8zQuSbOxiE4SYEGM7TeaWnmLJ0l37mRnPHaB2H4mGh0A=="
+ "resolved": "git+ssh://git@github.com/stanleyugwu/react-native-navigation-bar-color.git#e4f5e6bda1dc2d1b919756853f0aa542245de79e",
+ "license": "MIT"
},
"node_modules/react-native-safe-area-context": {
"version": "4.4.1",
@@ -19402,6 +19424,12 @@
"fastq": "^1.6.0"
}
},
+ "@react-native-clipboard/clipboard": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.11.1.tgz",
+ "integrity": "sha512-nvSIIHzybVWqYxcJE5hpT17ekxAAg383Ggzw5WrYHtkKX61N1AwaKSNmXs5xHV7pmKSOe/yWjtSwxIzfW51I5Q==",
+ "requires": {}
+ },
"@react-native-community/cli": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-9.3.2.tgz",
@@ -20882,6 +20910,11 @@
"@babel/types": "^7.3.0"
}
},
+ "@types/crypto-js": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz",
+ "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA=="
+ },
"@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
@@ -22366,6 +22399,11 @@
"which": "^2.0.1"
}
},
+ "crypto-js": {
+ "version": "3.1.9-1",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz",
+ "integrity": "sha512-W93aKztssqf29OvUlqfikzGyYbD1rpkXvGP9IQ1JchLY3bxaLXZSWYbwrtib2vk8DobrDzX7PIXcDWHp0B6Ymw=="
+ },
"css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -28104,9 +28142,8 @@
"requires": {}
},
"react-native-navigation-bar-color": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/react-native-navigation-bar-color/-/react-native-navigation-bar-color-2.0.1.tgz",
- "integrity": "sha512-1kE/oxWt+HYjRxdZdvke9tJ365xaee5n3+euOQA1En8zQuSbOxiE4SYEGM7TeaWnmLJ0l37mRnPHaB2H4mGh0A=="
+ "version": "git+ssh://git@github.com/stanleyugwu/react-native-navigation-bar-color.git#e4f5e6bda1dc2d1b919756853f0aa542245de79e",
+ "from": "react-native-navigation-bar-color@github:stanleyugwu/react-native-navigation-bar-color#stanleyugwu-patch-1"
},
"react-native-safe-area-context": {
"version": "4.4.1",
diff --git a/package.json b/package.json
index 2e02df0..0ce7bb0 100644
--- a/package.json
+++ b/package.json
@@ -11,10 +11,13 @@
},
"dependencies": {
"@hookform/resolvers": "^2.9.8",
+ "@react-native-clipboard/clipboard": "^1.11.1",
"@react-navigation/bottom-tabs": "^6.4.0",
"@react-navigation/native": "^6.0.13",
"@react-navigation/native-stack": "^6.9.0",
+ "@types/crypto-js": "^4.1.1",
"axios": "^1.0.0",
+ "crypto-js": "^3.1.9-1",
"flutterwave-react-native": "^1.0.2",
"lottie-react-native": "^5.1.4",
"react": "18.1.0",
diff --git a/react-app-env.d.ts b/react-app-env.d.ts
index baa80eb..fb768f6 100644
--- a/react-app-env.d.ts
+++ b/react-app-env.d.ts
@@ -55,4 +55,5 @@ declare module '@env' {
// CHORE: When new env var are added in .env file,
// export the key as const like below
export const FLUTTERWAVE_MERCHANT_API_KEY: S;
+ export const ENCRYPTION_KEY: S;
}
diff --git a/screens/airtime/airtime.d.ts b/screens/airtime/airtime.d.ts
index 9a84afe..2dd8dd6 100644
--- a/screens/airtime/airtime.d.ts
+++ b/screens/airtime/airtime.d.ts
@@ -4,9 +4,3 @@ export enum Carrier {
Glo = 'glo',
Etisalat = 'etisalat',
}
-
-export type IncompleteTopUp = {
- phone: string;
- amount: number;
- carrier: Carrier;
-};
diff --git a/screens/airtime/index.tsx b/screens/airtime/index.tsx
index eeff944..54c8bb7 100644
--- a/screens/airtime/index.tsx
+++ b/screens/airtime/index.tsx
@@ -1,6 +1,6 @@
//import libraries
-import React, {useRef, useState} from 'react';
-import {View} from 'react-native';
+import React, {useRef, useState, useMemo} from 'react';
+import {Keyboard, View} from 'react-native';
import tw from '../../lib/tailwind';
import SafeAreaScrollView from '~components/SafeAreaScrollView';
import AppHeader from '~components/AppHeader';
@@ -9,7 +9,7 @@ import {useForm, useWatch} from 'react-hook-form';
import {yupResolver} from '@hookform/resolvers/yup';
import AirtimeSchema from './airtime.schema';
import {InferType} from 'yup';
-import {Carrier, IncompleteTopUp} from './airtime.d';
+import {Carrier} from './airtime.d';
import Button from '~components/Button';
import SnackBar from '~components/SnackBar';
import getFirstError from '../../utils/getFirstError';
@@ -24,13 +24,10 @@ import WalletBalance from '~components/WalletBalance';
import reduceWalletBalanceBy from '../../utils/reduceWalletBalance';
import balanceIsSufficient from '../../utils/balanceIsSufficient';
import requestInAppReview from '../../utils/requestInAppReview';
-import useInAppUpdate from '../../hooks/useInAppUpdate';
+import FaultyTxModal from '~components/FaultyTxModal';
// Airtime Screen Component
const Airtime = () => {
- // in-app update
- useInAppUpdate();
-
/**
* Form and validation logics and handles
*/
@@ -59,34 +56,10 @@ const Airtime = () => {
// handles submission after form-level validation
const handleBuyAirtime = handleSubmit(data => {
+ Keyboard.dismiss();
// TODO: create a proper method of checking user login state instead of
// by checking the value of profile balance
- // here we check if there's an incomplete top-up so that we
- // complete it before initialising new transaction
- if (incompleteTopUpCache.current) {
- const values = incompleteTopUpCache.current;
- setLoaderVisible(true);
- return airtimeTopUp(values.phone, values.amount, values.carrier)
- .then(res => {
- // top-up completed. let's clear incomplete top up cache.
- // DON'T REMOVE BELOW LINE
- incompleteTopUpCache.current = undefined;
- setSuccessMsg(
- `Previous incomplete top-up of #${
- values.amount + ' ' + values.carrier
- } to ${values.phone} completed successfully`,
- );
- })
- .catch(error => {
- // we'll show error but won't clear incomplete top-up cache
- setRequestError(
- 'Error completing previous top-up transaction. Check your internet connection and try again',
- );
- })
- .finally(() => setLoaderVisible(false));
- }
-
// show payment methods bottom sheet
setPaymentSheetVisible(true);
});
@@ -100,15 +73,7 @@ const Airtime = () => {
const [paymentSheetVisible, setPaymentSheetVisible] = useState(false);
const [loaderVisible, setLoaderVisible] = useState(false);
const [successMsg, setSuccessMsg] = useState();
-
- /**
- * In order to handle faulty transactions, or edge case where user completes
- * payment but hits network error when app is about to top-up his/her account,
- * this ref will cache details of an incomplete top-up so when user has stable
- * connection, the top-up will be completed.
- */
- const incompleteTopUpCache = useRef(undefined);
-
+ const [faultyTxRef, setFaultyTxRef] = useState(undefined);
/**
* This ref will be used to cache form values when flutterwave payment is initialised.
* the form values will be used after successful payment, but we don't want to get the values
@@ -118,6 +83,18 @@ const Airtime = () => {
undefined,
);
+ // Airtime Tx ref
+ const TX_REF = useMemo(
+ () =>
+ `service=airtime;amt=${getValues('amount')};carrier=${getValues(
+ 'carrier',
+ )};phone=${getValues('phoneNumber')};dt=${Date.now()}`,
+ [
+ /* when the value of any form field changes we regenrate tx_ref*/
+ Object.values(getValues()).join(''),
+ ],
+ );
+
// Handles wallet payment
const handleWalletPaymentMethod = async () => {
const values = getValues();
@@ -179,19 +156,27 @@ const Airtime = () => {
setSuccessMsg(res.message); // show success overlay
})
.catch(error => {
- // TODO: find a better way to prevent faulty transaction or incomplete top-up
+ /**
+ * The most secured approach to faulty tx:
+ * when tx is faulty, try and encrypt and store tx_ref & top-up details in storage.
+ * if storage failed, show the user the tx_ref so to resolve the issue with customer care.
+ * customer care would verify tx_ref, release top-up to user, then store tx_ref in resolved tx db
+ * if storage succeeds, an error is shown to user and a 'retry-previous-tx' button appears on the
+ * service screen e.g airtime screen. The visibilty of the btn depends on the existence of the faulty tx
+ * storage in device. When btn is pressed, app retrieves and decrypts stored faulty tx details, verifies tx_ref,
+ * retrieves top-up amount from verified tx, releases top-up value, stores the tx_ref in resolved tx db, and
+ * deletes the faulty tx from storage.
+ *
+ * Another approach would be to embed every top-up detail e.g amount, service, even tx_id into the tx_ref and encode/encrypt it.
+ * The user can present the encrypted/encoded text to customer care and customer care will know the details of the
+ * tx and be able to confirm it. Or the app can take the encrypted/emcoded tx_ref, decode it and know what value to release
+ * to user
+ *
+ * However, the feasible approach now is to resolve issue using tx_ref through customer care
+ */
- // Edge case. Error when hitting server. we need make sure top-up is
- // completed cus user has completed payment. lets cache the top-up details
- // for later and show error.
- setRequestError(
- `Payment was successful but top-up failed. Connect to internet now and press 'Buy Airtime' button to complete your transaction. Don't close the app, and stay on this screen until top-up completes`,
- );
- incompleteTopUpCache.current = {
- amount: values.amount,
- phone: values.phoneNumber,
- carrier: values.carrier as Carrier,
- };
+ // show faulty tx view
+ setFaultyTxRef(data.tx_ref);
})
.finally(() => {
setPaymentSheetVisible(false);
@@ -232,6 +217,7 @@ const Airtime = () => {
value={formValues[0] ? `${formValues[0]}` : undefined}
keyboardType="number-pad"
onChangeText={value => setValue('amount', +value)}
+ onSubmitEditing={handleBuyAirtime}
/>
@@ -240,8 +257,7 @@ const Cable = ({}: StackScreen<'Cable'>) => {
keyboardType="phone-pad"
textContentType="telephoneNumber"
autoComplete="tel"
- autoCorrect
- onEndEditing={handleSubscribe}
+ onSubmitEditing={handleSubscribe}
onChangeText={text => form.setValue('phoneNumber', text)}
defaultValue={profile?.phone || ''}
/>
@@ -287,10 +303,15 @@ const Cable = ({}: StackScreen<'Cable'>) => {
handleFlutterwaveRedirect={handleFlutterwaveRedirect}
amount={form.getValues('amount') || NaN}
service="cable tv"
+ flutterwaveTxRef={TX_REF}
handleFlutterwaveInitError={handleFlutterwaveInitError}
handleWalletPayment={handleWalletPayment}
onFlutterwaveInit={() => (cachedFormValues.current = form.getValues())}
/>
+ setFaultyTxRef(undefined)}
+ />
>
);
};
diff --git a/screens/change_password/index.tsx b/screens/change_password/index.tsx
index e6fb507..41ea170 100644
--- a/screens/change_password/index.tsx
+++ b/screens/change_password/index.tsx
@@ -77,6 +77,7 @@ const ChangePassword = () => {
onChangeText={text => form.setValue('password', text)}
secureTextEntry={passwordMasked}
rightElement={GetEyeIcon(passwordMasked, setPasswordMasked)}
+ autoFocus
/>
) => {
- // in-app update
- useInAppUpdate();
-
const [requestError, setRequestError] = useState(
undefined,
);
@@ -46,6 +43,7 @@ const MobileData = (route: StackScreen<'Data'>) => {
const [plansSheetVisible, setPlansSheetVisible] = useState(false);
const [loaderVisible, setLoaderVisible] = useState(false);
const [successMsg, setSuccessMsg] = useState();
+ const [faultyTxRef, setFaultyTxRef] = useState();
/**
* We implement a security measure for top-up whereby we cache form values
* before initialising payment with external payment gateway so when payment is
@@ -118,6 +116,13 @@ const MobileData = (route: StackScreen<'Data'>) => {
*/
const formValues = useWatch({control, defaultValue: {carrier: Carrier.Mtn}});
+ // TX REF
+ const TX_REF = `service=data;amt=${formValues.amount};planId=${
+ formValues.planId
+ };phone=${formValues.phoneNumber};carrier=${
+ formValues.carrier
+ };dt=${Date.now()}`;
+
const handleSelectDataPlan = () => {
setPlansSheetVisible(true);
const currentQuery = getCurrentCarrierQuery();
@@ -125,35 +130,7 @@ const MobileData = (route: StackScreen<'Data'>) => {
};
const handleBuyData = handleSubmit(d => {
- // here we check if there's an incomplete top-up so that we
- // complete it before initialising new transaction
- if (incompleteTopUpCache.current) {
- const values = incompleteTopUpCache.current;
- setLoaderVisible(true);
- return dataTopUp(
- values.carrier as Carrier,
- values.planId,
- values.amount,
- values.phoneNumber,
- )
- .then(res => {
- // top-up completed. let's clear incomplete top up cache.
- // DON'T REMOVE BELOW LINE
- incompleteTopUpCache.current = undefined;
- setSuccessMsg(
- `Previous incomplete top-up of #${
- values.amount + ' ' + values.carrier + ' data'
- } to ${values.phoneNumber} completed successfully`,
- );
- })
- .catch(error => {
- // we'll show error but won't clear incomplete top-up cache
- setRequestError(
- 'Error completing previous top-up transaction. Check your internet connection and try again',
- );
- })
- .finally(() => setLoaderVisible(false));
- } else setPaymentSheetVisible(true);
+ setPaymentSheetVisible(true);
});
// Handles redirection after flutterwave payment
@@ -175,13 +152,8 @@ const MobileData = (route: StackScreen<'Data'>) => {
setSuccessMsg('Data top-up transaction successful'); // show success overlay
})
.catch(error => {
- // Edge case. Error when hitting server. we need make sure top-up is
- // completed cus user has completed payment. lets cache the top-up details
- // for later and show error.
- setRequestError(
- `Payment was successful but top-up failed. Connect to internet now and press 'Buy Airtime' button to complete your transaction. Don't close the app, and stay on this screen until top-up completes`,
- );
- incompleteTopUpCache.current = {...values};
+ // handle faulty tx
+ setFaultyTxRef(data.tx_ref);
})
.finally(() => {
setPaymentSheetVisible(false);
@@ -273,10 +245,10 @@ const MobileData = (route: StackScreen<'Data'>) => {
setValue('phoneNumber', numberAndCarrier[0]);
if (numberAndCarrier[1]) {
setValue('carrier', numberAndCarrier[1]);
- if(formValues.carrier !== numberAndCarrier[1]){
+ if (formValues.carrier !== numberAndCarrier[1]) {
setSelectedPlanName('Select a data plan');
// @ts-ignore
- setValue("planId", undefined);
+ setValue('planId', undefined);
}
}
}}
@@ -303,7 +275,8 @@ const MobileData = (route: StackScreen<'Data'>) => {
visible={paymentSheetVisible}
onDismiss={() => setPaymentSheetVisible(false)}
handleFlutterwaveRedirect={handleFlutterwaveRedirect}
- amount={formValues.amount || NaN}
+ amount={formValues.amount || 1000}
+ flutterwaveTxRef={TX_REF}
service="data"
handleFlutterwaveInitError={handleFlutterwaveInitError}
handleWalletPayment={handleWalletPayment}
@@ -366,6 +339,10 @@ const MobileData = (route: StackScreen<'Data'>) => {
requestInAppReview();
}}
/>
+ setFaultyTxRef(undefined)}
+ />
>
);
};
diff --git a/screens/electricity/index.tsx b/screens/electricity/index.tsx
index beae1eb..c7ca5e4 100644
--- a/screens/electricity/index.tsx
+++ b/screens/electricity/index.tsx
@@ -1,6 +1,6 @@
//import libraries
import React, {useRef, useState} from 'react';
-import {View} from 'react-native';
+import {Keyboard, View} from 'react-native';
import Text from '~components/Text';
import tw from '../../lib/tailwind';
import SafeAreaScrollView from '~components/SafeAreaScrollView';
@@ -26,9 +26,9 @@ import Loader from '~components/Loader';
import _topUpElectricity from '../../api/services/topUpElectricity';
import SuccessOverlay from '~components/SuccessOverlay';
import {RedirectParams} from 'flutterwave-react-native/dist/PayWithFlutterwave';
-import constants from '../../utils/constants';
import reduceWalletBalanceBy from '../../utils/reduceWalletBalance';
import requestInAppReview from '../../utils/requestInAppReview';
+import FaultyTxModal from '~components/FaultyTxModal';
type ElectricitySchemaFields = InferType;
@@ -41,6 +41,7 @@ const Electricity = () => {
useState(false);
const [loaderVisible, setLoaderVisible] = useState(false);
const [successMsg, setSuccessMsg] = useState(undefined);
+ const [faultyTxRef, setFaultyTxRef] = useState();
/**
* Caches form values before flutterwave initialisation
@@ -65,6 +66,14 @@ const Electricity = () => {
},
});
+ // TX REF
+ const formValues = form.getValues();
+ const TX_REF = `service=electricity;provider=${formValues.provider};amt=${
+ formValues.amount
+ };phone=${formValues.phoneNumber};email=${formValues.emailAddress};meterNo=${
+ formValues.meterNumber
+ };dt=${Date.now()}`;
+
/**
* Shareable wrapper for electricity top-up API service.
* This wrapper contains common side effects for wallet and flutterwave
@@ -73,7 +82,7 @@ const Electricity = () => {
const topUpElectricity = React.useCallback(
async (
values: Parameters['0'],
- errorMsg?: string,
+ txRef?: string,
) => {
// payment successful, let's hit server
setLoaderVisible(true);
@@ -83,8 +92,14 @@ const Electricity = () => {
setSuccessMsg('Electricity top-up transaction successful'); // show success overlay
return res;
} catch (error: any) {
- // Edge case. Error when hitting server to release service paid for.
- setRequestError(errorMsg || error.message);
+ // if txRef was passed, then it's flutterwave payment
+ // else it's wallet, and we handle errors from both methods differently
+ if (txRef) {
+ // handle faulty tx
+ setFaultyTxRef(txRef);
+ } else {
+ setRequestError(error.message);
+ }
} finally {
setPaymentBottomSheetVisible(false);
cachedFormValues.current = null;
@@ -98,6 +113,7 @@ const Electricity = () => {
* Handles form submission
*/
const handleBuyElectricity = form.handleSubmit(values => {
+ Keyboard.dismiss();
setPaymentBottomSheetVisible(true);
});
@@ -134,7 +150,7 @@ const Electricity = () => {
(data: RedirectParams) => {
if (data.status === 'successful') {
const values = cachedFormValues.current!;
- topUpElectricity(values, constants.FAULTY_TX_MSG);
+ topUpElectricity(values, data.tx_ref);
} else {
// payment failed
cachedFormValues.current = null;
@@ -170,6 +186,7 @@ const Electricity = () => {
placeholder="Enter your meter number"
keyboardType="number-pad"
onChangeText={text => form.setValue('meterNumber', text)}
+ autoFocus
/>
{
placeholder={'Enter phone number'}
onChangeText={text => form.setValue('phoneNumber', text)}
defaultValue={profile?.phone}
+ autoComplete="tel"
/>
form.setValue('emailAddress', text)}
defaultValue={profile?.email}
+ autoComplete="email"
+ onSubmitEditing={handleBuyElectricity}
/>