Skip to content

Commit d5e2030

Browse files
lelemmmatt-fidd
andauthored
Pluggy.ai bank sync (#4049)
* added Pluggy.ai bank sync * added md * code review nits * small fixes * fix syncs * refactory after redux changes * changed trunc to round * removed debugger * linter * linter again * sync-server changes * types * code review * typecheck * fixes * removed old sync server file * code review * added more fields to mapping * linter * code review * Update packages/sync-server/src/app-pluggyai/app-pluggyai.js Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk> * code review --------- Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
1 parent f9b8dde commit d5e2030

25 files changed

+1103
-6
lines changed

packages/desktop-client/src/components/Modals.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { OpenIDEnableModal } from './modals/OpenIDEnableModal';
6161
import { OutOfSyncMigrationsModal } from './modals/OutOfSyncMigrationsModal';
6262
import { PasswordEnableModal } from './modals/PasswordEnableModal';
6363
import { PayeeAutocompleteModal } from './modals/PayeeAutocompleteModal';
64+
import { PluggyAiInitialiseModal } from './modals/PluggyAiInitialiseModal';
6465
import { ScheduledTransactionMenuModal } from './modals/ScheduledTransactionMenuModal';
6566
import { SelectLinkedAccountsModal } from './modals/SelectLinkedAccountsModal';
6667
import { SimpleFinInitialiseModal } from './modals/SimpleFinInitialiseModal';
@@ -222,6 +223,11 @@ export function Modals() {
222223
/>
223224
);
224225

226+
case 'pluggyai-init':
227+
return (
228+
<PluggyAiInitialiseModal key={name} onSuccess={options.onSuccess} />
229+
);
230+
225231
case 'gocardless-external-msg':
226232
return (
227233
<GoCardlessExternalMsgModal

packages/desktop-client/src/components/banksync/EditSyncAccount.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ const mappableFields: MappableField[] = [
6262
'remittanceInformationStructured',
6363
'remittanceInformationStructuredArrayString',
6464
'additionalInformation',
65+
'paymentData.payer.accountNumber',
66+
'paymentData.payer.documentNumber.value',
67+
'paymentData.payer.name',
68+
'paymentData.receiver.accountNumber',
69+
'paymentData.receiver.documentNumber.value',
70+
'paymentData.receiver.name',
71+
'merchant.name',
72+
'merchant.businessName',
73+
'merchant.cnpj',
6574
],
6675
},
6776
{
@@ -73,6 +82,16 @@ const mappableFields: MappableField[] = [
7382
'remittanceInformationStructured',
7483
'remittanceInformationStructuredArrayString',
7584
'additionalInformation',
85+
'category',
86+
'paymentData.payer.accountNumber',
87+
'paymentData.payer.documentNumber.value',
88+
'paymentData.payer.name',
89+
'paymentData.receiver.accountNumber',
90+
'paymentData.receiver.documentNumber.value',
91+
'paymentData.receiver.name',
92+
'merchant.name',
93+
'merchant.businessName',
94+
'merchant.cnpj',
7695
],
7796
},
7897
];

