Merge pull request #121 from storytold/echelon-bflat-pricing

Echelon bflat pricing
This commit is contained in:
Brandon Thomas authored and GitHub committed 2022-11-16 04:24:28 -05:00
commit c144fa7d4d
18 files changed
+1365 -326

No files matched your search

+3
View File
@@ -1,6 +1,8 @@
# We can't redirect everything until legacy vocodes is dead
/about* /index.html 200
/category* /index.html 200
/checkout_cancel* /index.html 200
/checkout_success* /index.html 200
/clone* /index.html 200
/coming-soon* /index.html 200
/contribute* /index.html 200
@@ -14,6 +16,7 @@
/old* /index.html 200
/patrons* /index.html 200
/pending* /index.html 200
/portal_success* /index.html 200
/profile* /index.html 200
/signup* /index.html 200
/stream* /index.html 200
+16
View File
@@ -461,6 +461,22 @@ class ApiConfig {
//return `wss://obs.storyteller.io/obs/${twitchUsername}`;
}
// =============== Premium ===============
listActiveSubscriptions(): string {
return `${this.getScheme()}://${this.getNewApiHost()}/v1/billing/active_subscriptions`;
}
createStripeCheckoutRedirect(): string {
return `${this.getScheme()}://${this.getNewApiHost()}/v1/stripe/checkout/create_redirect`;
}
createStripePortalRedirect(): string {
return `${this.getScheme()}://${this.getNewApiHost()}/v1/stripe/portal/create_redirect`;
}
// =============== Helper ===============
private getScheme(): string {
return this.useSsl ? "https" : "http";
}
@@ -0,0 +1,58 @@
import { ApiConfig } from "@storyteller/components";
export interface CreateStripeCheckoutRedirectRequest {
internal_plan_key: string,
}
export interface CreateStripeCheckoutRedirectSuccessResponse {
success: boolean,
stripe_checkout_redirect_url: string,
}
export interface CreateStripeCheckoutRedirectErrorResponse {
success: boolean,
}
type CreateStripeCheckoutRedirectResponse = CreateStripeCheckoutRedirectSuccessResponse | CreateStripeCheckoutRedirectErrorResponse;
export function CreateStripeCheckoutRedirectIsSuccess(response: CreateStripeCheckoutRedirectResponse): response is CreateStripeCheckoutRedirectSuccessResponse {
return response?.success === true;
}
export function CreateStripeCheckoutRedirectIsError(response: CreateStripeCheckoutRedirectResponse): response is CreateStripeCheckoutRedirectErrorResponse {
return response?.success === false;
}
export async function CreateStripeCheckoutRedirect(internal_plan_key: string) : Promise<CreateStripeCheckoutRedirectResponse>
{
const endpoint = new ApiConfig().createStripeCheckoutRedirect();
const request : CreateStripeCheckoutRedirectRequest = {
internal_plan_key: internal_plan_key,
};
return await fetch(endpoint, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(request),
})
.then(res => res.json())
.then(res => {
if (!res) {
return { success : false };
}
if (res && 'success' in res) {
return res;
} else {
return { success : false };
}
})
.catch(e => {
return { success : false };
});
}
@@ -0,0 +1,58 @@
import { ApiConfig } from "@storyteller/components";
export interface CreateStripePortalRedirectRequest {
portal_config_id?: string,
}
export interface CreateStripePortalRedirectSuccessResponse {
success: boolean,
stripe_portal_redirect_url: string,
}
export interface CreateStripePortalRedirectErrorResponse {
success: boolean,
}
type CreateStripePortalRedirectResponse = CreateStripePortalRedirectSuccessResponse | CreateStripePortalRedirectErrorResponse;
export function CreateStripePortalRedirectIsSuccess(response: CreateStripePortalRedirectResponse): response is CreateStripePortalRedirectSuccessResponse {
return response?.success === true;
}
export function CreateStripePortalRedirectIsError(response: CreateStripePortalRedirectResponse): response is CreateStripePortalRedirectErrorResponse {
return response?.success === false;
}
export async function CreateStripePortalRedirect(portal_config_id?: string) : Promise<CreateStripePortalRedirectResponse>
{
const endpoint = new ApiConfig().createStripePortalRedirect();
const request : CreateStripePortalRedirectRequest = {
portal_config_id: portal_config_id,
};
return await fetch(endpoint, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(request),
})
.then(res => res.json())
.then(res => {
if (!res) {
return { success : false };
}
if (res && 'success' in res) {
return res;
} else {
return { success : false };
}
})
.catch(e => {
return { success : false };
});
}
@@ -0,0 +1,55 @@
import { ApiConfig } from "@storyteller/components";
export interface ListActiveSubscriptionsSuccessResponse {
success: boolean,
maybe_loyalty_program?: string,
active_subscriptions: ActiveSubscription[],
}
export interface ActiveSubscription {
namespace: string,
product_slug: string,
}
export interface ListActiveSubscriptionsErrorResponse {
success: boolean,
}
type ListActiveSubscriptionsResponse = ListActiveSubscriptionsSuccessResponse | ListActiveSubscriptionsErrorResponse;
export function ListActiveSubscriptionsIsSuccess(response: ListActiveSubscriptionsResponse): response is ListActiveSubscriptionsSuccessResponse {
return response?.success === true;
}
export function ListActiveSubscriptionsIsError(response: ListActiveSubscriptionsResponse): response is ListActiveSubscriptionsErrorResponse {
return response?.success === false;
}
export async function ListActiveSubscriptions() : Promise<ListActiveSubscriptionsResponse>
{
const endpoint = new ApiConfig().listActiveSubscriptions();
return await fetch(endpoint, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(res => res.json())
.then(res => {
if (!res) {
return { success : false };
}
if (res && 'success' in res) {
return res;
} else {
return { success : false };
}
})
.catch(e => {
return { success : false };
});
}
@@ -6,10 +6,14 @@ class ThirdPartyLinks {
// Old FakeYou Discord invite link: https://discord.gg/H72KFXm
public static readonly FAKEYOU_DISCORD = "https://discord.gg/fakeyou";
// A link to Twitter that will open a "Follow" prompt
// A link to FakeYou Twitter that will open a "Follow" prompt
public static readonly FAKEYOU_TWITTER_WITH_FOLLOW_INTENT = "https://twitter.com/intent/follow?screen_name=FakeYouApp";
public static readonly FAKEYOU_PATREON = "https://www.patreon.com/fakeyou";
// A link to echelon's Twitter that will open a "Follow" prompt
// Additional followers may convince VC's that our founder has "clout" (one could hope)
public static readonly ECHELON_TWITTER_WITH_FOLLOW_INTENT = "https://twitter.com/intent/follow?screen_name=echelon";
}
export { ThirdPartyLinks }
@@ -0,0 +1,48 @@
import React from "react";
import { faTwitter } from "@fortawesome/free-brands-svg-icons";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ThirdPartyLinks } from "../constants/ThirdPartyLinks";
interface Props {
hideIcon?: boolean;
iconBeforeText?: boolean;
title?: string;
children?: string | Element | Array<any>; // Optional link text, child elements, etc.
}
function EchelonTwitterLink(props: Props) {
let linkBody =
props.children === undefined ? <>echelon</> : <>{props.children}</>;
const showIcon = !(props.hideIcon ? true : false);
const iconBeforeText = props.iconBeforeText ? true : false;
const linkTitle = props.title ? props.title : "echelon's Twitter";
if (showIcon) {
linkBody = iconBeforeText ? (
<>
<FontAwesomeIcon icon={faTwitter as IconProp} title={linkTitle} />{" "}
{linkBody}
</>
) : (
<>
{linkBody}{" "}
<FontAwesomeIcon icon={faTwitter as IconProp} title={linkTitle} />
</>
);
}
return (
<a
href={ThirdPartyLinks.ECHELON_TWITTER_WITH_FOLLOW_INTENT}
target="_blank"
rel="noopener noreferrer"
>
{linkBody}
</a>
);
}
export { EchelonTwitterLink };
@@ -0,0 +1,38 @@
import { ListActiveSubscriptions, ListActiveSubscriptionsIsSuccess, ListActiveSubscriptionsSuccessResponse } from "../api/premium/ListActiveSubscriptions";
export class SessionSubscriptionsWrapper {
listActiveSubscriptionResponse?: ListActiveSubscriptionsSuccessResponse;
private constructor(listActiveSubscriptionsSuccessResponse?: ListActiveSubscriptionsSuccessResponse) {
if (listActiveSubscriptionsSuccessResponse !== undefined) {
this.listActiveSubscriptionResponse = listActiveSubscriptionsSuccessResponse;
}
}
public static async lookupActiveSubscriptions() : Promise<SessionSubscriptionsWrapper> {
let response = await ListActiveSubscriptions();
if (ListActiveSubscriptionsIsSuccess(response)) {
return new SessionSubscriptionsWrapper(response);
} else {
return SessionSubscriptionsWrapper.emptySubscriptions();
}
}
public static emptySubscriptions() : SessionSubscriptionsWrapper {
return new SessionSubscriptionsWrapper();
}
public hasFreeOrPaidPremiumFeatures() : boolean {
return this.hasLoyaltyProgram() || this.hasPaidFeatures();
}
public hasLoyaltyProgram() : boolean {
return !!this.listActiveSubscriptionResponse?.maybe_loyalty_program;
}
public hasPaidFeatures() : boolean {
const subs = this.listActiveSubscriptionResponse?.active_subscriptions || [];
return subs.length > 0;
}
}
+15 -1
View File
@@ -10,6 +10,7 @@ import { Language } from "@storyteller/components/src/i18n/Language";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { NewVocodesContainer } from "./v2/view/NewVocodesContainer";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import {
TtsInferenceJob,
TtsInferenceJobStateResponsePayload,
@@ -92,6 +93,7 @@ interface State {
// Rollout of vocodes 2.0
enableAlpha: boolean;
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
// Show flash notice of vocodes name change
isShowingVocodesNotice: boolean;
@@ -154,7 +156,9 @@ class App extends React.Component<Props, State> {
this.state = {
enableAlpha: enableAlpha,
migrationMode: migrationMode,
sessionWrapper: SessionWrapper.emptySession(),
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper.emptySubscriptions(),
isShowingVocodesNotice: props.flashVocodesNotice,
@@ -194,11 +198,13 @@ class App extends React.Component<Props, State> {
}
async componentDidMount() {
await this.querySession();
await this.queryLanguage();
await this.querySession();
await this.querySessionSubscriptions();
setInterval(async () => {
await this.querySession();
await this.querySessionSubscriptions();
}, 60000);
// TODO: Use websockets, this is dumb
setInterval(() => {
@@ -213,6 +219,13 @@ class App extends React.Component<Props, State> {
});
};
querySessionSubscriptions = async () => {
const sessionSubscriptionsWrapper = await SessionSubscriptionsWrapper.lookupActiveSubscriptions();
this.setState({
sessionSubscriptionsWrapper: sessionSubscriptionsWrapper,
});
};
queryLanguage = async () => {
let locale = await DetectLocale();
if (DetectLocaleIsOk(locale)) {
@@ -645,6 +658,7 @@ class App extends React.Component<Props, State> {
<NewVocodesContainer
sessionWrapper={this.state.sessionWrapper}
querySessionAction={this.querySession}
sessionSubscriptionsWrapper={this.state.sessionSubscriptionsWrapper}
isShowingVocodesNotice={this.state.isShowingVocodesNotice}
clearVocodesNotice={this.clearVocodesNotice}
isShowingLangaugeNotice={this.state.isShowingLanguageNotice}
+174 -54
View File
@@ -1,61 +1,181 @@
const FAKEYOU_PRICES = {
'free': {
tier: 'free',
//Starter Tier
starter: {
internal_plan_key: null,
tier: "Starter",
price: 0,
features: {
'Extended Audio': false,
'mp3': false,
'Priority Processing': false,
'Commercial Voices': false
}
tts: {
title: "TTS",
features: ["Unlimited generation", "Up to 12 seconds audio"],
},
// vcweb: {
// title: "VC Web",
// features: ["Up to 12 seconds audio"],
// },
// vcapp: {
// title: "VC App",
// features: [
// "5 model downloads",
// "Up to 12 secs prerecorded",
// "Up to 2 mins realtime",
// ],
// },
w2l: {
title: "Wav2Lip",
features: ["Up to 12 seconds video"],
},
priority: {
title: "Processing Priority",
features: ["Level 1"],
},
},
'basic': {
tier: 'basic',
price: 5,
features: {
'Extended Audio': true,
'mp3': false,
'Priority Processing': false,
'Commercial Voices': false
}
},
'pro': {
tier: 'pro',
price: 30,
features: {
'Extended Audio': true,
'mp3': true,
'Priority Processing': true,
'Commercial Voices': true
}
}
}
const STORYTELLER_PRICES = {
'free': {
tier: 'free',
price: 0,
features: {
'Priority Processing': false,
'Stream Labs': false
}
},
//Basic Tier
basic: {
tier: 'basic',
price: 5,
features: {
'Priority Processing': false,
'Stream Labs': false
}
internal_plan_key: null, // TODO
tier: "Basic",
price: 3,
tts: {
title: "TTS",
features: ["Unlimited generation", "Up to 20 seconds audio"],
},
// vcweb: {
// title: "VC Web",
// features: ["Up to 20 seconds audio", "Push to play"],
// },
// vcapp: {
// title: "VC App",
// features: [
// "7 model downloads",
// "Up to 20 secs prerecorded",
// "Up to 5 mins realtime",
// ],
// },
w2l: {
title: "Wav2Lip",
features: ["Up to 30 seconds video"],
},
priority: {
title: "Processing Priority",
features: ["Level 10"],
},
},
pro: {
tier: 'premium',
price: 50,
features: {
'Priority Processing': true,
"Stream Labs": true
}
}
}
export { FAKEYOU_PRICES, STORYTELLER_PRICES }
//Plus Tier
plus: {
internal_plan_key: "development_fakeyou_plus",
tier: "Plus",
price: 7,
tts: {
title: "TTS",
features: ["Unlimited generation", "Up to 30 seconds audio"],
},
// vcweb: {
// title: "VC Web",
// features: ["Up to 30 seconds audio", "Push to play"],
// },
// vcapp: {
// title: "VC App",
// features: [
// "10 model downloads",
// "Up to 30 secs prerecorded",
// "Up to 7 mins realtime",
// ],
// },
w2l: {
title: "Wav2Lip",
features: ["Up to 1 minute video"],
},
priority: {
title: "Processing Priority",
features: ["Level 20"],
},
},
//Pro Tier
pro: {
internal_plan_key: "development_fakeyou_pro",
tier: "Pro",
price: 15,
tts: {
title: "TTS",
features: [
"Unlimited generation",
"Up to 1 minute audio",
//"Generate MP3 file",
"Upload private models",
],
},
// vcweb: {
// title: "VC Web",
// features: ["Up to 30 seconds audio", "Push to play", "Generate MP3 file"],
// },
// vcapp: {
// title: "VC App",
// features: [
// "20 model downloads",
// "Up to 5 mins prerecorded",
// "Up to 15 mins realtime",
// ],
// },
w2l: {
title: "Wav2Lip",
features: ["Up to 2 minutes video"],
},
priority: {
title: "Processing Priority",
features: ["Level 30"],
},
api: {
title: "API Access",
features: ["Full API access"],
},
},
//Elite Tier
elite: {
internal_plan_key: "development_fakeyou_elite",
tier: "Elite",
price: 25,
tts: {
title: "TTS",
features: [
"Unlimited generation",
"Up to 5 minutes audio",
//"Generate MP3 file",
"Upload private models",
"Share private models",
],
},
// vcweb: {
// title: "VC Web",
// features: ["Up to 7 minutes audio", "Push to play", "Generate MP3 file"],
// },
// vcapp: {
// title: "VC App",
// features: [
// "Unlimited models",
// "Unlimited prerecorded",
// "Unlimited realtime",
// ],
// },
w2l: {
title: "Wav2Lip",
features: ["Up to 2 minutes video"],
},
priority: {
title: "Processing Priority",
features: ["Level 40"],
},
api: {
title: "API Access",
features: ["Full API access"],
},
commercial: {
title: "Commercial Voices",
features: ["FakeYou commercial voices"],
},
},
};
export { FAKEYOU_PRICES };
+13 -1
View File
@@ -390,11 +390,23 @@ styles with important tags:
.panel-border {
box-sizing: border-box;
box-shadow: inset 0px 0px 0px 4px lighten($primary, 5%);
box-shadow: inset 0px 0px 0px 3px $primary;
}
ul.pricing-list {
list-style: none;
padding: 0;
li {
font-size: 14px;
}
.small-text {
font-size: 12px;
font-weight: 400;
opacity: 0.8;
color: lighten($primary, 10%);
}
}
ul li {
@@ -16,6 +16,7 @@ import {
faFacebook,
} from "@fortawesome/free-brands-svg-icons";
import { ThirdPartyLinks } from "@storyteller/components/src/constants/ThirdPartyLinks";
import { EchelonTwitterLink } from "@storyteller/components/src/elements/EchelonTwitterLink";
interface Props {
sessionWrapper: SessionWrapper;
@@ -132,7 +133,7 @@ function NewFooterNavFc(props: Props) {
<div className="d-flex flex-column align-items-center gap-4">
<div>
<Trans i18nKey="coreUi.footerNav.builtBy">
Built by <Link to="/profile/echelon">echelon</Link> in Atlanta.
Built by <EchelonTwitterLink hideIcon={true} /> in Atlanta.
</Trans>
</div>
@@ -11,6 +11,7 @@ import { NewTopNavFc } from "./NewTopNavFc";
import { ProfileEditFc } from "./profile/profile_edit/ProfileEditFc";
import { ProfileFc } from "./profile/profile_view/ProfileFc";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { SignupPage } from "./signup/SignupPage";
import { Switch, Route } from "react-router-dom";
import { TermsPage } from "./about/terms_page/TermsPage";
@@ -56,11 +57,17 @@ import { Language } from "@storyteller/components/src/i18n/Language";
import { VoiceCloneRequestPage } from "./clone_voice_requests/VoiceCloneRequestPage";
import { VocodesPage } from "./vocodes/VocodesPage";
import { UploadVocoderPage } from "./upload/UploadVocoderPage";
import { PricingPage } from "./premium/PricingPage";
import { CheckoutSuccessPage } from "./premium/CheckoutSuccessPage";
import { CheckoutCancelPage } from "./premium/CheckoutCancelPage";
import { PortalSuccessPage } from "./premium/PortalSuccessPage";
interface Props {
sessionWrapper: SessionWrapper;
querySessionAction: () => void;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
isShowingVocodesNotice: boolean;
clearVocodesNotice: () => void;
@@ -174,14 +181,31 @@ class NewVocodesContainer extends React.Component<Props, State> {
/>
</Route>
{/*
<Route path="/pricing">
<Route path="/pricing" exact={true}>
<PricingPage
querySessionCallback={() => { }}
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={this.props.sessionSubscriptionsWrapper}
/>
</Route>
<Route path="/checkout_success" exact={true}>
<CheckoutSuccessPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/checkout_cancel" exact={true}>
<CheckoutCancelPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/portal_success" exact={true}>
<PortalSuccessPage
querySessionCallback={() => {}}
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
*/}
<Route path="/tts/result/:token/edit">
<TtsResultEditPage sessionWrapper={this.props.sessionWrapper} />
@@ -0,0 +1,80 @@
import React from "react";
import { Link } from "react-router-dom";
import { motion } from "framer-motion";
import { container, item, image } from "../../../data/animation";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
interface Props {
sessionWrapper: SessionWrapper;
querySessionCallback: () => void;
}
function CheckoutCancelPage(props: Props) {
return (
<motion.div initial="hidden" animate="visible" variants={container}>
<div className="container mb-4">
<div className="row gx-3 flex-lg-row-reverse align-items-center">
<div className="col-lg-6">
<div className="d-flex justify-content-center">
<motion.img
src="/mascot/kitsune_pose7.webp"
className="img-fluid"
width="516"
height="444"
alt="FakeYou Kitsune Mascot!"
variants={image}
/>
</div>
</div>
<div className="col-lg-6 px-md-2 ps-lg-5 ps-xl-2">
<div className="text-center text-lg-start">
<div>
<motion.h1
className="display-5 fw-bold lh-1 mb-4"
variants={item}
>
Oh no!
</motion.h1>
</div>
<div>
<motion.p className="lead" variants={item}>
It's okay if you don't want to support FakeYou right now, but we'd
appreciate it if you reconsider us in the future.
</motion.p>
<br />
<motion.p className="lead" variants={item}>
Paid plans go directly to helping us afford more GPUs, engineering,
and research talent.
</motion.p>
<br />
<motion.p className="lead" variants={item}>
We're trying to build a film and music production system that you
can use to make any content you dream up. Please consider supporting
us monetarily.
</motion.p>
</div>
</div>
</div>
<div className="col-lg-6">
<div className="d-flex justify-content-center">
<Link to="/" className="btn btn-secondary fs-6">
Back to FakeYou
</Link>
</div>
</div>
<div className="col-lg-6">
<div className="d-flex justify-content-center">
<Link to="/pricing" className="btn btn-primary fs-6">
Select a Premium Plan
</Link>
</div>
</div>
</div>
</div>
</motion.div>
);
}
export { CheckoutCancelPage };
@@ -0,0 +1,73 @@
import React from "react";
import { Link } from "react-router-dom";
import { motion } from "framer-motion";
import { container, item, image, panel } from "../../../data/animation";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
interface Props {
sessionWrapper: SessionWrapper;
querySessionCallback: () => void;
}
function CheckoutSuccessPage(props: Props) {
return (
<motion.div initial="hidden" animate="visible" variants={container}>
<div className="container mb-4">
<div className="row gx-3 flex-lg-row-reverse align-items-center">
<div className="col-lg-6">
<div className="d-flex justify-content-center">
<motion.img
src="/mascot/kitsune_pose3.webp"
className="img-fluid"
width="516"
height="444"
alt="FakeYou Kitsune Mascot!"
variants={image}
/>
</div>
</div>
<div className="col-lg-6 px-md-2 ps-lg-5 ps-xl-2">
<div className="text-center text-lg-start">
<div>
<motion.h1
className="display-5 fw-bold lh-1 mb-4"
variants={item}
>
Thank you!
</motion.h1>
</div>
<br />
<div>
<motion.p className="lead" variants={item}>
We're grateful you're helping FakeYou build the future creative
suite. We're building tools for small creators and have an eye
on building a full AI-powered Hollywood and music production
studio.
</motion.p>
<br />
<motion.p className="lead" variants={item}>
Your support helps us immensely!
</motion.p>
<br />
<motion.p className="lead" variants={item}>
Please give it a few minutes for your purchase to take effect.
</motion.p>
</div>
</div>
</div>
<div className="col-lg-12">
<div className="d-flex justify-content-center">
<Link to="/" className="btn btn-primary fs-6">
Back to FakeYou
</Link>
</div>
</div>
</div>
</div>
</motion.div>
);
}
export { CheckoutSuccessPage };
@@ -0,0 +1,63 @@
import React from "react";
import { Link } from "react-router-dom";
import { motion } from "framer-motion";
import { container, item, image, panel } from "../../../data/animation";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
interface Props {
sessionWrapper: SessionWrapper;
querySessionCallback: () => void;
}
function PortalSuccessPage(props: Props) {
return (
<motion.div initial="hidden" animate="visible" variants={container}>
<div className="container mb-4">
<div className="row gx-3 flex-lg-row-reverse align-items-center">
<div className="col-lg-6">
<div className="d-flex justify-content-center">
<motion.img
src="/mascot/kitsune_pose3.webp"
className="img-fluid"
width="516"
height="444"
alt="FakeYou Kitsune Mascot!"
variants={image}
/>
</div>
</div>
<div className="col-lg-6 px-md-2 ps-lg-5 ps-xl-2">
<div className="text-center text-lg-start">
<div>
<motion.h1
className="display-5 fw-bold lh-1 mb-4"
variants={item}
>
Thank You!
</motion.h1>
</div>
<div>
<motion.p className="lead" variants={item}>
Your support means a lot to us.
</motion.p>
<motion.p className="lead" variants={item}>
Please continue to enjoy FakeYou!
</motion.p>
</div>
</div>
</div>
<div className="col-lg-12">
<div className="d-flex justify-content-center">
<Link to="/" className="btn btn-primary fs-6">
Back to FakeYou
</Link>
</div>
</div>
</div>
</div>
</motion.div>
);
}
export { PortalSuccessPage };
@@ -0,0 +1,636 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "react-router-dom";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { FAKEYOU_PRICES as FYP } from "@storyteller/fakeyou/src/data/PriceTiers";
import {
CreateStripeCheckoutRedirect,
CreateStripeCheckoutRedirectIsError,
CreateStripeCheckoutRedirectIsSuccess,
} from "@storyteller/components/src/api/premium/CreateStripeCheckoutRedirect";
import {
CreateStripePortalRedirect,
CreateStripePortalRedirectIsError,
CreateStripePortalRedirectIsSuccess,
} from "@storyteller/components/src/api/premium/CreateStripePortalRedirect";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
function PricingPage(props: Props) {
const beginStripeCheckoutFlow = async (internal_plan_key: string) : Promise<boolean> => {
const response = await CreateStripeCheckoutRedirect(internal_plan_key);
if (CreateStripeCheckoutRedirectIsSuccess(response)) {
window.location.href = response.stripe_checkout_redirect_url;
} else if (CreateStripeCheckoutRedirectIsError(response)) {
// TODO
}
return false;
}
const beginStripePortalFlow = async () : Promise<boolean> => {
const response = await CreateStripePortalRedirect();
if (CreateStripePortalRedirectIsSuccess(response)) {
window.location.href = response.stripe_portal_redirect_url;
} else if (CreateStripePortalRedirectIsError(response)) {
// TODO
}
return false;
}
const beginStripeFlow = async (internal_plan_key: string) : Promise<boolean> => {
if (props.sessionSubscriptionsWrapper.hasPaidFeatures()) {
return await beginStripePortalFlow();
} else {
return await beginStripeCheckoutFlow(internal_plan_key);
}
};
const hasPaidPremium = props.sessionSubscriptionsWrapper.hasPaidFeatures();
let freeButtonText = 'Use for free';
let plusButtonText = 'Buy Plus';
let proButtonText = 'Buy Pro';
let eliteButtonText = 'Buy Elite';
if (hasPaidPremium) {
freeButtonText = 'Unsubscribe';
plusButtonText = 'Switch to Plus';
proButtonText = 'Switch to Pro';
eliteButtonText = 'Switch to Elite';
}
return (
<div>
<div className="container py-5 text-center">
<h1 className="display-5 fw-bold">Pricing</h1>
<p className="fs-5">
By purchasing FakeYou premium, you help us build more!
</p>
</div>
<div className="container mt-3 mb-5">
<div className="row gx-3 gy-4">
{/* Starter Tier */}
<div className="col-12 col-sm-6 col-lg-3">
<div className="rounded panel p-4 h-100">
<h2 className="text-center my-2 fw-bold mb-4">
{FYP.starter.tier}
</h2>
<Link to="/" className="btn btn-secondary w-100 fs-6">
{freeButtonText}
</Link>
<h2 className="display-5 fw-bold text-center my-5">
${FYP.starter.price}
<span className="fs-5 opacity-75 fw-normal"> /month</span>
</h2>
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">
{FYP.starter.priority.title}
</li>
{FYP.starter.priority.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
{" "}
<span className="small-text">(registered users)</span>
</li>
);
})}
<li className="fw-semibold">{FYP.starter.tts.title}</li>
{FYP.starter.tts.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
{/*<li className="fw-semibold">{FYP.starter.vcweb.title}</li>
{FYP.starter.vcweb.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">
{FYP.starter.vcapp.title}{" "}
<span className="small-text">(registered users)</span>
</li>
{FYP.starter.vcapp.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}*/}
<li className="fw-semibold">{FYP.starter.w2l.title}</li>
{FYP.starter.w2l.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
</ul>
</div>
</div>
{/* Basic Tier (For Latin American countries) */}
{/* <div className="col-12 col-sm-6 col-lg-3">
<div className="rounded panel p-4 h-100">
<h2 className="text-center my-2 fw-bold mb-4">
{FYP.basic.tier}
</h2>
<Link to="/" className="btn btn-primary w-100 fs-6">
Buy {FYP.basic.tier}
</Link>
<h2 className="display-5 fw-bold text-center my-5">
${FYP.basic.price}
<span className="fs-5 opacity-75 fw-normal"> /month</span>
</h2>
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">{FYP.basic.tts.title}</li>
{FYP.basic.tts.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.basic.vcweb.title}</li>
{FYP.basic.vcweb.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.basic.vcapp.title}</li>
{FYP.basic.vcapp.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.basic.w2l.title}</li>
{FYP.basic.w2l.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.basic.priority.title}</li>
{FYP.basic.priority.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
</ul>
</div>
</div> */}
{/* Plus Tier */}
<div className="col-12 col-sm-6 col-lg-3">
<div className="rounded panel p-4 h-100">
<h2 className="text-center my-2 fw-bold mb-4">{FYP.plus.tier}</h2>
<button
onClick={() => beginStripeFlow(FYP.plus.internal_plan_key)}
className="btn btn-primary w-100 fs-6"
>
{plusButtonText}
</button>
<h2 className="display-5 fw-bold text-center my-5">
${FYP.plus.price}
<span className="fs-5 opacity-75 fw-normal"> /month</span>
</h2>
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">{FYP.plus.priority.title}</li>
{FYP.plus.priority.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.plus.tts.title}</li>
{FYP.plus.tts.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
{/*<li className="fw-semibold">{FYP.plus.vcweb.title}</li>
{FYP.plus.vcweb.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.plus.vcapp.title}</li>
{FYP.plus.vcapp.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}*/}
<li className="fw-semibold">{FYP.plus.w2l.title}</li>
{FYP.plus.w2l.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
</ul>
</div>
</div>
{/* Pro Tier */}
<div className="col-12 col-sm-6 col-lg-3">
<div className="rounded panel p-4 h-100">
<h2 className="text-center my-2 fw-bold mb-4">{FYP.pro.tier}</h2>
<button
onClick={() => beginStripeFlow(FYP.pro.internal_plan_key)}
className="btn btn-primary w-100 fs-6"
>
{proButtonText}
</button>
<h2 className="display-5 fw-bold text-center my-5">
${FYP.pro.price}
<span className="fs-5 opacity-75 fw-normal"> /month</span>
</h2>
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">{FYP.pro.priority.title}</li>
{FYP.pro.priority.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.pro.tts.title}</li>
{FYP.pro.tts.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
{/*<li className="fw-semibold">{FYP.pro.vcweb.title}</li>
{FYP.pro.vcweb.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.pro.vcapp.title}</li>
{FYP.pro.vcapp.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}*/}
<li className="fw-semibold">{FYP.pro.w2l.title}</li>
{FYP.pro.w2l.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
{/*<li className="fw-semibold">{FYP.pro.api.title}</li>
{FYP.pro.api.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}*/}
</ul>
</div>
</div>
{/* Elite Tier */}
<div className="col-12 col-sm-6 col-lg-3">
<div className="rounded panel p-4 h-100">
<h2 className="text-center my-2 fw-bold mb-4">
{FYP.elite.tier}
</h2>
<button
onClick={() => beginStripeFlow(FYP.elite.internal_plan_key)}
className="btn btn-primary w-100 fs-6"
>
{eliteButtonText}
</button>
<h2 className="display-5 fw-bold text-center my-5">
${FYP.elite.price}
<span className="fs-5 opacity-75 fw-normal"> /month</span>
</h2>
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">{FYP.elite.priority.title}</li>
{FYP.elite.priority.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.elite.tts.title}</li>
{FYP.elite.tts.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
{/*<li className="fw-semibold">{FYP.elite.vcweb.title}</li>
{FYP.elite.vcweb.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
<li className="fw-semibold">{FYP.elite.vcapp.title}</li>
{FYP.elite.vcapp.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}*/}
<li className="fw-semibold">{FYP.elite.w2l.title}</li>
{FYP.elite.w2l.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
{/*<li className="fw-semibold">{FYP.elite.api.title}</li>
{FYP.elite.api.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}*/}
<li className="fw-semibold">{FYP.elite.commercial.title}</li>
{FYP.elite.commercial.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
</ul>
</div>
</div>
</div>
{/* Starter Tier (to show for Latin American countries) */}
{/* <div className="w-100 mt-4">
<div className="rounded panel p-4 h-100">
<div className="d-flex w-100">
<h2 className="my-2 fw-bold mb-4 flex-grow-1">
{FYP.starter.tier}
</h2>
<h2 className="display-6 fw-bold text-right">
${FYP.starter.price}
<span className="fs-5 opacity-75 fw-normal"> /month</span>
</h2>
</div>
<Link to="/" className="btn btn-secondary w-100 fs-6">
Use for free
</Link>
<div className="row mt-5">
<div className="col-4 d-flex flex-column gap-3">
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">{FYP.starter.tts.title}</li>
{FYP.starter.tts.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
</ul>
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">{FYP.starter.vcweb.title}</li>
{FYP.starter.vcweb.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
</ul>
</div>
<div className="col-4 d-flex flex-column gap-3">
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">
{FYP.starter.vcapp.title}{" "}
<span className="small-text">(registered users)</span>
</li>
{FYP.starter.vcapp.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
</ul>
</div>
<div className="col-4 d-flex flex-column gap-3">
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">{FYP.starter.w2l.title}</li>
{FYP.starter.w2l.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
</ul>
<ul className="pricing-list d-flex flex-column gap-2">
<li className="fw-semibold">
{FYP.starter.priority.title}{" "}
<span className="small-text">(registered users)</span>
</li>
{FYP.starter.priority.features.map((e: any) => {
return (
<li key={e}>
<FontAwesomeIcon
icon={faCheck}
className="text-red me-3"
/>
{e}
</li>
);
})}
</ul>
</div>
</div>
</div>
</div> */}
</div>
</div>
);
}
export { PricingPage };
@@ -1,264 +0,0 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "react-router-dom";
import {
faCheckCircle as TRUE,
faTimesCircle as FALSE,
faCheck,
} from "@fortawesome/free-solid-svg-icons";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import {
FAKEYOU_PRICES as FYP,
STORYTELLER_PRICES as STP,
} from "@storyteller/fakeyou/src/data/PriceTiers";
import {
distance,
pricing1,
pricing2,
pricing3,
delay2,
duration,
} from "../../../data/animation";
const Fade = require("react-reveal/Fade");
interface Props {
sessionWrapper: SessionWrapper;
querySessionCallback: () => void;
}
function PricingPage(props: Props) {
function capitalize(word: string) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
const [FY_TIERS, ST_TIERS] = [Object.keys(FYP), Object.keys(STP)];
const [FY_FEATURES, ST_FEATURES] = [
Object.keys(FYP.free.features),
Object.keys(STP.free.features),
];
let current_tiers: string[], current_features: string[], current_list: any;
const site = window.location.hostname;
switch (site) {
case "fakeyou":
[current_tiers, current_features, current_list] = [
FY_TIERS,
FY_FEATURES,
FYP,
];
break;
case "storyteller":
[current_tiers, current_features, current_list] = [
ST_TIERS,
ST_FEATURES,
STP,
];
break;
default:
[current_tiers, current_features, current_list] = [
FY_TIERS,
FY_FEATURES,
FYP,
];
break;
}
return (
<div>
<Fade bottom duration={duration} distance={distance}>
<div className="container py-5 text-center">
<h1 className="display-5 fw-bold">FakeYou Pricing</h1>
</div>
</Fade>
<div className="container mt-3">
<div className="row gy-4">
<div className="col-12 col-lg-4">
<Fade
bottom
duration={duration}
distance={distance}
delay={pricing1}
>
<div className="rounded panel p-4 h-100">
<h2 className="text-center my-2 fw-bold">Free</h2>
<p className="mb-4 text-center">Great for hobbyists</p>
<Link to="/" className="btn btn-secondary w-100 fs-5">
Create free TTS
</Link>
<h2 className="display-5 fw-bold text-center my-5">
${current_list.free.price}
<span className="fs-5 opacity-75 fw-normal"> /month</span>
</h2>
<ul className="pricing-list d-flex flex-column gap-3">
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
Unlimited TTS Generation
</li>
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
Up to 10 seconds audio
</li>
</ul>
</div>
</Fade>
</div>
<div className="col-12 col-lg-4">
<Fade
bottom
duration={duration}
distance={distance}
delay={pricing2}
>
<div className="rounded panel p-4 panel-border h-100">
<h2 className="text-center my-2 fw-bold">Basic</h2>
<p className="mb-4 text-center">Great for hobbyists</p>
<Link to="/" className="btn btn-primary w-100 fs-5">
Buy Basic
</Link>
<h2 className="display-5 fw-bold text-center my-5">
${current_list.basic.price}
<span className="fs-5 opacity-75 fw-normal"> /month</span>
</h2>
<ul className="pricing-list d-flex flex-column gap-3">
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
Unlimited TTS Generation
</li>
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
Up to 30 seconds audio
</li>
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
Priority processing
</li>
</ul>
</div>
</Fade>
</div>
<div className="col-12 col-lg-4">
<Fade
bottom
duration={duration}
distance={distance}
delay={pricing3}
>
<div className="rounded panel p-4 h-100">
<h2 className="text-center my-2 fw-bold">Pro</h2>
<p className="mb-4 text-center">Great for hobbyists</p>
<Link to="/" className="btn btn-primary w-100 fs-5">
Buy Pro
</Link>
<h2 className="display-5 fw-bold text-center my-5">
${current_list.pro.price}
<span className="fs-5 opacity-75 fw-normal"> /month</span>
</h2>
<ul className="pricing-list d-flex flex-column gap-3">
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
Unlimited TTS Generation
</li>
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
Up to 1 minute audio
</li>
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
Priority processing
</li>
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
Generate mp3 file
</li>
<li>
<FontAwesomeIcon icon={faCheck} className="text-red me-3" />
For Commercial Use
</li>
</ul>
</div>
</Fade>
</div>
</div>
</div>
<Fade bottom duration={duration} distance={distance} delay={delay2}>
<div className="container-panel mt-5 py-5">
<div className="panel p-4">
<table className="table">
<thead>
<tr>
<th className="">Feature List</th>
{current_tiers.map((e) => {
return (
<th key={e} className="">
{capitalize(e)}
</th>
);
})}
</tr>
</thead>
<tbody>
<tr>
<td className="has-text-weight-bold">Price</td>
<td>${current_list.free.price}/month</td>
<td>${current_list.basic.price}/month</td>
<td>${current_list.pro.price}/month</td>
</tr>
{current_features.map((e, i) => {
return (
<tr key={i}>
<td>{e}</td>
<td>
<FontAwesomeIcon
icon={current_list.free.features[e] ? TRUE : FALSE}
className={`fa-2x ${
current_list.free.features[e]
? "has-text-success"
: "has-text-danger"
}`}
/>
</td>
<td>
<FontAwesomeIcon
icon={current_list.basic.features[e] ? TRUE : FALSE}
className={`fa-2x ${
current_list.basic.features[e]
? "has-text-success"
: "has-text-danger"
}`}
/>
</td>
<td>
<FontAwesomeIcon
icon={current_list.pro.features[e] ? TRUE : FALSE}
className={`fa-2x ${
current_list.pro.features[e]
? "has-text-success"
: "has-text-danger"
}`}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</Fade>
</div>
);
}
export { PricingPage };