diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx
index 5048b7961..e0a8786d2 100644
--- a/frontend/src/Layout.tsx
+++ b/frontend/src/Layout.tsx
@@ -16,8 +16,10 @@ import { HomeOutlined } from '@mui/icons-material';
import ProPill from './components/ProPill';
import { CodeSplitLoader } from './CodeSplitLoader';
import useSidePanel from './views/panel/useSidePanel';
-import { Alert, AlertTitle } from '@mui/material';
+import { Alert, AlertTitle, Button, Hidden } from '@mui/material';
import useBackendCapabilities from './global/useBackendCapabilities';
+import useIsPro from 'auth/useIsPro';
+import ProButton from 'components/ProButton';
const Home = lazy(() => import('./views/Home' /* webpackChunkName: "home" */));
const Game = lazy(() => import('./views/Game' /* webpackChunkName: "game" */));
@@ -90,6 +92,7 @@ function App() {
const { toggle: togglePanel } = useSidePanel();
const isInitialised = useIsInitialised();
const user = useUser();
+ const isPro = useIsPro();
const goToHome = useCallback(() => history.push('/'), [history]);
useEffect(() => {
trackPageView(window.location.pathname);
@@ -144,6 +147,18 @@ function App() {
+ {!isPro ? (
+
+
+
+
+
+
+
+ ) : null}
+
{isInitialised ? (
@@ -193,10 +208,16 @@ const HomeButton = styled.div`
margin-right: 10px;
`;
-const ProPillContainer = styled.div`
- flex: 1;
+const ProPillContainer = styled.div``;
+
+const GoProContainer = styled.div`
+ margin-left: 20px;
`;
const Initialising = styled.div``;
+const Spacer = styled.div`
+ flex: 1;
+`;
+
export default App;
diff --git a/frontend/src/common/payloads.ts b/frontend/src/common/payloads.ts
index 32488c721..9fdf11fec 100644
--- a/frontend/src/common/payloads.ts
+++ b/frontend/src/common/payloads.ts
@@ -36,6 +36,7 @@ export interface CreateSubscriptionPayload {
currency: Currency;
locale: StripeLocales;
domain: string | null;
+ yearly: boolean;
}
export interface CreateSessionPayload {
diff --git a/frontend/src/components/ProButton/index.tsx b/frontend/src/components/ProButton/index.tsx
index 355e9401c..356a991d5 100644
--- a/frontend/src/components/ProButton/index.tsx
+++ b/frontend/src/components/ProButton/index.tsx
@@ -47,8 +47,9 @@ function ProButton({ children, quota }: ProButtonProps) {
e.preventDefault();
trackEvent('trial/modal/subscribe');
history.push('/subscribe');
+ close();
},
- [history]
+ [history, close]
);
const handleStartTrial = useCallback(
diff --git a/frontend/src/translations/ar.ts b/frontend/src/translations/ar.ts
index 70bb8b32e..908d3ac1d 100644
--- a/frontend/src/translations/ar.ts
+++ b/frontend/src/translations/ar.ts
@@ -357,6 +357,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/de.ts b/frontend/src/translations/de.ts
index 9fac1540a..4ebbaa2f0 100644
--- a/frontend/src/translations/de.ts
+++ b/frontend/src/translations/de.ts
@@ -364,6 +364,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/en.ts b/frontend/src/translations/en.ts
index 4de58ad3f..69e57efb5 100644
--- a/frontend/src/translations/en.ts
+++ b/frontend/src/translations/en.ts
@@ -426,6 +426,9 @@ export default {
users: (users: number) => `${users} users`,
unlimited_seats: 'Unlimited',
month: 'month',
+ year: 'year',
+ wantToPayYearly:
+ 'I want to pay annually (every 12 months), and get one month free per year!',
},
Encryption: {
createEncryptedSession: 'Encrypted Session',
diff --git a/frontend/src/translations/es.ts b/frontend/src/translations/es.ts
index 94eb07d02..432295b5c 100644
--- a/frontend/src/translations/es.ts
+++ b/frontend/src/translations/es.ts
@@ -360,6 +360,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/fr.ts b/frontend/src/translations/fr.ts
index 8aa5dac97..1593a22b1 100644
--- a/frontend/src/translations/fr.ts
+++ b/frontend/src/translations/fr.ts
@@ -427,6 +427,9 @@ export default {
users: (users: number) => `${users} utilisateurs`,
unlimited_seats: 'Illimité',
month: 'mois',
+ year: 'an',
+ wantToPayYearly:
+ 'Je souhaite payer annuellement, et obtenir un mois gratuit par an !',
},
Encryption: {
createEncryptedSession: 'Session cryptée',
diff --git a/frontend/src/translations/hu.ts b/frontend/src/translations/hu.ts
index 17f28b292..a21fe5d34 100644
--- a/frontend/src/translations/hu.ts
+++ b/frontend/src/translations/hu.ts
@@ -360,6 +360,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/it.ts b/frontend/src/translations/it.ts
index bebe26331..fa5159261 100644
--- a/frontend/src/translations/it.ts
+++ b/frontend/src/translations/it.ts
@@ -370,6 +370,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/ja.ts b/frontend/src/translations/ja.ts
index 4d3c42da0..595995735 100644
--- a/frontend/src/translations/ja.ts
+++ b/frontend/src/translations/ja.ts
@@ -358,6 +358,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/nl.ts b/frontend/src/translations/nl.ts
index 01d4332a8..2fea7dca7 100644
--- a/frontend/src/translations/nl.ts
+++ b/frontend/src/translations/nl.ts
@@ -370,6 +370,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/pl.ts b/frontend/src/translations/pl.ts
index 1dc3487bc..f1a8a4c75 100644
--- a/frontend/src/translations/pl.ts
+++ b/frontend/src/translations/pl.ts
@@ -360,6 +360,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/pt-br.ts b/frontend/src/translations/pt-br.ts
index 388f1e6f6..7e10462cc 100644
--- a/frontend/src/translations/pt-br.ts
+++ b/frontend/src/translations/pt-br.ts
@@ -360,6 +360,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/ru.ts b/frontend/src/translations/ru.ts
index 645b8faef..ee1359b8a 100644
--- a/frontend/src/translations/ru.ts
+++ b/frontend/src/translations/ru.ts
@@ -358,6 +358,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/types.ts b/frontend/src/translations/types.ts
index b6adc2426..9bdb1bfcc 100644
--- a/frontend/src/translations/types.ts
+++ b/frontend/src/translations/types.ts
@@ -360,6 +360,8 @@ export interface Translation {
users?: (users: number) => string;
unlimited_seats?: string;
month?: string;
+ year?: string;
+ wantToPayYearly?: string;
};
Encryption: {
createEncryptedSession?: string;
diff --git a/frontend/src/translations/zh-cn.ts b/frontend/src/translations/zh-cn.ts
index b80e09be4..e175c9635 100644
--- a/frontend/src/translations/zh-cn.ts
+++ b/frontend/src/translations/zh-cn.ts
@@ -358,6 +358,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/translations/zh-tw.ts b/frontend/src/translations/zh-tw.ts
index d3be83a31..5e95d2011 100644
--- a/frontend/src/translations/zh-tw.ts
+++ b/frontend/src/translations/zh-tw.ts
@@ -358,6 +358,8 @@ export default {
users: undefined,
unlimited_seats: undefined,
month: undefined,
+ year: undefined,
+ wantToPayYearly: undefined,
},
Encryption: {
createEncryptedSession: undefined,
diff --git a/frontend/src/views/subscribe/SubscribePage.tsx b/frontend/src/views/subscribe/SubscribePage.tsx
index 6a05451ed..a806fc592 100644
--- a/frontend/src/views/subscribe/SubscribePage.tsx
+++ b/frontend/src/views/subscribe/SubscribePage.tsx
@@ -6,7 +6,7 @@ import { useCallback } from 'react';
import styled from '@emotion/styled';
import Step from './components/Step';
import Button from '@mui/material/Button';
-import { colors } from '@mui/material';
+import { colors, FormControlLabel, Switch } from '@mui/material';
import { Currency, FullUser, Plan } from 'common';
import CurrencyPicker from './components/CurrencyPicker';
import ProductPicker from './components/ProductPicker';
@@ -39,6 +39,7 @@ function SubscriberPage() {
: 'team';
const [currency, setCurrency] = useState
('eur');
const [plan, setPlan] = useState(defaultProduct);
+ const [yearly, setYearly] = useState(false);
const product = useMemo(() => {
if (!plan || !products) {
return null;
@@ -47,7 +48,8 @@ function SubscriberPage() {
}, [plan, products]);
const [domain, setDomain] = useState(DEFAULT_DOMAIN);
const stripe = useStripe();
- const { SubscribePage: translations } = useTranslations();
+ const { SubscribePage: translations, Products: productsTranslations } =
+ useTranslations();
const language = useLanguage();
const needDomain = product && product.plan === 'unlimited';
const needLogin =
@@ -91,7 +93,8 @@ function SubscriberPage() {
product.plan,
currency,
language.stripeLocale,
- !product.seats ? domain : null
+ !product.seats ? domain : null,
+ yearly
);
if (session && stripe) {
@@ -101,22 +104,14 @@ function SubscriberPage() {
}
}
}
- }, [stripe, product, currency, domain, language]);
+ }, [stripe, product, currency, domain, language, yearly]);
const validForm = (!needDomain || validDomain) && !!product && !needLogin;
- return (
-
-
- {user && user.pro && !user.subscriptionsId ? (
- {translations.alertAlreadyPro}
- ) : null}
- {user && user.subscriptionsId && !user.trial ? (
- {translations.alertAlreadySubscribed}
- ) : null}
-
+ const steps = [
+ (index: number) => (
@@ -124,26 +119,46 @@ function SubscriberPage() {
value={plan}
products={products}
currency={currency}
+ yearly={yearly}
onChange={setPlan}
/>
+
+ setYearly(evt.target.checked)}
+ name="yearly"
+ size="medium"
+ />
+ }
+ label={`🎁 ${productsTranslations.wantToPayYearly!}`}
+ />
- {needDomain ? (
-
-
-
- ) : null}
+ ),
+ needDomain
+ ? (index: number) => (
+
+
+
+ )
+ : null,
+ (index: number) => (
@@ -158,9 +173,10 @@ function SubscriberPage() {
onChange={setCurrency}
/>
-
+ ),
+ (index: number) => (
+ ),
+ ].filter(Boolean) as Array<(index: number) => JSX.Element>;
+
+ return (
+
+
+ {user && user.pro && !user.subscriptionsId ? (
+ {translations.alertAlreadyPro}
+ ) : null}
+ {user && user.subscriptionsId && !user.trial ? (
+ {translations.alertAlreadySubscribed}
+ ) : null}
+
+ {steps.map((step, index) => {
+ return step(index + 1);
+ })}
);
}
diff --git a/frontend/src/views/subscribe/api.ts b/frontend/src/views/subscribe/api.ts
index ff80512eb..43f37e2b4 100644
--- a/frontend/src/views/subscribe/api.ts
+++ b/frontend/src/views/subscribe/api.ts
@@ -14,13 +14,15 @@ export async function createCheckoutSession(
plan: Plan,
currency: Currency,
locale: StripeLocales,
- domain: string | null
+ domain: string | null,
+ yearly: boolean
): Promise {
const payload: CreateSubscriptionPayload = {
plan,
currency,
domain,
locale,
+ yearly,
};
return await fetchPostGet<
CreateSubscriptionPayload,
diff --git a/frontend/src/views/subscribe/components/Product.tsx b/frontend/src/views/subscribe/components/Product.tsx
index 1515ab3d0..878a5eb5b 100644
--- a/frontend/src/views/subscribe/components/Product.tsx
+++ b/frontend/src/views/subscribe/components/Product.tsx
@@ -9,6 +9,7 @@ interface ProductDisplayProps {
product: Product;
currency: Currency;
selected: boolean;
+ yearly: boolean;
onSelect: (product: Product) => void;
}
@@ -16,14 +17,21 @@ function ProductDisplay({
product,
selected,
currency,
+ yearly,
onSelect,
}: ProductDisplayProps) {
const { Products: translations, SubscribeModal: subscribeTranslations } =
useTranslations();
+
const handleOrder = useCallback(() => {
onSelect(product);
}, [onSelect, product]);
+ const price =
+ yearly && product.recurring
+ ? (product[currency] / 100) * 11
+ : product[currency] / 100;
+
return (
@@ -36,9 +44,11 @@ function ProductDisplay({
- {(product[currency] / 100).toFixed(2)} {currency.toUpperCase()}
+ {price.toFixed(2)} {currency.toUpperCase()}
{product.recurring ? (
- / {translations.month}
+
+ / {yearly ? translations.year : translations.month}
+
) : null}
diff --git a/frontend/src/views/subscribe/components/ProductPicker.tsx b/frontend/src/views/subscribe/components/ProductPicker.tsx
index 0977c33d0..1aebc2184 100644
--- a/frontend/src/views/subscribe/components/ProductPicker.tsx
+++ b/frontend/src/views/subscribe/components/ProductPicker.tsx
@@ -8,6 +8,7 @@ interface ProductPickerProps {
value: Plan | null;
currency: Currency;
products: Product[];
+ yearly: boolean;
onChange: (value: Plan) => void;
}
@@ -15,6 +16,7 @@ function ProductPicker({
value,
currency,
products,
+ yearly,
onChange,
}: ProductPickerProps) {
const handleChange = useCallback(
@@ -30,6 +32,7 @@ function ProductPicker({
key={product.plan}
product={product}
currency={currency}
+ yearly={yearly}
onSelect={handleChange}
selected={value === product.plan}
/>
diff --git a/integration/package.json b/integration/package.json
index caca860ad..7028acb10 100644
--- a/integration/package.json
+++ b/integration/package.json
@@ -1,6 +1,6 @@
{
"name": "retro-board-integration",
- "version": "4.12.1",
+ "version": "4.13.0",
"description": "Integrations tests",
"main": "index.js",
"directories": {
diff --git a/package.json b/package.json
index 529a6a3ee..447895ab2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "retrospected",
- "version": "4.12.1",
+ "version": "4.13.0",
"description": "An agile retrospective board - Powering www.retrospected.com",
"private": true,
"scripts": {