Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pluggy.ai bank sync #4049

Merged
merged 34 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
11752f4
added Pluggy.ai bank sync
lelemm Dec 27, 2024
f5917da
added md
lelemm Dec 27, 2024
57ded0f
code review nits
lelemm Dec 27, 2024
f75eccc
small fixes
lelemm Jan 2, 2025
df0ce3a
Merge remote-tracking branch 'origin/master' into pluggy.ai
lelemm Jan 6, 2025
bbc2b7c
fix syncs
lelemm Jan 6, 2025
f9f0c76
Merge remote-tracking branch 'origin/master' into pluggy.ai
lelemm Jan 8, 2025
6aaf5d2
Merge remote-tracking branch 'origin/master' into pluggy.ai
lelemm Jan 8, 2025
c743066
Merge remote-tracking branch 'origin/master' into pluggy.ai
lelemm Jan 14, 2025
a7d6ff0
refactory after redux changes
lelemm Jan 14, 2025
300f632
changed trunc to round
lelemm Jan 14, 2025
041f8e5
removed debugger
lelemm Jan 15, 2025
df49224
Merge remote-tracking branch 'origin/master' into pluggy.ai
lelemm Jan 23, 2025
77d43aa
linter
lelemm Jan 23, 2025
866a0f7
Merge branch 'pluggy.ai' of https://github.com/lelemm/actual into plu…
lelemm Jan 23, 2025
c8d59eb
linter again
lelemm Jan 23, 2025
3fe1d59
Merge branch 'master' into pluggy.ai
lelemm Jan 23, 2025
5cd4362
Merge branch 'master' into pluggy.ai
matt-fidd Feb 21, 2025
a18598f
sync-server changes
lelemm Dec 27, 2024
65c9893
Merge branch 'master' into pluggy.ai
matt-fidd Feb 21, 2025
c9936d6
types
matt-fidd Feb 21, 2025
9ceb693
Merge remote-tracking branch 'org/master' into pluggy.ai
lelemm Feb 26, 2025
e4038a5
code review
lelemm Feb 26, 2025
e80d332
typecheck
lelemm Feb 26, 2025
92cadb0
fixes
lelemm Feb 26, 2025
14530fe
removed old sync server file
lelemm Feb 26, 2025
5ded866
code review
lelemm Feb 27, 2025
959628f
added more fields to mapping
lelemm Feb 27, 2025
99b15af
linter
lelemm Feb 27, 2025
458f8ea
code review
lelemm Feb 27, 2025
5b34a53
Update packages/sync-server/src/app-pluggyai/app-pluggyai.js
lelemm Feb 27, 2025
330fc08
code review
lelemm Feb 27, 2025
7c3147d
Merge branch 'pluggy.ai' of https://github.com/lelemm/actual into plu…
lelemm Feb 27, 2025
0c88669
Merge branch 'master' into pluggy.ai
matt-fidd Feb 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/desktop-client/src/components/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { OpenIDEnableModal } from './modals/OpenIDEnableModal';
import { OutOfSyncMigrationsModal } from './modals/OutOfSyncMigrationsModal';
import { PasswordEnableModal } from './modals/PasswordEnableModal';
import { PayeeAutocompleteModal } from './modals/PayeeAutocompleteModal';
import { PluggyAiInitialiseModal } from './modals/PluggyAiInitialiseModal';
import { ScheduledTransactionMenuModal } from './modals/ScheduledTransactionMenuModal';
import { SelectLinkedAccountsModal } from './modals/SelectLinkedAccountsModal';
import { SimpleFinInitialiseModal } from './modals/SimpleFinInitialiseModal';
Expand Down Expand Up @@ -222,6 +223,11 @@ export function Modals() {
/>
);

case 'pluggyai-init':
return (
<PluggyAiInitialiseModal key={name} onSuccess={options.onSuccess} />
);

