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

feat: add a quick way to authorize my current account #1764

Merged
merged 22 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
99cc053
feat: poc to detect authorized accounts
helciofranco Jan 8, 2025
220c3de
feat: add useCurrentAccount
helciofranco Jan 9, 2025
1827973
feat: add useCurrentTab
helciofranco Jan 9, 2025
f3996ba
feat: add useConnection
helciofranco Jan 9, 2025
fd6af5b
chore: remove unused stuff
helciofranco Jan 9, 2025
6231acd
feat: add QuickAccountConnect
helciofranco Jan 9, 2025
5947ffa
fix: prevent to check url while loading env
helciofranco Jan 10, 2025
b58627a
feat: add quick account connection to the top bar
helciofranco Jan 10, 2025
9d43642
feat: redirect user to the edit connection page
helciofranco Jan 10, 2025
ad8a460
feat: add skeleton to the quick account badge
helciofranco Jan 10, 2025
71ebf0d
docs: add changeset
helciofranco Jan 10, 2025
9974a21
feat: add DappAvatar
helciofranco Jan 10, 2025
7a0b55a
fix: prevent to display a broken favicon
helciofranco Jan 10, 2025
b2419dd
feat: display title or favicon as fallback if no connections were found
helciofranco Jan 10, 2025
2ae82b9
feat: add quick account with toast
helciofranco Jan 14, 2025
caa0e83
test: update test locators
helciofranco Jan 14, 2025
e91c345
refactor: remove unused useeffect flow
helciofranco Jan 14, 2025
3731bf1
Merge branch 'master' into hf/feat/authorized-accounts
helciofranco Jan 14, 2025
3d6eb26
fix: add missing improt
helciofranco Jan 14, 2025
adb9922
Merge branch 'master' into hf/feat/authorized-accounts
LuizAsFight Jan 18, 2025
fc61fdd
chore
LuizAsFight Jan 18, 2025
03685b9
chore
LuizAsFight Jan 18, 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
5 changes: 5 additions & 0 deletions .changeset/blue-ducks-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fuels-wallet": minor
---

