Skip to content
This repository was archived by the owner on Apr 25, 2022. It is now read-only.

Commit 8af8de7

Browse files
committed
Add a landing page for app purchases
This page doesn't display anything yet, it just asks the backend for a token and then redirects to another page to give flathub-authenticator the token. Finally, flathub-authenticator redirects once again to another landing page to let the user know the transaction is finished.
1 parent 5c0f1e0 commit 8af8de7

File tree

6 files changed

+145
-1
lines changed

6 files changed

+145
-1
lines changed

pages/purchase/finished.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { GetStaticProps } from 'next'
2+
import { useTranslation } from 'next-i18next'
3+
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
4+
import { NextSeo } from 'next-seo'
5+
import Main from '../../src/components/layout/Main'
6+
7+
export default function Purchase({ providers }) {
8+
const { t } = useTranslation()
9+
10+
return (
11+
<Main>
12+
<NextSeo title={t('thank-you-for-your-purchase')} noindex={true} />
13+
<div className='main-container'>
14+
<h1>{t('thank-you-for-your-purchase')}</h1>
15+
{t('safe-to-close-page')}
16+
</div>
17+
</Main>
18+
)
19+
}
20+
21+
export const getStaticProps: GetStaticProps = async ({ locale }) => {
22+
return {
23+
props: {
24+
...(await serverSideTranslations(locale, ['common'])),
25+
},
26+
}
27+
}

pages/purchase/index.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { GetStaticProps } from 'next'
2+
import { useTranslation } from 'next-i18next'
3+
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
4+
import { NextSeo } from 'next-seo'
5+
import { useRouter } from 'next/router'
6+
import { ReactElement, useEffect, useState } from 'react'
7+
import { toast } from 'react-toastify'
8+
import Main from '../../src/components/layout/Main'
9+
import Spinner from '../../src/components/Spinner'
10+
import { generateTokens } from '../../src/context/actions'
11+
12+
const PERMITTED_REDIRECTS = [/http:\/\/localhost:\d+/, /http:\/\/127.0.0.1:\d+/];
13+
14+
export default function Purchase() {
15+
const { t } = useTranslation();
16+
const [waiting, setWaiting] = useState(false);
17+
const [token, setToken] = useState('');
18+
19+
const router = useRouter();
20+
21+
useEffect(() => {
22+
if (!router.isReady) return;
23+
24+
let redirect = router.query.return.toString();
25+
if (!PERMITTED_REDIRECTS.some(r => redirect.match(r))) {
26+
toast.error(t('incorrect-redirect'));
27+
return;
28+
}
29+
30+
let refs = router.query.refs.toString().split(";");
31+
/* We get refs in the form app/<app ID>/<arch>/<branch>, we just want the app ID part */
32+
let appIDs = refs.map(ref => ref.split("/")[1]);
33+
34+
setWaiting(true);
35+
generateTokens(setToken, appIDs)
36+
.catch(err => toast.error(t(err)))
37+
.finally(() => setWaiting(false));
38+
}, [router]);
39+
40+
useEffect(() => {
41+
if (token) {
42+
window.location.href = router.query.return.toString() + "?token=" + token;
43+
}
44+
}, [token]);
45+
46+
let content: ReactElement = null;
47+
48+
if (waiting)
49+
content = <Spinner size={150} />;
50+
51+
return (
52+
<Main>
53+
<NextSeo title={t('purchase-apps-title')} noindex={true} />
54+
<div className='main-container'>
55+
{content}
56+
</div>
57+
</Main>
58+
)
59+
}
60+
61+
// Need available login providers to show options on page
62+
export const getStaticProps: GetStaticProps = async ({ locale }) => {
63+
return {
64+
props: {
65+
...(await serverSideTranslations(locale, ['common'])),
66+
}
67+
}
68+
}

public/locales/en/common.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
"install": "Install",
122122
"screenshot": "Screenshot",
123123
"donate": "Donate",
124+
"purchase-apps-title": "Purchase Apps",
125+
"thank-you-for-your-purchase": "Thank you for your purchase!",
126+
"safe-to-close-page": "It is safe to close this page.",
127+
"incorrect-redirect": "Incorrect redirect specified.",
124128
"other-apps-by-developer": "Other apps by {{developer}}",
125129
"loading": "Loading…",
126130
"could-not-find-match-for-search": "Could not find a match for search.",

src/context/actions.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ParsedUrlQuery } from "querystring";
22
import { Dispatch } from "react";
33
import {
4-
LOGIN_PROVIDERS_URL, LOGOUT_URL, USER_DELETION_URL, USER_INFO_URL
4+
LOGIN_PROVIDERS_URL, LOGOUT_URL, TOKEN_GENERATION_URL, USER_DELETION_URL, USER_INFO_URL
55
} from "../env";
66
import { UserStateAction } from "../types/Login";
77

@@ -152,3 +152,39 @@ export async function deleteAccount(
152152
throw 'network-error-try-again'
153153
}
154154
}
155+
156+
157+
/**
158+
* Generates a token to download a set of apps.
159+
* @param token Function to set the token when finished
160+
* @param waiting Function to set the async state of the component
161+
* @param error Function for displaying errors (usually component state)
162+
* @param appids A list of app IDs to generate tokens for
163+
*/
164+
export async function generateTokens(
165+
token: Dispatch<string>,
166+
appids: string[],
167+
) {
168+
let res: Response
169+
try {
170+
res = await fetch(TOKEN_GENERATION_URL, {
171+
method: 'POST',
172+
credentials: 'include',
173+
headers: { 'Content-Type': 'application/json' },
174+
body: JSON.stringify(appids)
175+
})
176+
} catch {
177+
throw 'network-error-try-again';
178+
}
179+
180+
if (res.ok) {
181+
const data = await res.json()
182+
if (data.token) {
183+
token(data.token);
184+
} else {
185+
throw data.message;
186+
}
187+
} else {
188+
throw 'network-error-try-again';
189+
}
190+
}

src/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const LOGIN_PROVIDERS_URL: string = `${BASE_URI}/auth/login`
3838
export const USER_INFO_URL: string = `${BASE_URI}/auth/userinfo`
3939
export const LOGOUT_URL: string = `${BASE_URI}/auth/logout`
4040
export const USER_DELETION_URL: string = `${BASE_URI}/auth/deleteuser`
41+
export const TOKEN_GENERATION_URL: string = `${BASE_URI}/generate-download-token`
4142

4243
export const IS_PRODUCTION: boolean =
4344
process.env.NEXT_PUBLIC_IS_PRODUCTION === 'true'

src/types/Purchase.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface Transaction {
2+
token?: string;
3+
appids: string[];
4+
}
5+
6+
export interface TransactionStateAction {
7+
token: string;
8+
}

0 commit comments

Comments
 (0)