Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(free-trial): Add free trials to chart widget #4

Open
wants to merge 3 commits into
base: free-trials-templates
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions chart-widget/template/package.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ to: package.json
"name": "<%= packageName %>",
"version": "1.0.0",
"dependencies": {
"@tanstack/react-query": "^4.36.1",
"@wix/app-management": "^1.0.0",
"@wix/design-system": "^1.0.0",
"@wix/editor": "^1.0.0",
"@wix/essentials": "^0.1.4",
"@wix/wix-ui-icons-common": "^3.0.0",
"react-to-webcomponent": "^2.0.0",
"recharts": "^2.0.0"
Expand Down
14 changes: 14 additions & 0 deletions chart-widget/template/src/backend/api/instance/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { auth } from "@wix/essentials";
import { appInstances } from "@wix/app-management";

export async function GET() {
try {
const { instance: appInstance } = await auth.elevate(
appInstances.getAppInstance
)();

return Response.json(appInstance);
} catch (error) {
return new Response("Failed to fetch app instance", { status: 500 });
}
}
40 changes: 40 additions & 0 deletions chart-widget/template/src/hooks/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useCallback, useEffect, useState } from 'react';
import { appInstances } from '@wix/app-management';
import { useQuery } from '@tanstack/react-query';
import { httpClient } from '@wix/essentials';

/*
This is the URL to the pricing page of the app.
This url looks like this:
https://www.wix.com/apps/upgrade/APPIDHERE?appInstanceId=INSTANCEIDHERE

You can find more information about this here:
https://dev.wix.com/docs/build-apps/launch-your-app/pricing-and-billing/set-up-a-freemium-business-model#step-4--create-an-upgrade-entry-point-to-your-pricing-page
*/
const getPricingPage = (instanceId: string) => `https://www.wix.com/apps/upgrade/<%= devCenter.appId %>?appInstanceId=${instanceId}`

export const QUERY_INSTANCE = 'queryInstance';

export function useAppInstance() {
return useQuery<appInstances.AppInstance>({
queryKey: [QUERY_INSTANCE],
queryFn: async () => {
try {
const response = await httpClient.fetchWithAuth(
`${import.meta.env.BASE_API_URL}/instance`
);
return response.json();
} catch (error) {
console.log("Error fetching instance:", error);
}
},
});
}