Add current account connection status to the header
85 changes: 68 additions & 17 deletions packages/app/playwright/e2e/Accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import test, { chromium, expect } from '@playwright/test';
import {
getButtonByText,
getByAriaLabel,
getElementByText,
getInputByName,
hasAriaLabel,
hasText,
Expand Down Expand Up @@ -44,14 +43,26 @@ test.describe('New Accounts', () => {
await visit(page, '/wallet');
await hasText(page, /Assets/i);
await getByAriaLabel(page, 'Accounts').click();
await hasText(page, data.accounts[0].name);
await hasText(page, data.accounts[1].name);
await getByAriaLabel(page, data.accounts[1].name).click({
position: {
x: 10,
y: 10,
},
});
await expect(
page.getByRole('heading', { name: data.accounts[0].name, exact: true })
).toBeVisible();
await expect(
page.getByRole('heading', {
name: data.accounts[1].name,
exact: true,
})
).toBeVisible();
await page
.getByRole('heading', {
name: data.accounts[1].name,
exact: true,
})
.click({
position: {
x: 10,
y: 10,
},
});
await waitUrl(page, '/wallet');
await hasText(page, /Assets/i);
const address = data.accounts[1].address.toString();
Expand All @@ -62,7 +73,12 @@ test.describe('New Accounts', () => {
await visit(page, '/wallet');
await hasText(page, /Assets/i);
await getByAriaLabel(page, 'Accounts').click();
await hasText(page, data.accounts[0].name);
await expect(
page.getByRole('heading', {
name: data.accounts[0].name,
exact: true,
})
).toBeVisible();
await getByAriaLabel(
page,
`Account Actions ${data.accounts[0].name}`
Expand All @@ -82,7 +98,12 @@ test.describe('New Accounts', () => {
await visit(page, '/wallet');
await hasText(page, /Assets/i);
await getByAriaLabel(page, 'Accounts').click();
await hasText(page, data.accounts[0].name);
await expect(
page.getByRole('heading', {
name: data.accounts[0].name,
exact: true,
})
).toBeVisible();
await getByAriaLabel(
page,
`Account Actions ${data.accounts[0].name}`
Expand All @@ -100,8 +121,18 @@ test.describe('New Accounts', () => {
await visit(page, '/wallet');
await hasText(page, /Assets/i);
await getByAriaLabel(page, 'Accounts').click();
await hasText(page, data.accounts[0].name);
await hasText(page, data.accounts[1].name);
await expect(
page.getByRole('heading', {
name: data.accounts[0].name,
exact: true,
})
).toBeVisible();
await expect(
page.getByRole('heading', {
name: data.accounts[1].name,
exact: true,
})
).toBeVisible();
await getByAriaLabel(
page,
`Account Actions ${data.accounts[1].name}`
Expand All @@ -110,7 +141,12 @@ test.describe('New Accounts', () => {
await hasText(page, 'Show hidden accounts');
await page.getByText(data.accounts[1].name).isHidden();
await getByAriaLabel(page, 'Toggle hidden accounts').click();
await hasText(page, data.accounts[1].name);
await expect(
page.getByRole('heading', {
name: data.accounts[1].name,
exact: true,
})
).toBeVisible();
await getByAriaLabel(
page,
`Account Actions ${data.accounts[1].name}`
Expand All @@ -123,8 +159,18 @@ test.describe('New Accounts', () => {
await visit(page, '/wallet');
await hasText(page, /Assets/i);
await getByAriaLabel(page, 'Accounts').click();
await hasText(page, data.accounts[0].name);
await hasText(page, data.accounts[1].name);
await expect(
page.getByRole('heading', {
name: data.accounts[0].name,
exact: true,
})
).toBeVisible();
await expect(
page.getByRole('heading', {
name: data.accounts[1].name,
exact: true,
})
).toBeVisible();
await getByAriaLabel(
page,
`Account Actions ${data.accounts[1].name}`
Expand All @@ -143,7 +189,12 @@ test.describe('New Accounts', () => {
await visit(page, '/wallet');
await hasText(page, /Assets/i);
await getByAriaLabel(page, 'Accounts').click();
await hasText(page, data.accounts[0].name);
await expect(
page.getByRole('heading', {
name: data.accounts[0].name,
exact: true,
})
).toBeVisible();
await getByAriaLabel(
page,
`Account Actions ${data.accounts[0].name}`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { cssObj } from '@fuel-ui/css';
import {
Alert,
Avatar,
Box,
Icon,
IconButton,
Text,
VStack,
toast,
} from '@fuel-ui/react';
import { useEffect, useMemo, useState } from 'react';
import { useCurrentTab } from '~/systems/CRX/hooks/useCurrentTab';
import { useConnection } from '~/systems/DApp/hooks/useConnection';
import { useOrigin } from '~/systems/DApp/hooks/useOrigin';
import { ConnectionService } from '~/systems/DApp/services';
import { useCurrentAccount } from '../../hooks/useCurrentAccount';

enum ConnectionStatus {
CurrentAccount = 'CURRENT_ACCOUNT',
OtherAccount = 'OTHER_ACCOUNT',
NoAccounts = 'NO_ACCOUNTS',
}

export const getDismissKey = (account: string, origin: string) => {
return `quick-account-connect-${account}-${origin}`;
};

export const QuickAccountConnect = () => {
const { account } = useCurrentAccount();
const { currentTab } = useCurrentTab();
const origin = useOrigin({ url: currentTab?.url });
const { connection, fetchConnection } = useConnection({
origin: origin?.full,
});

const [dismissed, setDismissed] = useState(true);

const status = useMemo<ConnectionStatus>(() => {
if (!account || !connection) {
return ConnectionStatus.NoAccounts;
}

if (connection.accounts.includes(account.address)) {
return ConnectionStatus.CurrentAccount;
}

return ConnectionStatus.OtherAccount;
}, [account, connection]);

const onConnect = async () => {
if (!origin || !account) return;
await ConnectionService.addAccountTo({
origin: origin.full,
account: account.address,
});
await fetchConnection();
toast.success(`${account?.name} connected`);
};

const onDismiss = () => {
if (!origin || !account) return;
setDismissed(true);
localStorage.setItem(getDismissKey(account.address, origin.full), 'true');
};

useEffect(() => {
if (!origin || !account) return;
const hasDismissed = localStorage.getItem(
getDismissKey(account.address, origin.full)
);
setDismissed(!!hasDismissed);
}, [account, origin]);

return (
<Box
css={styles.wrapper}
data-open={status === ConnectionStatus.OtherAccount && !dismissed}
>
<Alert status="info" css={styles.alert}>
<Alert.Description as="div">
<Avatar.Generated
size="sm"
hash={account?.address as string}
css={{ boxShadow: '$sm', flexShrink: 0 }}
/>

<VStack gap="$1">
<span>
{account?.name || 'This account'} isn't connected to{' '}
{origin?.short || 'this app'}
</span>

<Text color="scalesBlue10" css={styles.cta} onClick={onConnect}>
Connect account
</Text>
</VStack>

<IconButton
icon={Icon.is('X')}
aria-label="Close"
variant="link"
onPress={onDismiss}
/>
</Alert.Description>
</Alert>
</Box>
);
};

const styles = {
wrapper: cssObj({
position: 'fixed',
paddingLeft: '$4',
paddingRight: '$4',
paddingBottom: '$4',
bottom: 0,
zIndex: '$2',
opacity: 0,
transition: 'opacity 0.2s ease-in-out',
pointerEvents: 'none',

'&[data-open="true"]': {
opacity: 1,
pointerEvents: 'auto',
},
}),
alert: cssObj({
'& .fuel_Alert-icon': {
display: 'none',
},
'& .fuel_Alert-description': {
display: 'flex',
gap: '$2',
alignItems: 'flex-start',
},
}),
cta: cssObj({
'&:hover': {
cursor: 'pointer',
textDecoration: 'underline',
},
}),
};
16 changes: 16 additions & 0 deletions packages/app/src/systems/Account/hooks/useCurrentAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Services, store } from '~/store';
import type { AccountsMachineState } from '../machines';

const selectors = {
account(state: AccountsMachineState) {
return state.context.account;
},
};

export function useCurrentAccount() {
const account = store.useSelector(Services.accounts, selectors.account);

return {
account,
};
}
28 changes: 28 additions & 0 deletions packages/app/src/systems/CRX/hooks/useCurrentTab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useState } from 'react';

interface CurrentTab {
url: string | undefined;
title: string | undefined;
faviconUrl: string | undefined;
}

export function useCurrentTab() {
const [currentTab, setCurrentTab] = useState<CurrentTab | undefined>();

useEffect(() => {
if (!chrome?.tabs) return;

chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const currentTab = tabs[0];
setCurrentTab({
url: currentTab?.url,
title: currentTab?.title,
faviconUrl: currentTab?.favIconUrl,
});
});
}, []);

return {
currentTab,
};
}
26 changes: 26 additions & 0 deletions packages/app/src/systems/DApp/hooks/useConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Connection } from '@fuel-wallet/types';
import { useCallback, useEffect, useState } from 'react';
import { ConnectionService } from '../services';

interface UseConnectionProps {
origin: string | undefined;
}

export const useConnection = ({ origin }: UseConnectionProps) => {
const [connection, setConnection] = useState<Connection | undefined>();

const fetchConnection = useCallback(async () => {
if (!origin) return;
const existingConnection = await ConnectionService.getConnection(origin);
setConnection(existingConnection);
}, [origin]);

useEffect(() => {
fetchConnection();
}, [fetchConnection]);

return {
connection,
fetchConnection,
};
};
Loading
Loading