-
Matiss Janis Aboltins authored
Part 4 of the migration. Final moves. Previous PR: https://github.com/actualbudget/actual/pull/1420
Matiss Janis Aboltins authoredPart 4 of the migration. Final moves. Previous PR: https://github.com/actualbudget/actual/pull/1420
GoCardlessExternalMsg.js 9.03 KiB
import React, { useEffect, useState, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { pushModal } from 'loot-core/src/client/actions/modals';
import { sendCatch } from 'loot-core/src/platform/client/fetch';
import useGoCardlessStatus from '../../hooks/useGoCardlessStatus';
import AnimatedLoading from '../../icons/AnimatedLoading';
import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple';
import { colors } from '../../style';
import { Error, Warning } from '../alerts';
import Autocomplete from '../autocomplete/Autocomplete';
import Button from '../common/Button';
import ExternalLink from '../common/ExternalLink';
import LinkButton from '../common/LinkButton';
import Menu from '../common/Menu';
import Modal from '../common/Modal';
import Paragraph from '../common/Paragraph';
import View from '../common/View';
import { FormField, FormLabel } from '../forms';
import { Tooltip } from '../tooltips';
import { COUNTRY_OPTIONS } from './countries';
function useAvailableBanks(country) {
const [banks, setBanks] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
async function fetch() {
setIsError(false);
if (!country) {
setBanks([]);
setIsLoading(false);
return;
}
setIsLoading(true);
const { data, error } = await sendCatch('gocardless-get-banks', country);
if (error) {
setIsError(true);
setBanks([]);
} else {
setBanks(data);
}
setIsLoading(false);
}
fetch();
}, [setBanks, setIsLoading, country]);
return {
data: banks,
isLoading,
isError,
};
}
function renderError(error) {
return (
<Error style={{ alignSelf: 'center' }}>
{error === 'timeout'
? 'Timed out. Please try again.'
: 'An error occurred while linking your account, sorry!'}
</Error>
);
}
export default function GoCardlessExternalMsg({
modalProps,
onMoveExternal,
onSuccess,
onClose: originalOnClose,
}) {
const dispatch = useDispatch();
let [waiting, setWaiting] = useState(null);
let [success, setSuccess] = useState(false);
let [institutionId, setInstitutionId] = useState();
let [country, setCountry] = useState();
let [error, setError] = useState(null);
let [isGoCardlessSetupComplete, setIsGoCardlessSetupComplete] =
useState(null);
let [menuOpen, setMenuOpen] = useState(false);
let data = useRef(null);
const {
data: bankOptions,
isLoading: isBankOptionsLoading,
isError: isBankOptionError,
} = useAvailableBanks(country);
const { configured: isConfigured, isLoading: isConfigurationLoading } =
useGoCardlessStatus();
async function onJump() {
setError(null);
setWaiting('browser');
let res = await onMoveExternal({ institutionId });
if (res.error) {
setError(res.error);
setWaiting(null);
return;
}
data.current = res.data;
setWaiting(null);
setSuccess(true);
}
function onClose() {
originalOnClose?.();
modalProps.onClose();
}
async function onContinue() {
setWaiting('accounts');
await onSuccess(data.current);
setWaiting(null);
}
const onGoCardlessInit = () => {
dispatch(
pushModal('gocardless-init', {
onSuccess: () => setIsGoCardlessSetupComplete(true),
}),
);
};
const renderLinkButton = () => {
return (
<View style={{ gap: 10 }}>
<FormField>
<FormLabel title="Choose your country:" htmlFor="country-field" />
<Autocomplete
strict
highlightFirst
disabled={isConfigurationLoading}
suggestions={COUNTRY_OPTIONS}
onSelect={setCountry}
value={country}
inputProps={{ id: 'country-field', placeholder: '(please select)' }}
/>
</FormField>
{isBankOptionError ? (
<Error>
Failed loading available banks: GoCardless access credentials might
be misconfigured. Please{' '}
<LinkButton
onClick={onGoCardlessInit}
style={{ color: colors.b3, display: 'inline' }}
>
set them up
</LinkButton>{' '}
again.
</Error>
) : (
country &&
(isBankOptionsLoading ? (
'Loading banks...'
) : (
<FormField>
<FormLabel title="Choose your bank:" htmlFor="bank-field" />
<Autocomplete
focused
strict
highlightFirst
suggestions={bankOptions}
onSelect={setInstitutionId}
value={institutionId}
inputProps={{
id: 'bank-field',
placeholder: '(please select)',
}}
/>
</FormField>
))
)}
<Warning>
By enabling bank-sync, you will be granting GoCardless (a third party
service) read-only access to your entire account’s transaction
history. This service is not affiliated with Actual in any way. Make
sure you’ve read and understand GoCardless’s{' '}
<ExternalLink to="https://gocardless.com/privacy/">
Privacy Policy
</ExternalLink>{' '}
before proceeding.
</Warning>
<View style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }}>
<Button
type="primary"
style={{
padding: '10px 0',
fontSize: 15,
fontWeight: 600,
flexGrow: 1,
}}
onClick={onJump}
disabled={!institutionId || !country}
>
Link bank in browser →
</Button>
<Button
type="bare"
onClick={() => setMenuOpen(true)}
aria-label="Menu"
>
<DotsHorizontalTriple
width={15}
height={15}
style={{ transform: 'rotateZ(90deg)' }}
/>
{menuOpen && (
<Tooltip
position="bottom-right"
width={200}
style={{ padding: 0 }}
onClose={() => setMenuOpen(false)}
>
<Menu
onMenuSelect={item => {
if (item === 'reconfigure') {
onGoCardlessInit();
}
}}
items={[
{
name: 'reconfigure',
text: 'Set new API secrets',
},
]}
/>
</Tooltip>
)}
</Button>
</View>
</View>
);
};
return (
<Modal
title="Link Your Bank"
{...modalProps}
onClose={onClose}
style={{ flex: 0 }}
>
{() => (
<View>
<Paragraph style={{ fontSize: 15 }}>
To link your bank account, you will be redirected to a new page
where GoCardless will ask to connect to your bank. GoCardless will
not be able to withdraw funds from your accounts.
</Paragraph>
{error && renderError(error)}
{waiting || isConfigurationLoading ? (
<View style={{ alignItems: 'center', marginTop: 15 }}>
<AnimatedLoading
color={colors.n1}
style={{ width: 20, height: 20 }}
/>
<View style={{ marginTop: 10, color: colors.n4 }}>
{isConfigurationLoading
? 'Checking GoCardless configuration..'
: waiting === 'browser'
? 'Waiting on GoCardless...'
: waiting === 'accounts'
? 'Loading accounts...'
: null}
</View>
{waiting === 'browser' && (
<LinkButton onClick={onJump} style={{ marginTop: 10 }}>
(Account linking not opening in a new tab? Click here)
</LinkButton>
)}
</View>
) : success ? (
<Button
type="primary"
style={{
padding: '10px 0',
fontSize: 15,
fontWeight: 600,
marginTop: 10,
backgroundColor: colors.g4,
borderColor: colors.g4,
}}
onClick={onContinue}
>
Success! Click to continue →
</Button>
) : isConfigured || isGoCardlessSetupComplete ? (
renderLinkButton()
) : (
<>
<Paragraph style={{ color: colors.r5 }}>
GoCardless integration has not yet been configured.
</Paragraph>
<Button type="primary" onClick={onGoCardlessInit}>
Configure GoCardless integration
</Button>
</>
)}
</View>
)}
</Modal>
);
}