export function useNavigateToPricingPage(instance: appInstances.AppInstance): () => void {
return useCallback(() => {
if (instance?.instanceId) {
window.open(getPricingPage(instance?.instanceId), "_blank");
}
}, [instance?.instanceId]);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React, { type FC, useState, useEffect } from 'react';
import { widget } from '@wix/editor';
import { SidePanel, WixDesignSystemProvider } from '@wix/design-system';
import '@wix/design-system/styles.global.css';
import { SidePanel, Loader, Box } from '@wix/design-system';
import { DEFAULT_TYPE, DEFAULT_ITEMS, type ChartItem } from './common.js';
import { withProviders } from './withProviders.js';
import Slice from './panel/slice.js';
import { ChartType } from './panel/chart-type.js';
import { Subscription } from './panel/subscription.js';
import { useAppInstance } from '../../../../hooks/instance.js';

const Panel: FC = () => {
const [loaded, setLoaded] = useState(false);
const [type, setType] = useState<string>('');
const [items, setItems] = useState<ChartItem[]>([]);
const { data: appInstance, isLoading: isAppInstanceLoading } = useAppInstance();

useEffect(() => {
Promise.all([widget.getProp('type'), widget.getProp('items')]).then(([type, items]) => {
Expand All @@ -19,36 +22,42 @@ const Panel: FC = () => {
});
}, []);

if (isAppInstanceLoading || !loaded) {
return (
<Box align="center" verticalAlign="middle" height="50vh">
<Loader text="Loading..." />
</Box>
)
}

return (
<WixDesignSystemProvider>
<SidePanel width="300">
{loaded && (
<SidePanel.Content noPadding>
<ChartType
type={type}
onChange={(type) => {
setType(type);
widget.setProp('type', type);
}}
/>
{items.map((item, index) => (
<Slice
key={index}
title={`Slice ${index + 1}`}
item={item}
onChange={(item) => {
const newItems = [...items];
newItems[index] = item;
setItems(newItems);
widget.setProp('items', JSON.stringify(newItems));
}}
/>
))}
</SidePanel.Content>
)}
</SidePanel>
</WixDesignSystemProvider>
<SidePanel width="300">
<SidePanel.Content noPadding>
<Subscription instance={appInstance}/>
<ChartType
isPremium={!appInstance.isFree}
type={type}
onChange={(type) => {
setType(type);
widget.setProp('type', type);
}}
/>
{items.map((item, index) => (
<Slice
key={index}
title={`Slice ${index + 1}`}
item={item}
onChange={(item) => {
const newItems = [...items];
newItems[index] = item;
setItems(newItems);
widget.setProp('items', JSON.stringify(newItems));
}}
/>
))}
</SidePanel.Content>
</SidePanel>
);
};

export default Panel;
export default withProviders(Panel);
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Box, Text, Button, SectionHelper, SectionHelperAppearance, ButtonSkin } from '@wix/design-system';
import React from 'react';

export interface BannerProps {
appearance?: SectionHelperAppearance;
title: string;
description: string;
action?: string;
actionSkin?: ButtonSkin;
onActionClick?: () => void;
}

export const Banner: React.FC<BannerProps> = ({
appearance,
title,
description,
action,
actionSkin,
onActionClick
}) => {
return (
<Box padding="SP2">
<SectionHelper
fullWidth
title={title}
appearance={appearance}
>
<Box gap="SP2" direction="vertical">
<Text size="small">{description}</Text>
{action && (
<Button
size="small"
skin={actionSkin}
onClick={onActionClick}
children={action}
/>
)}
</Box>
</SectionHelper>
</Box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import { Box, FieldSet, SidePanel, Thumbnail } from '@wix/design-system';
import { PieChart, BarChartSplit } from '@wix/wix-ui-icons-common';

interface Props {
isPremium: boolean;
type: string;
onChange: (type: string) => void;
}

const options = [
{ value: 'pie', icon: PieChart },
{ value: 'bar', icon: BarChartSplit },
{ value: 'pie', icon: PieChart, free: true },
{ value: 'bar', icon: BarChartSplit, free: false },
];

export const ChartType: FC<Props> = ({ type, onChange }) => {
export const ChartType: FC<Props> = ({ type, onChange, isPremium }) => {
return (
<SidePanel.Field>
<FieldSet
Expand All @@ -21,10 +22,11 @@ export const ChartType: FC<Props> = ({ type, onChange }) => {
gap="medium"
columns="min-content"
>
{options.map(({ value, icon: Icon }) => (
{options.map(({ value, icon: Icon, free }) => (
<Thumbnail
key={value}
selected={type === value}
disabled={!free && !isPremium}
onClick={() => onChange(value)}
hideSelectedIcon
noPadding
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { appInstances } from '@wix/app-management';
import { Banner } from './banner';
import { useNavigateToPricingPage } from '../../../../../hooks/instance';

export const Subscription: React.FC<{instance: appInstances.AppInstance}> = ({instance}) => {
const {billing, isFree, freeTrialAvailable } = instance;
const navigateToPricingPage = useNavigateToPricingPage(instance);

if (isFree && freeTrialAvailable) {
return (
<Banner
appearance="premium"
title="Free plan available"
description="Upgrade to a premium plan to unlock more charts."
action="Start Free Trial"
actionSkin="premium"
onActionClick={navigateToPricingPage}
/>
);
}

if (isFree && !freeTrialAvailable) {
return (
<Banner
appearance="premium"
title="Choose your plan"
description="Upgrade to a premium plan to unlock more charts."
action="Upgrade"
actionSkin="premium"
onActionClick={navigateToPricingPage}
/>
);
}

if (!isFree && billing?.freeTrialInfo?.status === appInstances.FreeTrialStatus.IN_PROGRESS) {
const endDate = new Date(billing?.freeTrialInfo?.endDate!)
return (
<Banner
appearance="standard"
title="Free Trial in progress"
description={`Your free trial is available to ${endDate.toLocaleString()}.`}
/>
);
}

return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { i18n } from '@wix/essentials';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { WixDesignSystemProvider } from '@wix/design-system';
import '@wix/design-system/styles.global.css';

const queryClient = new QueryClient();

export function withProviders<P extends {} = {}>(Component: React.FC<P>) {
return function CustomElementProviders(props: P) {
const locale = i18n.getLocale();
return (
<WixDesignSystemProvider locale={locale} features={{ newColorsBranding: true }}>
<QueryClientProvider client={queryClient}>
<Component {...props} />
</QueryClientProvider>
</WixDesignSystemProvider>
);
};
}