Compare commits

...

6 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
161d60393a Merge pull request #629 from bigcapitalhq/details-subscription
fix: Add subscription plans offer text
2024-08-25 19:44:34 +02:00
Ahmed Bouhuolia
79413fa85e fix: Add subscription plans offer text 2024-08-25 19:43:54 +02:00
Ahmed Bouhuolia
58552c6c94 Merge pull request #628 from bigcapitalhq/fix-webapp-env-variables
fix: Make webapp package env variables dynamic
2024-08-25 18:21:55 +02:00
Ahmed Bouhuolia
2072e35cfa fix: Make webapp package env variables dynamic 2024-08-25 18:21:08 +02:00
Ahmed Bouhuolia
1eaac9d691 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-08-25 14:18:04 +02:00
Ahmed Bouhuolia
a916e8a0cb fix: syntax error 2024-08-25 14:17:23 +02:00
18 changed files with 250 additions and 122 deletions

View File

@@ -246,8 +246,11 @@ module.exports = {
apiKey: process.env.LOOPS_API_KEY, apiKey: process.env.LOOPS_API_KEY,
}, },
oneClickDemoAccounts: parseBoolean( /**
process.env.ONE_CLICK_DEMO_ACCOUNTS, * One-click demo accounts.
false */
), oneClickDemoAccounts: {
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
},
}; };

View File