packages/desktop-client/src/components/banksync/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const useSyncSourceReadable = () => {
2828
const syncSourceReadable: Record<SyncProviders, string> = {
2929
goCardless: 'GoCardless',
3030
simpleFin: 'SimpleFIN',
31+
pluggyai: 'Pluggy.ai',
3132
unlinked: t('Unlinked'),
3233
};
3334

packages/desktop-client/src/components/modals/CreateAccountModal.tsx

+174-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import { Popover } from '@actual-app/components/popover';
1010
import { Text } from '@actual-app/components/text';
1111
import { View } from '@actual-app/components/view';
1212

13-
import { pushModal } from 'loot-core/client/actions';
13+
import { addNotification, pushModal } from 'loot-core/client/actions';
1414
import { send } from 'loot-core/platform/client/fetch';
1515

1616
import { useAuth } from '../../auth/AuthProvider';
1717
import { Permissions } from '../../auth/types';
1818
import { authorizeBank } from '../../gocardless';
19+
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
1920
import { useGoCardlessStatus } from '../../hooks/useGoCardlessStatus';
21+
import { usePluggyAiStatus } from '../../hooks/usePluggyAiStatus';
2022
import { useSimpleFinStatus } from '../../hooks/useSimpleFinStatus';
2123
import { useSyncServerStatus } from '../../hooks/useSyncServerStatus';
2224
import { SvgDotsHorizontalTriple } from '../../icons/v1';
@@ -34,6 +36,8 @@ type CreateAccountProps = {
3436
export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
3537
const { t } = useTranslation();
3638

39+
const isPluggyAiEnabled = useFeatureFlag('pluggyAiBankSync');
40+
3741
const syncServerStatus = useSyncServerStatus();
3842
const dispatch = useDispatch();
3943
const [isGoCardlessSetupComplete, setIsGoCardlessSetupComplete] = useState<
@@ -42,6 +46,9 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
4246
const [isSimpleFinSetupComplete, setIsSimpleFinSetupComplete] = useState<
4347
boolean | null
4448
>(null);
49+
const [isPluggyAiSetupComplete, setIsPluggyAiSetupComplete] = useState<
50+
boolean | null
51+
>(null);
4552
const { hasPermission } = useAuth();
4653
const multiuserEnabled = useMultiuserEnabled();
4754

@@ -118,6 +125,70 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
118125
setLoadingSimpleFinAccounts(false);
119126
};
120127

128+
const onConnectPluggyAi = async () => {
129+
if (!isPluggyAiSetupComplete) {
130+
onPluggyAiInit();
131+
return;
132+
}
133+
134+
try {
135+
const results = await send('pluggyai-accounts');
136+
if (results.error_code) {
137+
throw new Error(results.reason);
138+
} else if ('error' in results) {
139+
throw new Error(results.error);
140+
}
141+
142+
const newAccounts = [];
143+
144+
type NormalizedAccount = {
145+
account_id: string;
146+
name: string;
147+
institution: string;
148+
orgDomain: string | null;
149+
orgId: string;
150+
balance: number;
151+
};
152+
153+
for (const oldAccount of results.accounts) {
154+
const newAccount: NormalizedAccount = {
155+
account_id: oldAccount.id,
156+
name: `${oldAccount.name.trim()} - ${oldAccount.type === 'BANK' ? oldAccount.taxNumber : oldAccount.owner}`,
157+
institution: oldAccount.name,
158+
orgDomain: null,
159+
orgId: oldAccount.id,
160+
balance:
161+
oldAccount.type === 'BANK'
162+
? oldAccount.bankData.automaticallyInvestedBalance +
163+
oldAccount.bankData.closingBalance
164+
: oldAccount.balance,
165+
};
166+
167+
newAccounts.push(newAccount);
168+
}
169+
170+
dispatch(
171+
pushModal('select-linked-accounts', {
172+
accounts: newAccounts,
173+
syncSource: 'pluggyai',
174+
}),
175+
);
176+
} catch (err) {
177+
console.error(err);
178+
addNotification({
179+
type: 'error',
180+
title: t('Error when trying to contact Pluggy.ai'),
181+
message: (err as Error).message,
182+
timeout: 5000,
183+
});
184+
dispatch(
185+
pushModal('pluggyai-init', {
186+
onSuccess: () => setIsPluggyAiSetupComplete(true),
187+
}),
188+
);
189+
}
190+
};
191+
121192
const onGoCardlessInit = () => {
122193
dispatch(
123194
pushModal('gocardless-init', {
@@ -134,6 +205,14 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
134205
);
135206
};
136207

208+
const onPluggyAiInit = () => {
209+
dispatch(
210+
pushModal('pluggyai-init', {
211+
onSuccess: () => setIsPluggyAiSetupComplete(true),
212+
}),
213+
);
214+
};
215+
137216
const onGoCardlessReset = () => {
138217
send('secret-set', {
139218
name: 'gocardless_secretId',
@@ -162,6 +241,25 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
162241
});
163242
};
164243

244+
const onPluggyAiReset = () => {
245+
send('secret-set', {
246+
name: 'pluggyai_clientId',
247+
value: null,
248+
}).then(() => {
249+
send('secret-set', {
250+
name: 'pluggyai_clientSecret',
251+
value: null,
252+
}).then(() => {
253+
send('secret-set', {
254+
name: 'pluggyai_itemIds',
255+
value: null,
256+
}).then(() => {
257+
setIsPluggyAiSetupComplete(false);
258+
});
259+
});
260+
});
261+
};
262+
165263
const onCreateLocalAccount = () => {
166264
dispatch(pushModal('add-local-account'));
167265
};
@@ -176,6 +274,11 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
176274
setIsSimpleFinSetupComplete(configuredSimpleFin);
177275
}, [configuredSimpleFin]);
178276

277+
const { configuredPluggyAi } = usePluggyAiStatus();
278+
useEffect(() => {
279+
setIsPluggyAiSetupComplete(configuredPluggyAi);
280+
}, [configuredPluggyAi]);
281+
179282
let title = t('Add account');
180283
const [loadingSimpleFinAccounts, setLoadingSimpleFinAccounts] =
181284
useState(false);
@@ -359,9 +462,77 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
359462
hundreds of banks.
360463
</Trans>
361464
</Text>
465+
{isPluggyAiEnabled && (
466+
<>
467+
<View
468+
style={{
469+
flexDirection: 'row',
470+
gap: 10,
471+
alignItems: 'center',
472+
}}
473+
>
474+
<ButtonWithLoading
475+
isDisabled={syncServerStatus !== 'online'}
476+
style={{
477+
padding: '10px 0',
478+
fontSize: 15,
479+
fontWeight: 600,
480+
flex: 1,
481+
}}
482+
onPress={onConnectPluggyAi}
483+
>
484+
{isPluggyAiSetupComplete
485+
? t('Link bank account with Pluggy.ai')
486+
: t('Set up Pluggy.ai for bank sync')}
487+
</ButtonWithLoading>
488+
{isPluggyAiSetupComplete && (
489+
<DialogTrigger>
490+
<Button
491+
variant="bare"
492+
aria-label={t('Pluggy.ai menu')}
493+
>
494+
<SvgDotsHorizontalTriple
495+
width={15}
496+
height={15}
497+
style={{ transform: 'rotateZ(90deg)' }}
498+
/>
499+
</Button>
500+
501+
<Popover>
502+
<Menu
503+
onMenuSelect={item => {
504+
if (item === 'reconfigure') {
505+
onPluggyAiReset();
506+
}
507+
}}
508+
items={[
509+
{
510+
name: 'reconfigure',
511+
text: t('Reset Pluggy.ai credentials'),
512+
},
513+
]}
514+
/>
515+
</Popover>
516+
</DialogTrigger>
517+
)}
518+
</View>
519+
<Text style={{ lineHeight: '1.4em', fontSize: 15 }}>
520+
<Trans>
521+
<strong>
522+
Link a <em>Brazilian</em> bank account
523+
</strong>{' '}
524+
to automatically download transactions. Pluggy.ai
525+
provides reliable, up-to-date information from
526+
hundreds of banks.
527+
</Trans>
528+
</Text>
529+
</>
530+
)}
362531
</>
363532
)}
364-
{(!isGoCardlessSetupComplete || !isSimpleFinSetupComplete) &&
533+
{(!isGoCardlessSetupComplete ||
534+
!isSimpleFinSetupComplete ||
535+
!isPluggyAiSetupComplete) &&
365536
!canSetSecrets && (
366537
<Warning>
367538
<Trans>
@@ -371,6 +542,7 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
371542
{[
372543
isGoCardlessSetupComplete ? '' : 'GoCardless',
373544
isSimpleFinSetupComplete ? '' : 'SimpleFin',
545+
isPluggyAiSetupComplete ? '' : 'Pluggy.ai',
374546
]
375547
.filter(Boolean)
376548
.join(' or ')}

0 commit comments

Comments
 (0)