mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 09:28:11 +00:00
Merge pull request #121 from storytold/echelon-bflat-pricing
Echelon bflat pricing
This commit is contained in:
18 files changed
+1365
-326
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
Reference in new issue
Block a user