@@ -76,6 +76,10 @@ export interface IAuthSendedResetPassword {
export interface IAuthGetMetaPOJO { export interface IAuthGetMetaPOJO {
signupDisabled: boolean; signupDisabled: boolean;
oneClickDemo: {
enable: boolean;
demoUrl: string;
};
} }
export interface IAuthSignUpVerifingEventPayload { export interface IAuthSignUpVerifingEventPayload {

View File

@@ -11,6 +11,10 @@ export class GetAuthMeta {
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> { public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return { return {
signupDisabled: config.signupRestrictions.disabled, signupDisabled: config.signupRestrictions.disabled,
oneClickDemo: {
enable: config.oneClickDemoAccounts.enable,
demoUrl: config.oneClickDemoAccounts.demoUrl,
},
}; };
} }
} }

View File

@@ -18,7 +18,10 @@ export class LemonResumeSubscription {
* @param {string} subscriptionSlug - Subscription slug by default main subscription. * @param {string} subscriptionSlug - Subscription slug by default main subscription.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async resumeSubscription(tenantId: number, subscriptionSlug: string = 'main') { public async resumeSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
configureLemonSqueezy(); configureLemonSqueezy();
const subscription = await PlanSubscription.query().findOne({ const subscription = await PlanSubscription.query().findOne({
@@ -34,7 +37,7 @@ export class LemonResumeSubscription {
cancelled: false, cancelled: false,
}); });
if (returnedSub.error) { if (returnedSub.error) {
throw new ServiceError(ٌٌُERRORS.SOMETHING_WENT_WRONG_WITH_LS); throw new ServiceError(ERRORS.SOMETHING_WENT_WRONG_WITH_LS);
} }
// Triggers `onSubscriptionResume` event. // Triggers `onSubscriptionResume` event.
await this.eventPublisher.emitAsync( await this.eventPublisher.emitAsync(

View File

@@ -20,7 +20,6 @@ import { queryConfig } from '../hooks/query/base';
import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified'; import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified';
import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated'; import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated';
import { EnsureUserEmailNotVerified } from './Guards/EnsureUserEmailNotVerified'; import { EnsureUserEmailNotVerified } from './Guards/EnsureUserEmailNotVerified';
import { EnsureOneClickDemoAccountEnabled } from '@/containers/OneClickDemo/EnsureOneClickDemoAccountEnabled';
const EmailConfirmation = LazyLoader({ const EmailConfirmation = LazyLoader({
loader: () => import('@/containers/Authentication/EmailConfirmation'), loader: () => import('@/containers/Authentication/EmailConfirmation'),
@@ -31,6 +30,7 @@ const RegisterVerify = LazyLoader({
const OneClickDemoPage = LazyLoader({ const OneClickDemoPage = LazyLoader({
loader: () => import('@/containers/OneClickDemo/OneClickDemoPage'), loader: () => import('@/containers/OneClickDemo/OneClickDemoPage'),
}); });
/** /**
* App inner. * App inner.
*/ */
@@ -40,13 +40,7 @@ function AppInsider({ history }) {
<DashboardThemeProvider> <DashboardThemeProvider>
<Router history={history}> <Router history={history}>
<Switch> <Switch>
<Route path={'/one_click_demo'}> <Route path={'/one_click_demo'} children={<OneClickDemoPage />} />
<EnsureOneClickDemoAccountEnabled>
<EnsureAuthNotAuthenticated>
<OneClickDemoPage />
</EnsureAuthNotAuthenticated>
</EnsureOneClickDemoAccountEnabled>
</Route>
<Route path={'/auth/register/verify'}> <Route path={'/auth/register/verify'}>
<EnsureAuthenticated> <EnsureAuthenticated>
<EnsureUserEmailNotVerified> <EnsureUserEmailNotVerified>

View File

@@ -1,6 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { useApplicationBoot } from '@/components'; import { useApplicationBoot } from '@/components';
import { useAuthMetadata } from '@/hooks/query/authentication';
/** /**
* Private pages provider. * Private pages provider.
@@ -9,7 +10,10 @@ export function PrivatePagesProvider({
// #ownProps // #ownProps
children, children,
}) { }) {
const { isLoading } = useApplicationBoot(); const { isLoading: isAppBootLoading } = useApplicationBoot();
const { isLoading: isAuthMetaLoading } = useAuthMetadata();
const isLoading = isAppBootLoading || isAuthMetaLoading;
return <React.Fragment>{!isLoading ? children : null}</React.Fragment>; return <React.Fragment>{!isLoading ? children : null}</React.Fragment>;
} }

View File

@@ -1,6 +0,0 @@
export const Config = {
oneClickDemo: {
enable: process.env.REACT_APP_ONE_CLICK_DEMO_ENABLE === 'true',
demoUrl: process.env.REACT_APP_DEMO_ACCOUNT_URL || '',
},
};

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { Config } from '@/config'; import { useOneClickDemoBoot } from './OneClickDemoBoot';
interface EnsureOneClickDemoAccountEnabledProps { interface EnsureOneClickDemoAccountEnabledProps {
children: React.ReactNode; children: React.ReactNode;
@@ -11,9 +11,10 @@ export const EnsureOneClickDemoAccountEnabled = ({
children, children,
redirectTo = '/', redirectTo = '/',
}: EnsureOneClickDemoAccountEnabledProps) => { }: EnsureOneClickDemoAccountEnabledProps) => {
const enabeld = Config.oneClickDemo.enable || false; const { authMeta } = useOneClickDemoBoot();
const enabled = authMeta?.meta?.one_click_demo?.enable || false;
if (!enabeld) { if (!enabled) {
return <Redirect to={{ pathname: redirectTo }} />; return <Redirect to={{ pathname: redirectTo }} />;
} }
return <>{children}</>; return <>{children}</>;

View File

@@ -0,0 +1,45 @@
import React, { createContext, useContext, ReactNode } from 'react';
import { useAuthMetadata } from '@/hooks/query/authentication';
interface OneClickDemoContextType {
authMeta: any;
}
const OneClickDemoContext = createContext<OneClickDemoContextType>(
{} as OneClickDemoContextType,
);
export const useOneClickDemoBoot = () => {
const context = useContext(OneClickDemoContext);
if (!context) {
throw new Error(
'useOneClickDemo must be used within a OneClickDemoProvider',
);
}
return context;
};
interface OneClickDemoBootProps {
children: ReactNode;
}
export const OneClickDemoBoot: React.FC<OneClickDemoBootProps> = ({
children,
}) => {
const { isLoading: isAuthMetaLoading, data: authMeta } = useAuthMetadata();
const value = {
isAuthMetaLoading,
authMeta,
};
if (isAuthMetaLoading) {
return null;
}
return (
<OneClickDemoContext.Provider value={value}>
{children}
</OneClickDemoContext.Provider>
);
};

View File

@@ -1,97 +1,16 @@
// @ts-nocheck import { EnsureAuthNotAuthenticated } from '@/components/Guards/EnsureAuthNotAuthenticated';
import { Button, Intent, ProgressBar, Text } from '@blueprintjs/core'; import { EnsureOneClickDemoAccountEnabled } from './EnsureOneClickDemoAccountEnabled';
import { useEffect, useState } from 'react'; import { OneClickDemoBoot } from './OneClickDemoBoot';
import { import { OneClickDemoPageContent } from './OneClickDemoPageContent';
useCreateOneClickDemo,
useOneClickDemoSignin,
} from '@/hooks/query/oneclick-demo';
import { Box, Icon, Stack } from '@/components';
import { useJob } from '@/hooks/query';
import style from './OneClickDemoPage.module.scss';
export default function OneClickDemoPage() { export default function OneClickDemoPage() {
const {
mutateAsync: createOneClickDemo,
isLoading: isCreateOneClickLoading,
} = useCreateOneClickDemo();
const {
mutateAsync: oneClickDemoSignIn,
isLoading: isOneclickDemoSigningIn,
} = useOneClickDemoSignin();
// Job states.
const [demoId, setDemoId] = useState<string>('');
const [buildJobId, setBuildJobId] = useState<string>('');
const [isJobDone, setIsJobDone] = useState<boolean>(false);
const {
data: { running, completed },
} = useJob(buildJobId, {
refetchInterval: 2000,
enabled: !isJobDone && !!buildJobId,
});
useEffect(() => {
if (completed) {
setIsJobDone(true);
}
}, [completed, setIsJobDone]);
// One the job done request sign-in using the demo id.
useEffect(() => {
if (isJobDone) {
oneClickDemoSignIn({ demoId }).then((res) => {
debugger;
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isJobDone]);
const handleCreateAccountBtnClick = () => {
createOneClickDemo({})
.then(({ data: { data } }) => {
setBuildJobId(data?.build_job?.job_id);
setDemoId(data?.demo_id);
})
.catch(() => {});
};
const isLoading = running || isOneclickDemoSigningIn;
return ( return (
<Box className={style.root}> <EnsureAuthNotAuthenticated>
<Box className={style.inner}> <OneClickDemoBoot>
<Stack align={'center'} spacing={40}> <EnsureOneClickDemoAccountEnabled>
<Icon icon="bigcapital" height={37} width={228} /> <OneClickDemoPageContent />
</EnsureOneClickDemoAccountEnabled>
{isLoading && ( </OneClickDemoBoot>
<Stack align={'center'} spacing={15}> </EnsureAuthNotAuthenticated>
<ProgressBar stripes value={null} className={style.progressBar} />
{isOneclickDemoSigningIn && (
<Text className={style.waitingText}>
It's signin-in to your demo account, Just a second!
</Text>
)}
{running && (
<Text className={style.waitingText}>
We're preparing temporary environment for trial, It typically
take few seconds. Do not close or refresh the page.
</Text>
)}
</Stack>
)}
</Stack>
{!isLoading && (
<Button
className={style.oneClickBtn}
intent={Intent.NONE}
onClick={handleCreateAccountBtnClick}
loading={isCreateOneClickLoading}
>
Create Demo Account
</Button>
)}
</Box>
</Box>
); );
} }

View File

@@ -0,0 +1,97 @@
// @ts-nocheck
import { Button, Intent, ProgressBar, Text } from '@blueprintjs/core';
import { useEffect, useState } from 'react';
import {
useCreateOneClickDemo,
useOneClickDemoSignin,
} from '@/hooks/query/oneclick-demo';
import { Box, Icon, Stack } from '@/components';
import { useJob } from '@/hooks/query';
import style from './OneClickDemoPage.module.scss';
export function OneClickDemoPageContent() {
const {
mutateAsync: createOneClickDemo,
isLoading: isCreateOneClickLoading,
} = useCreateOneClickDemo();
const {
mutateAsync: oneClickDemoSignIn,
isLoading: isOneclickDemoSigningIn,
} = useOneClickDemoSignin();
// Job states.
const [demoId, setDemoId] = useState<string>('');
const [buildJobId, setBuildJobId] = useState<string>('');
const [isJobDone, setIsJobDone] = useState<boolean>(false);
const {
data: { running, completed },
} = useJob(buildJobId, {
refetchInterval: 2000,
enabled: !isJobDone && !!buildJobId,
});
useEffect(() => {
if (completed) {
setIsJobDone(true);
}
}, [completed, setIsJobDone]);
// One the job done request sign-in using the demo id.
useEffect(() => {
if (isJobDone) {
oneClickDemoSignIn({ demoId }).then((res) => {
debugger;
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isJobDone]);
const handleCreateAccountBtnClick = () => {
createOneClickDemo({})
.then(({ data: { data } }) => {
setBuildJobId(data?.build_job?.job_id);
setDemoId(data?.demo_id);
})
.catch(() => {});
};
const isLoading = running || isOneclickDemoSigningIn;
return (
<Box className={style.root}>
<Box className={style.inner}>
<Stack align={'center'} spacing={40}>
<Icon icon="bigcapital" height={37} width={228} />
{isLoading && (
<Stack align={'center'} spacing={15}>
<ProgressBar stripes value={null} className={style.progressBar} />
{isOneclickDemoSigningIn && (
<Text className={style.waitingText}>
It's signin-in to your demo account, Just a second!
</Text>
)}
{running && (
<Text className={style.waitingText}>
We're preparing temporary environment for trial, It typically
take few seconds. Do not close or refresh the page.
</Text>
)}
</Stack>
)}
</Stack>
{!isLoading && (
<Button
className={style.oneClickBtn}
intent={Intent.NONE}
onClick={handleCreateAccountBtnClick}
loading={isCreateOneClickLoading}
>
Create Demo Account
</Button>
)}
</Box>
</Box>
);
}

View File

@@ -6,7 +6,7 @@ import { Icon, For, FormattedMessage as T, Stack } from '@/components';
import { getFooterLinks } from '@/constants/footerLinks'; import { getFooterLinks } from '@/constants/footerLinks';
import { useAuthActions } from '@/hooks/state'; import { useAuthActions } from '@/hooks/state';
import style from './SetupLeftSection.module.scss'; import style from './SetupLeftSection.module.scss';
import { Config } from '@/config'; import { useAuthMetadata } from '@/hooks/query';
/** /**
* Footer item link. * Footer item link.
@@ -28,13 +28,16 @@ function SetupLeftSectionFooter() {
// Retrieve the footer links. // Retrieve the footer links.
const footerLinks = getFooterLinks(); const footerLinks = getFooterLinks();
const { data: authMeta } = useAuthMetadata();
const demoUrl = authMeta?.meta?.one_click_demo?.demo_url;
const handleDemoBtnClick = () => { const handleDemoBtnClick = () => {
window.open(Config.oneClickDemo.demoUrl); window.open(demoUrl);
}; };
return ( return (
<div className={'content__footer'}> <div className={'content__footer'}>
{Config.oneClickDemo.demoUrl && ( {demoUrl && (
<Stack spacing={16}> <Stack spacing={16}>
<Text className={style.demoButtonLabel}>Not Now?</Text> <Text className={style.demoButtonLabel}>Not Now?</Text>
<button className={style.demoButton} onClick={handleDemoBtnClick}> <button className={style.demoButton} onClick={handleDemoBtnClick}>

View File

@@ -0,0 +1,20 @@
.container {
text-align: center;
margin-bottom: 1.15rem;
}
.iconText {
display: inline-flex;
font-size: 14px;
margin-right: 16px;
color: #00824d;
&:last-child {
margin-right: 0; /* Remove the margin on the last item */
}
}
.icon {
margin-right: 2px;
}

View File

@@ -0,0 +1,35 @@
import styles from './SubscriptionPlansOfferChecks.module.scss';
export function SubscriptionPlansOfferChecks() {
return (
<div className={styles.container}>
<span className={styles.iconText}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="18px"
width="18px"
viewBox="0 -960 960 960"
fill="rgb(0, 130, 77)"
className={styles.icon}
>
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
</svg>
14-day free trial
</span>
<span className={styles.iconText}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="18px"
width="18px"
viewBox="0 -960 960 960"
fill="rgb(0, 130, 77)"
className={styles.icon}
>
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
</svg>
24/7 online support
</span>
</div>
);
}

View File

@@ -24,7 +24,7 @@ function SubscriptionPlansPeriodSwitcherRoot({
); );
}; };
return ( return (
<Group position={'center'} spacing={10} style={{ marginBottom: '1.2rem' }}> <Group position={'center'} spacing={10} style={{ marginBottom: '1.6rem' }}>
<Text>Pay Monthly</Text> <Text>Pay Monthly</Text>
<Switch <Switch
large large

View File

@@ -1,6 +1,7 @@
import { Callout } from '@blueprintjs/core'; import { Callout } from '@blueprintjs/core';
import { SubscriptionPlans } from './SubscriptionPlans'; import { SubscriptionPlans } from './SubscriptionPlans';
import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher'; import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
import { SubscriptionPlansOfferChecks } from './SubscriptionPlansOfferChecks';
/** /**
* Billing plans. * Billing plans.
@@ -14,6 +15,7 @@ export function SubscriptionPlansSection() {
include applicable taxes. include applicable taxes.
</Callout> </Callout>
<SubscriptionPlansOfferChecks />
<SubscriptionPlansPeriodSwitcher /> <SubscriptionPlansPeriodSwitcher />
<SubscriptionPlans /> <SubscriptionPlans />
</section> </section>

View File

@@ -100,7 +100,7 @@ export const useAuthResetPassword = (props) => {
/** /**
* Fetches the authentication page metadata. * Fetches the authentication page metadata.
*/ */
export const useAuthMetadata = (props) => { export const useAuthMetadata = (props = {}) => {
return useRequestQuery( return useRequestQuery(
[t.AUTH_METADATA_PAGE], [t.AUTH_METADATA_PAGE],
{ {

View File

@@ -29,7 +29,7 @@
&__content { &__content {
width: 100%; width: 100%;
padding-bottom: 40px; padding-bottom: 80px;
} }
&__left-section { &__left-section {