Skip to content

Commit

Permalink
Merge pull request #1582 from aeternity/deep-links
Browse files Browse the repository at this point in the history
Test and fix deep links
  • Loading branch information
davidyuk authored Feb 18, 2025
2 parents cc46630 + df517f7 commit 9947080
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 26 deletions.
2 changes: 1 addition & 1 deletion src/pages/aens/AuctionBid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default {
this.endsAt = endsAt;
this.highestBid = new BigNumber(highestBid).shiftedBy(-MAGNITUDE);
};
this.$watch(({ internalName }) => internalName, debounce(fetchDetails, 300));
this.$watch(({ internalName }) => internalName, debounce(fetchDetails, 200));
await fetchDetails(this.internalName);
},
methods: {
Expand Down
10 changes: 4 additions & 6 deletions src/store/plugins/sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ class AccountStore extends AccountBase {
this.#store = store;
}

sign(data, { signal }) {
sign(data, { signal } = {}) {
return this.#store.dispatch('accounts/sign', { data, signal });
}

signTransaction(transaction, { signal }) {
signTransaction(transaction, { signal } = {}) {
return this.#store.dispatch('accounts/signTransaction', { transaction, signal });
}
}
Expand All @@ -54,10 +54,8 @@ export default (store) => {
getters: {
node: (_, { currentNetwork }) => new Node(currentNetwork.url, { retryCount: 0 }),
middleware: (_, { currentNetwork }) => new Middleware(currentNetwork.middlewareUrl),
sdk: (_, getters) => new AeSdkMethods({
onNode: getters.node,
onAccount: new AccountStore(getters['accounts/active']?.address, store),
}),
account: (_, getters) => new AccountStore(getters['accounts/active']?.address, store),
sdk: (_, { node, account }) => new AeSdkMethods({ onNode: node, onAccount: account, }),
},
mutations: {
setNetworkId(state, networkId) {
Expand Down
87 changes: 68 additions & 19 deletions src/store/plugins/ui/urlRequestHandler.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,66 @@
import { times } from 'lodash-es';
import { ensureLoggedIn, mergeEnterHandlers } from '../../../router/utils';

const urlRequestMethods = ['address', 'addressAndNetworkUrl', 'sign', 'signTransaction'];

export default (store) => {
const handleUrlRequest = async (url) => {
const method = url.path.replace('/', '');
async function ensureActiveAccountAccess(appHost) {
// TODO: extract duplicate code
const accessToAccounts = store.getters.getApp(appHost)?.permissions.accessToAccounts ?? [];
const getActiveAddress = () => store.getters['accounts/active'].address;
if (accessToAccounts.includes(getActiveAddress())) return;

const controller = new AbortController();
const unsubscribe = store.watch(
() => getActiveAddress(),
(address) => accessToAccounts.includes(address) && controller.abort(),
);
try {
await store.dispatch(
'modals/open',
{ name: 'confirmAccountAccess', signal: controller.signal, appHost },
);
store.commit('toggleAccessToAccount', { appHost, accountAddress: getActiveAddress() });
} catch (error) {
if (error.message === 'Modal aborted') return;
throw error;
} finally {
unsubscribe();
}
}

async function ensureRoutedToTransfer() {
await new Promise((resolve) => {
const unsubscribe = store.watch(
(state) => state.route.name,
(name) => {
if (name !== 'transfer') return;
resolve();
unsubscribe();
},
{ immediate: true },
)
});
}

const urlRequestHandlers = {
async address(host) {
await ensureActiveAccountAccess(host);
return store.getters['accounts/active'].address;
},
async sign(_host, data) {
return store.getters.account.sign(data);
},
signTransaction(_host, tx) {
return store.getters.account.signTransaction(tx);
},
};

const handleUrlRequest = async (url, method) => {
const callbackUrl = new URL(url.query.callback);
const lastParamIdx = Math.max(
-1,
...Array.from(Object.keys(url.query))
.map((key) => key.startsWith('param') && +key.replace('param', '')),
.filter((key) => key.startsWith('param'))
.map((key) => +key.replace('param', '')),
);
const params = times(
lastParamIdx + 1,
Expand All @@ -36,31 +86,30 @@ export default (store) => {
reply({ error: new Error(`Unknown protocol: ${callbackUrl.protocol}`) });
return;
}
if (!urlRequestMethods.includes(method)) {
reply({ error: new Error(`Unknown method: ${method}`) });
return;
}
try {
await store.state.sdk;
reply({
result: await store.state.sdk[method](
...params,
store.state.sdk.getApp(callbackUrl.host),
),
});
await ensureRoutedToTransfer();
reply({ result: await urlRequestHandlers[method](callbackUrl.host, ...params) });
} catch (error) {
reply({ error });
}
};

urlRequestMethods.forEach((methodName) => store.dispatch('router/addRoute', {
// Each 'router/addRoute' call reevaluates beforeEnter, but we need to call `handleUrlRequest` once
let handling = false;
Object.keys(urlRequestHandlers).forEach((methodName) => store.dispatch('router/addRoute', {
name: methodName,
path: `/${methodName}`,
beforeEnter: mergeEnterHandlers(
ensureLoggedIn,
(to, from, next) => {
handleUrlRequest(to);
async (to, _from, next) => {
next(false);
if (handling) return;
handling = true;
try {
await handleUrlRequest(to, methodName);
} finally {
handling = false;
}
},
),
}));
Expand Down
128 changes: 128 additions & 0 deletions tests/e2e/specs/deep-links.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { verify } from '@aeternity/aepp-sdk-next';

describe('Deep links', () => {
const signer = 'ak_2ujJ8N4GdKapdE2a7aEy4Da3pfPdV7EtJdaA7BUpJ8uqgkQdEB';

describe('Get address', () => {
it('returns', () => {
const url = new URL('http://localhost/address');
url.searchParams.append('callback', 'http://faucet.aepps.com');
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), {
login: 'wallet-empty',
});
const button = cy.get('button').contains('Allow').should('be.visible');
cy.get('.confirm-account-access img').should(($img) => {
expect($img[0].naturalWidth).to.be.greaterThan(0);
});
cy.matchImage();
button.click();

cy.url()
.should('contain', 'faucet.aepps.com')
.then((u) => {
const resultUrl = new URL(u);
const result = JSON.parse(decodeURIComponent(resultUrl.searchParams.get('result')));
expect(result).to.equal(signer);
});
});

it('returns if allowed before', () => {
const url = new URL('http://localhost/address');
url.searchParams.append('callback', 'http://faucet.aepps.com');
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), {
login: 'wallet-empty',
state: {
apps: [
{
host: 'faucet.aepps.com',
permissions: {
accessToAccounts: [signer],
},
},
],
},
});

cy.url()
.should('contain', 'faucet.aepps.com')
.then((u) => {
const resultUrl = new URL(u);
const result = JSON.parse(decodeURIComponent(resultUrl.searchParams.get('result')));
expect(result).to.equal(signer);
});
});

it('redirects if selected correct account', () => {
const url = new URL('http://localhost/address');
const signer2 = 'ak_2Mz7EqTRdmGfns7fvLYfLFLoKyXj8jbfHbfwERfwFZgoZs4Z3T';
url.searchParams.append('callback', 'http://faucet.aepps.com');
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), {
login: 'wallet-empty',
state: {
apps: [
{
host: 'faucet.aepps.com',
permissions: {
accessToAccounts: [signer2],
},
},
],
},
});
cy.get('button').contains('Allow').should('be.visible');
cy.get('.tab-bar .ae-identicon').last().click();
cy.get('.account-switcher-modal .list-item').contains('Account #2').click();

cy.url()
.should('contain', 'faucet.aepps.com')
.then((u) => {
const resultUrl = new URL(u);
const result = JSON.parse(decodeURIComponent(resultUrl.searchParams.get('result')));
expect(result).to.equal(signer2);
});
});

it('cancels', () => {
const url = new URL('http://localhost/address');
url.searchParams.append('callback', 'about:blank');
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), { login: true });
cy.get('button').contains('Deny').click();
cy.url().should('equal', 'about:blank?error=Rejected+by+user');
});
});

describe('Sign raw data', () => {
const data = 'test';

it('signs', () => {
const url = new URL('http://localhost/sign');
url.searchParams.append('param0', `"${data}"`);
url.searchParams.append('callback', 'about:blank');
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), {
login: 'wallet-empty',
});
const button = cy.get('button').contains('Confirm').should('be.visible');
cy.matchImage();
button.click();

cy.url()
.should('contain', 'about:blank')
.then((u) => {
const resultUrl = new URL(u);
const signature = Buffer.from(
Object.values(JSON.parse(decodeURIComponent(resultUrl.searchParams.get('result')))),
);
expect(verify(Buffer.from(data), signature, signer)).to.equal(true);
});
});

it('cancels', () => {
const url = new URL('http://localhost/sign');
url.searchParams.append('param0', `"${data}"`);
url.searchParams.append('callback', 'about:blank');
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), { login: true });
cy.get('button').contains('Cancel').click();
cy.url().should('equal', 'about:blank?error=Rejected+by+user');
});
});
});

0 comments on commit 9947080

Please sign in to comment.