Skip to content
This repository was archived by the owner on Apr 25, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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'])),
},
}
}
97 changes: 97 additions & 0 deletions pages/purchase/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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 ApplicationCollection from '../../src/components/application/Collection'
import Main from '../../src/components/layout/Main'
import Spinner from '../../src/components/Spinner'
import { generateTokens } from '../../src/context/actions'
import { APP_DETAILS } from '../../src/env'

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

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

const router = useRouter();

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

let redirect = router.query.return.toString();
if (!PERMITTED_REDIRECTS.some(r => r.test(redirect))) {
toast.error(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)
.then(result => {
if (result.token) {
setToken(result.token);
} else if (result.detail == "not_logged_in") {
router.push('/login');
} else if (result.detail == "purchase_necessary") {
Promise.all(
result.missing_appids.map(id => fetch(`${APP_DETAILS(id)}`))
)
.then(responses => Promise.all(responses.map(res => res.json())))
.then(apps => setMissingApps(apps.filter(app => app != null)));
} else {
throw 'network-error-try-again';
}
})
.catch(err => toast.error(t(err)))
.finally(() => setWaiting(false));
}, [router]);

useEffect(() => {
if (token) {
fetch(router.query.return.toString() + "success?token=" + encodeURIComponent(token))
.then(() => {
router.push('/purchase/finished');
})
.catch(() => toast.error(t('app-install-error-try-again')));
}
}, [token]);

let content: ReactElement = null;

if (waiting)
content = <Spinner size={150} />;
else if (missingApps.length) {
content =
<ApplicationCollection
title={t('checkout-apps')}
applications={missingApps}
/>;
}

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 }) => {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
}
}
}
5 changes: 5 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@
"install": "Install",
"screenshot": "Screenshot",
"donate": "Donate",
"purchase-apps-title": "Purchase Apps",
"thank-you-for-your-purchase": "Thank you for your purchase!",
"app-install-error-try-again": "Could not install the app. Try going back to the app store and installing again.",
"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
29 changes: 28 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,30 @@ 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';
}

return await res.json();
}
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;
}