case 'gocardless-external-msg':
return (
<GoCardlessExternalMsgModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ const mappableFields: MappableField[] = [
'remittanceInformationStructured',
'remittanceInformationStructuredArrayString',
'additionalInformation',
'paymentData.payer.accountNumber',
'paymentData.payer.documentNumber.value',
'paymentData.payer.name',
'paymentData.receiver.accountNumber',
'paymentData.receiver.documentNumber.value',
'paymentData.receiver.name',
'merchant.name',
'merchant.businessName',
'merchant.cnpj',
],
},
{
Expand All @@ -73,6 +82,16 @@ const mappableFields: MappableField[] = [
'remittanceInformationStructured',
'remittanceInformationStructuredArrayString',
'additionalInformation',
'category',
'paymentData.payer.accountNumber',
'paymentData.payer.documentNumber.value',
'paymentData.payer.name',
'paymentData.receiver.accountNumber',
'paymentData.receiver.documentNumber.value',
'paymentData.receiver.name',
'merchant.name',
'merchant.businessName',
'merchant.cnpj',
],
},
];
Expand Down
1 change: 1 addition & 0 deletions packages/desktop-client/src/components/banksync/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const useSyncSourceReadable = () => {
const syncSourceReadable: Record<SyncProviders, string> = {
goCardless: 'GoCardless',
simpleFin: 'SimpleFIN',
pluggyai: 'Pluggy.ai',
unlinked: t('Unlinked'),
};

Expand Down
176 changes: 174 additions & 2 deletions packages/desktop-client/src/components/modals/CreateAccountModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { Popover } from '@actual-app/components/popover';
import { Text } from '@actual-app/components/text';
import { View } from '@actual-app/components/view';

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

import { useAuth } from '../../auth/AuthProvider';
import { Permissions } from '../../auth/types';
import { authorizeBank } from '../../gocardless';
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
import { useGoCardlessStatus } from '../../hooks/useGoCardlessStatus';
import { usePluggyAiStatus } from '../../hooks/usePluggyAiStatus';
import { useSimpleFinStatus } from '../../hooks/useSimpleFinStatus';
import { useSyncServerStatus } from '../../hooks/useSyncServerStatus';
import { SvgDotsHorizontalTriple } from '../../icons/v1';
Expand All @@ -34,6 +36,8 @@ type CreateAccountProps = {
export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
const { t } = useTranslation();

const isPluggyAiEnabled = useFeatureFlag('pluggyAiBankSync');

const syncServerStatus = useSyncServerStatus();
const dispatch = useDispatch();
const [isGoCardlessSetupComplete, setIsGoCardlessSetupComplete] = useState<
Expand All @@ -42,6 +46,9 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
const [isSimpleFinSetupComplete, setIsSimpleFinSetupComplete] = useState<
boolean | null
>(null);
const [isPluggyAiSetupComplete, setIsPluggyAiSetupComplete] = useState<
boolean | null
>(null);
const { hasPermission } = useAuth();
const multiuserEnabled = useMultiuserEnabled();

Expand Down Expand Up @@ -118,6 +125,70 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
setLoadingSimpleFinAccounts(false);
};

const onConnectPluggyAi = async () => {
if (!isPluggyAiSetupComplete) {
onPluggyAiInit();
return;
}

try {
const results = await send('pluggyai-accounts');
if (results.error_code) {
throw new Error(results.reason);
} else if ('error' in results) {
throw new Error(results.error);
}

const newAccounts = [];

type NormalizedAccount = {
account_id: string;
name: string;
institution: string;
orgDomain: string | null;
orgId: string;
balance: number;
};

for (const oldAccount of results.accounts) {
const newAccount: NormalizedAccount = {
account_id: oldAccount.id,
name: `${oldAccount.name.trim()} - ${oldAccount.type === 'BANK' ? oldAccount.taxNumber : oldAccount.owner}`,
institution: oldAccount.name,
orgDomain: null,
orgId: oldAccount.id,
balance:
oldAccount.type === 'BANK'
? oldAccount.bankData.automaticallyInvestedBalance +
oldAccount.bankData.closingBalance
: oldAccount.balance,
};

newAccounts.push(newAccount);
}

dispatch(
pushModal('select-linked-accounts', {
accounts: newAccounts,
syncSource: 'pluggyai',
}),
);
} catch (err) {
console.error(err);
addNotification({
type: 'error',
title: t('Error when trying to contact Pluggy.ai'),
message: (err as Error).message,
timeout: 5000,
});
dispatch(
pushModal('pluggyai-init', {
onSuccess: () => setIsPluggyAiSetupComplete(true),
}),
);
}
};

const onGoCardlessInit = () => {
dispatch(
pushModal('gocardless-init', {
Expand All @@ -134,6 +205,14 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
);
};

const onPluggyAiInit = () => {
dispatch(
pushModal('pluggyai-init', {
onSuccess: () => setIsPluggyAiSetupComplete(true),
}),
);
};

const onGoCardlessReset = () => {
send('secret-set', {
name: 'gocardless_secretId',
Expand Down Expand Up @@ -162,6 +241,25 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
});
};

const onPluggyAiReset = () => {
send('secret-set', {
name: 'pluggyai_clientId',
value: null,
}).then(() => {
send('secret-set', {
name: 'pluggyai_clientSecret',
value: null,
}).then(() => {
send('secret-set', {
name: 'pluggyai_itemIds',
value: null,
}).then(() => {
setIsPluggyAiSetupComplete(false);
});
});
});
};

const onCreateLocalAccount = () => {
dispatch(pushModal('add-local-account'));
};
Expand All @@ -176,6 +274,11 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
setIsSimpleFinSetupComplete(configuredSimpleFin);
}, [configuredSimpleFin]);

const { configuredPluggyAi } = usePluggyAiStatus();
useEffect(() => {
setIsPluggyAiSetupComplete(configuredPluggyAi);
}, [configuredPluggyAi]);

let title = t('Add account');
const [loadingSimpleFinAccounts, setLoadingSimpleFinAccounts] =
useState(false);
Expand Down Expand Up @@ -359,9 +462,77 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
hundreds of banks.
</Trans>
</Text>
{isPluggyAiEnabled && (
<>
<View
style={{
flexDirection: 'row',
gap: 10,
alignItems: 'center',
}}
>
<ButtonWithLoading
isDisabled={syncServerStatus !== 'online'}
style={{
padding: '10px 0',
fontSize: 15,
fontWeight: 600,
flex: 1,
}}
onPress={onConnectPluggyAi}
>
{isPluggyAiSetupComplete
? t('Link bank account with Pluggy.ai')
: t('Set up Pluggy.ai for bank sync')}
</ButtonWithLoading>
{isPluggyAiSetupComplete && (
<DialogTrigger>
<Button
variant="bare"
aria-label={t('Pluggy.ai menu')}
>
<SvgDotsHorizontalTriple
width={15}
height={15}
style={{ transform: 'rotateZ(90deg)' }}
/>
</Button>

<Popover>
<Menu
onMenuSelect={item => {
if (item === 'reconfigure') {
onPluggyAiReset();
}
}}
items={[
{
name: 'reconfigure',
text: t('Reset Pluggy.ai credentials'),
},
]}
/>
</Popover>
</DialogTrigger>
)}
</View>
<Text style={{ lineHeight: '1.4em', fontSize: 15 }}>
<Trans>
<strong>
Link a <em>Brazilian</em> bank account
</strong>{' '}
to automatically download transactions. Pluggy.ai
provides reliable, up-to-date information from
hundreds of banks.
</Trans>
</Text>
</>
)}
</>
)}
{(!isGoCardlessSetupComplete || !isSimpleFinSetupComplete) &&
{(!isGoCardlessSetupComplete ||
!isSimpleFinSetupComplete ||
!isPluggyAiSetupComplete) &&
!canSetSecrets && (
<Warning>
<Trans>
Expand All @@ -371,6 +542,7 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) {
{[
isGoCardlessSetupComplete ? '' : 'GoCardless',
isSimpleFinSetupComplete ? '' : 'SimpleFin',
isPluggyAiSetupComplete ? '' : 'Pluggy.ai',
]
.filter(Boolean)
.join(' or ')}
Expand Down
Loading