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

Commit

Permalink
Add a landing page for app purchases
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jameswestman committed Mar 12, 2022
1 parent 86f2125 commit 6382258
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 1 deletion.
27 changes: 27 additions & 0 deletions pages/purchase/finished.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { GetStaticProps } from 'next'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { NextSeo } from 'next-seo'
import Main from '../../src/components/layout/Main'

export default function Purchase({ providers }) {
const { t } = useTranslation()

return (
<Main>
<NextSeo title={t('thank-you-for-your-purchase')} noindex={true} />
<div className='main-container'>
<h1>{t('thank-you-for-your-purchase')}</h1>
{t('safe-to-close-page')}
</div>
</Main>
)
}

export const getStaticProps: GetStaticProps = async ({ locale }) => {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
},
}
}
74 changes: 74 additions & 0 deletions pages/purchase/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { GetStaticProps } from 'next'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
import { ReactElement, useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import Main from '../../src/components/layout/Main'
import Spinner from '../../src/components/Spinner'
import { generateTokens } from '../../src/context/actions'
import { fetchLoginProviders } from '../../src/fetchers'
import { LoginProvider } from '../../src/types/Login'

const PERMITTED_REDIRECTS = [/http:\/\/localhost:\d+/, /http:\/\/127.0.0.1:\d+/];

export default function Purchase({ providers }) {
const { t } = useTranslation();
const [waiting, setWaiting] = useState(false);
const [error, setError] = useState('');
const [token, setToken] = useState('');

const router = useRouter();

useEffect(() => {
if (!router.isReady) return;

let redirect = router.query.return.toString();
if (!PERMITTED_REDIRECTS.some(r => redirect.match(r))) {
setError(t('incorrect-redirect'));
return;
}

let refs = router.query.refs.toString().split(";");
/* We get refs in the form app/<app ID>/<arch>/<branch>, we just want the app ID part */
let appIDs = refs.map(ref => ref.split("/")[1]);

setWaiting(true);
generateTokens(setToken, appIDs)
.catch(err => toast.error(t(err)))
.finally(() => setWaiting(false));
}, [router]);

useEffect(() => {
if (token) {
window.location.href = router.query.return.toString() + "?token=" + token;
}
}, [token]);

let content: ReactElement = null;

if (waiting)
content = <Spinner size={150} />;

return (
<Main>
<NextSeo title={t('purchase-apps-title')} noindex={true} />
<div className='main-container'>
{content}
</div>
</Main>
)
}

// Need available login providers to show options on page
export const getStaticProps: GetStaticProps = async ({ locale }) => {
const providers: LoginProvider[] = await fetchLoginProviders()

return {
props: {
...(await serverSideTranslations(locale, ['common'])),
providers,
}
}
}
4 changes: 4 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@
"install": "Install",
"screenshot": "Screenshot",
"donate": "Donate",
"purchase-apps-title": "Purchase Apps",
"thank-you-for-your-purchase": "Thank you for your purchase!",
"safe-to-close-page": "It is safe to close this page.",
"incorrect-redirect": "Incorrect redirect specified.",
"other-apps-by-developer": "Other apps by {{developer}}",
"loading": "Loading…",
"could-not-find-match-for-search": "Could not find a match for search.",
Expand Down
38 changes: 37 additions & 1 deletion src/context/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ParsedUrlQuery } from "querystring";
import { Dispatch } from "react";
import {
LOGIN_PROVIDERS_URL, LOGOUT_URL, USER_DELETION_URL, USER_INFO_URL
LOGIN_PROVIDERS_URL, LOGOUT_URL, TOKEN_GENERATION_URL, USER_DELETION_URL, USER_INFO_URL
} from "../env";
import { UserStateAction } from "../types/Login";

Expand Down Expand Up @@ -152,3 +152,39 @@ export async function deleteAccount(
throw 'network-error-try-again'
}
}


/**
* Generates a token to download a set of apps.
* @param token Function to set the token when finished
* @param waiting Function to set the async state of the component
* @param error Function for displaying errors (usually component state)
* @param appids A list of app IDs to generate tokens for
*/
export async function generateTokens(
token: Dispatch<string>,
appids: string[],
) {
let res: Response
try {
res = await fetch(TOKEN_GENERATION_URL, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(appids)
})
} catch {
throw 'network-error-try-again';
}

if (res.ok) {
const data = await res.json()
if (data.token) {
token(data.token);
} else {
throw data.message;
}
} else {
throw 'network-error-try-again';
}
}
1 change: 1 addition & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const LOGIN_PROVIDERS_URL: string = `${BASE_URI}/auth/login`
export const USER_INFO_URL: string = `${BASE_URI}/auth/userinfo`
export const LOGOUT_URL: string = `${BASE_URI}/auth/logout`
export const USER_DELETION_URL: string = `${BASE_URI}/auth/deleteuser`
export const TOKEN_GENERATION_URL: string = `${BASE_URI}/generate-download-token`

export const IS_PRODUCTION: boolean =
process.env.NEXT_PUBLIC_IS_PRODUCTION === 'true'
8 changes: 8 additions & 0 deletions src/types/Purchase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Transaction {
token?: string;
appids: string[];
}

export interface TransactionStateAction {
token: string;
}

0 comments on commit 6382258

Please sign in to comment.