Skip to content

Commit 9947080

Browse files
authored
Merge pull request #1582 from aeternity/deep-links
Test and fix deep links
2 parents cc46630 + df517f7 commit 9947080

File tree

4 files changed

+201
-26
lines changed

4 files changed

+201
-26
lines changed

src/pages/aens/AuctionBid.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export default {
132132
this.endsAt = endsAt;
133133
this.highestBid = new BigNumber(highestBid).shiftedBy(-MAGNITUDE);
134134
};
135-
this.$watch(({ internalName }) => internalName, debounce(fetchDetails, 300));
135+
this.$watch(({ internalName }) => internalName, debounce(fetchDetails, 200));
136136
await fetchDetails(this.internalName);
137137
},
138138
methods: {

src/store/plugins/sdk.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ class AccountStore extends AccountBase {
3737
this.#store = store;
3838
}
3939

40-
sign(data, { signal }) {
40+
sign(data, { signal } = {}) {
4141
return this.#store.dispatch('accounts/sign', { data, signal });
4242
}
4343

44-
signTransaction(transaction, { signal }) {
44+
signTransaction(transaction, { signal } = {}) {
4545
return this.#store.dispatch('accounts/signTransaction', { transaction, signal });
4646
}
4747
}
@@ -54,10 +54,8 @@ export default (store) => {
5454
getters: {
5555
node: (_, { currentNetwork }) => new Node(currentNetwork.url, { retryCount: 0 }),
5656
middleware: (_, { currentNetwork }) => new Middleware(currentNetwork.middlewareUrl),
57-
sdk: (_, getters) => new AeSdkMethods({
58-
onNode: getters.node,
59-
onAccount: new AccountStore(getters['accounts/active']?.address, store),
60-
}),
57+
account: (_, getters) => new AccountStore(getters['accounts/active']?.address, store),
58+
sdk: (_, { node, account }) => new AeSdkMethods({ onNode: node, onAccount: account, }),
6159
},
6260
mutations: {
6361
setNetworkId(state, networkId) {

src/store/plugins/ui/urlRequestHandler.js

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,66 @@
11
import { times } from 'lodash-es';
22
import { ensureLoggedIn, mergeEnterHandlers } from '../../../router/utils';
33

4-
const urlRequestMethods = ['address', 'addressAndNetworkUrl', 'sign', 'signTransaction'];
5-
64
export default (store) => {
7-
const handleUrlRequest = async (url) => {
8-
const method = url.path.replace('/', '');
5+
async function ensureActiveAccountAccess(appHost) {
6+
// TODO: extract duplicate code
7+
const accessToAccounts = store.getters.getApp(appHost)?.permissions.accessToAccounts ?? [];
8+
const getActiveAddress = () => store.getters['accounts/active'].address;
9+
if (accessToAccounts.includes(getActiveAddress())) return;
10+
11+
const controller = new AbortController();
12+
const unsubscribe = store.watch(
13+
() => getActiveAddress(),
14+
(address) => accessToAccounts.includes(address) && controller.abort(),
15+
);
16+
try {
17+
await store.dispatch(
18+
'modals/open',
19+
{ name: 'confirmAccountAccess', signal: controller.signal, appHost },
20+
);
21+
store.commit('toggleAccessToAccount', { appHost, accountAddress: getActiveAddress() });
22+
} catch (error) {
23+
if (error.message === 'Modal aborted') return;
24+
throw error;
25+
} finally {
26+
unsubscribe();
27+
}
28+
}
29+
30+
async function ensureRoutedToTransfer() {
31+
await new Promise((resolve) => {
32+
const unsubscribe = store.watch(
33+
(state) => state.route.name,
34+
(name) => {
35+
if (name !== 'transfer') return;
36+
resolve();
37+
unsubscribe();
38+
},
39+
{ immediate: true },
40+
)
41+
});
42+
}
43+
44+
const urlRequestHandlers = {
45+
async address(host) {
46+
await ensureActiveAccountAccess(host);
47+
return store.getters['accounts/active'].address;
48+
},
49+
async sign(_host, data) {
50+
return store.getters.account.sign(data);
51+
},
52+
signTransaction(_host, tx) {
53+
return store.getters.account.signTransaction(tx);
54+
},
55+
};
56+
57+
const handleUrlRequest = async (url, method) => {
958
const callbackUrl = new URL(url.query.callback);
1059
const lastParamIdx = Math.max(
1160
-1,
1261
...Array.from(Object.keys(url.query))
13-
.map((key) => key.startsWith('param') && +key.replace('param', '')),
62+
.filter((key) => key.startsWith('param'))
63+
.map((key) => +key.replace('param', '')),
1464
);
1565
const params = times(
1666
lastParamIdx + 1,
@@ -36,31 +86,30 @@ export default (store) => {
3686
reply({ error: new Error(`Unknown protocol: ${callbackUrl.protocol}`) });
3787
return;
3888
}
39-
if (!urlRequestMethods.includes(method)) {
40-
reply({ error: new Error(`Unknown method: ${method}`) });
41-
return;
42-
}
4389
try {
44-
await store.state.sdk;
45-
reply({
46-
result: await store.state.sdk[method](
47-
...params,
48-
store.state.sdk.getApp(callbackUrl.host),
49-
),
50-
});
90+
await ensureRoutedToTransfer();
91+
reply({ result: await urlRequestHandlers[method](callbackUrl.host, ...params) });
5192
} catch (error) {
5293
reply({ error });
5394
}
5495
};
5596

56-
urlRequestMethods.forEach((methodName) => store.dispatch('router/addRoute', {
97+
// Each 'router/addRoute' call reevaluates beforeEnter, but we need to call `handleUrlRequest` once
98+
let handling = false;
99+
Object.keys(urlRequestHandlers).forEach((methodName) => store.dispatch('router/addRoute', {
57100
name: methodName,
58101
path: `/${methodName}`,
59102
beforeEnter: mergeEnterHandlers(
60103
ensureLoggedIn,
61-
(to, from, next) => {
62-
handleUrlRequest(to);
104+
async (to, _from, next) => {
63105
next(false);
106+
if (handling) return;
107+
handling = true;
108+
try {
109+
await handleUrlRequest(to, methodName);
110+
} finally {
111+
handling = false;
112+
}
64113
},
65114
),
66115
}));

tests/e2e/specs/deep-links.cy.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { verify } from '@aeternity/aepp-sdk-next';
2+
3+
describe('Deep links', () => {
4+
const signer = 'ak_2ujJ8N4GdKapdE2a7aEy4Da3pfPdV7EtJdaA7BUpJ8uqgkQdEB';
5+
6+
describe('Get address', () => {
7+
it('returns', () => {
8+
const url = new URL('http://localhost/address');
9+
url.searchParams.append('callback', 'http://faucet.aepps.com');
10+
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), {
11+
login: 'wallet-empty',
12+
});
13+
const button = cy.get('button').contains('Allow').should('be.visible');
14+
cy.get('.confirm-account-access img').should(($img) => {
15+
expect($img[0].naturalWidth).to.be.greaterThan(0);
16+
});
17+
cy.matchImage();
18+
button.click();
19+
20+
cy.url()
21+
.should('contain', 'faucet.aepps.com')
22+
.then((u) => {
23+
const resultUrl = new URL(u);
24+
const result = JSON.parse(decodeURIComponent(resultUrl.searchParams.get('result')));
25+
expect(result).to.equal(signer);
26+
});
27+
});
28+
29+
it('returns if allowed before', () => {
30+
const url = new URL('http://localhost/address');
31+
url.searchParams.append('callback', 'http://faucet.aepps.com');
32+
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), {
33+
login: 'wallet-empty',
34+
state: {
35+
apps: [
36+
{
37+
host: 'faucet.aepps.com',
38+
permissions: {
39+
accessToAccounts: [signer],
40+
},
41+
},
42+
],
43+
},
44+
});
45+
46+
cy.url()
47+
.should('contain', 'faucet.aepps.com')
48+
.then((u) => {
49+
const resultUrl = new URL(u);
50+
const result = JSON.parse(decodeURIComponent(resultUrl.searchParams.get('result')));
51+
expect(result).to.equal(signer);
52+
});
53+
});
54+
55+
it('redirects if selected correct account', () => {
56+
const url = new URL('http://localhost/address');
57+
const signer2 = 'ak_2Mz7EqTRdmGfns7fvLYfLFLoKyXj8jbfHbfwERfwFZgoZs4Z3T';
58+
url.searchParams.append('callback', 'http://faucet.aepps.com');
59+
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), {
60+
login: 'wallet-empty',
61+
state: {
62+
apps: [
63+
{
64+
host: 'faucet.aepps.com',
65+
permissions: {
66+
accessToAccounts: [signer2],
67+
},
68+
},
69+
],
70+
},
71+
});
72+
cy.get('button').contains('Allow').should('be.visible');
73+
cy.get('.tab-bar .ae-identicon').last().click();
74+
cy.get('.account-switcher-modal .list-item').contains('Account #2').click();
75+
76+
cy.url()
77+
.should('contain', 'faucet.aepps.com')
78+
.then((u) => {
79+
const resultUrl = new URL(u);
80+
const result = JSON.parse(decodeURIComponent(resultUrl.searchParams.get('result')));
81+
expect(result).to.equal(signer2);
82+
});
83+
});
84+
85+
it('cancels', () => {
86+
const url = new URL('http://localhost/address');
87+
url.searchParams.append('callback', 'about:blank');
88+
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), { login: true });
89+
cy.get('button').contains('Deny').click();
90+
cy.url().should('equal', 'about:blank?error=Rejected+by+user');
91+
});
92+
});
93+
94+
describe('Sign raw data', () => {
95+
const data = 'test';
96+
97+
it('signs', () => {
98+
const url = new URL('http://localhost/sign');
99+
url.searchParams.append('param0', `"${data}"`);
100+
url.searchParams.append('callback', 'about:blank');
101+
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), {
102+
login: 'wallet-empty',
103+
});
104+
const button = cy.get('button').contains('Confirm').should('be.visible');
105+
cy.matchImage();
106+
button.click();
107+
108+
cy.url()
109+
.should('contain', 'about:blank')
110+
.then((u) => {
111+
const resultUrl = new URL(u);
112+
const signature = Buffer.from(
113+
Object.values(JSON.parse(decodeURIComponent(resultUrl.searchParams.get('result')))),
114+
);
115+
expect(verify(Buffer.from(data), signature, signer)).to.equal(true);
116+
});
117+
});
118+
119+
it('cancels', () => {
120+
const url = new URL('http://localhost/sign');
121+
url.searchParams.append('param0', `"${data}"`);
122+
url.searchParams.append('callback', 'about:blank');
123+
cy.viewport('iphone-se2').visit(url.href.replace('http://localhost', ''), { login: true });
124+
cy.get('button').contains('Cancel').click();
125+
cy.url().should('equal', 'about:blank?error=Rejected+by+user');
126+
});
127+
});
128+
});

0 commit comments

Comments
 (0)