Skip to content
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 inventory-countdown/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/dashboard": "^1.3.21",
"@wix/design-system": "^1.0.0",
"@wix/essentials": "^0.1.4",
"@wix/editor": "^1.308.0",
"@wix/site-window": "^1.19.0",
"@wix/stores": "^1.0.239",
Expand Down
15 changes: 15 additions & 0 deletions inventory-countdown/template/src/backend/api/instance/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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) {
console.error("Failed to fetch app instance:", error);
return new Response("Failed to fetch app instance", { status: 500 });
}
}
40 changes: 40 additions & 0 deletions inventory-countdown/template/src/hooks/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useCallback } 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
@@ -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
@@ -0,0 +1,49 @@
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);
console.log({instance})

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

if (isFree && !freeTrialAvailable) {
return (
<Banner
appearance="premium"
title="Choose your plan"
description="Customizations for stock countdown are available only in the premium plan."
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
Expand Up @@ -2,80 +2,105 @@ import React, { type FC, useState, useEffect } from 'react';
import { widget } from '@wix/editor';
import {
SidePanel,
WixDesignSystemProvider,
Input,
FieldSet,
Slider,
ToggleSwitch,
FormField,
Loader,
Box,
} from '@wix/design-system';
import '@wix/design-system/styles.global.css';
import { CALL_TO_ACTION } from './consts.js';
import { withProviders } from './withProviders.js';
import { useAppInstance } from '../../../../hooks/instance.js';
import { Subscription } from './components/subscription.js';

const Panel: FC = () => {
const [loaded, setLoaded] = useState(false);
const [threshold, setThreshold] = useState<number>(3);
const [callToAction, setCallToAction] = useState<string>();
const [showBadge, setShowBadge] = useState<string>();
const { data: appInstance, isLoading: isAppInstanceLoading } = useAppInstance();

useEffect(() => {
widget
.getProp('threshold')
.then((threshold) => setThreshold(threshold ? Number(threshold) : 3));
widget.getProp('call-to-action').then(setCallToAction);
widget.getProp('show-badge').then(setShowBadge);
Promise.all([
widget.getProp('threshold'),
widget.getProp('call-to-action'),
widget.getProp('show-badge'),
]).then(([threshold, callToAction, showBadge]) => {
setThreshold(threshold ? Number(threshold) : 3);
setCallToAction(callToAction);
setShowBadge(showBadge);
setLoaded(true);
});
}, []);

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

return (
<WixDesignSystemProvider features={{ newColorsBranding: true }}>
<SidePanel.Field>
<FieldSet gap="medium" legend="Threshold" columns="auto 60px">
<Slider
onChange={(value) => {
setThreshold(value as number);
widget.setProp('threshold', value.toString());
}}
min={0}
max={25}
value={threshold}
displayMarks={false}
/>
<Input
value={threshold}
size="small"
onChange={(event) => {
setThreshold(Number(event.target.value));
widget.setProp('threshold', event.target.value);
}}
/>
</FieldSet>
</SidePanel.Field>
<SidePanel.Field>
<FormField label="Show Badge" labelPlacement="left" labelWidth="1fr">
<ToggleSwitch
size="small"
checked={showBadge === 'true'}
onChange={(event) => {
setShowBadge(event.target.checked.toString());
widget.setProp('show-badge', event.target.checked.toString());
}}
/>
</FormField>
</SidePanel.Field>
<SidePanel.Field>
<FieldSet legend="Call to Action">
<Input
value={callToAction}
placeholder={CALL_TO_ACTION}
size="small"
onChange={(event) => {
setCallToAction(event.target.value);
widget.setProp('call-to-action', event.target.value || CALL_TO_ACTION);
}}
/>
</FieldSet>
</SidePanel.Field>
</WixDesignSystemProvider>
<SidePanel width="300jk">
<SidePanel.Content noPadding>
<Subscription instance={appInstance} />
<SidePanel.Field>
<FieldSet gap="medium" legend="Threshold" columns="auto 60px">
<Slider
disabled={appInstance.isFree}
onChange={(value) => {
setThreshold(value as number);
widget.setProp('threshold', value.toString());
}}
min={0}
max={25}
value={threshold}
displayMarks={false}
/>
<Input
disabled={appInstance.isFree}
value={threshold}
size="small"
onChange={(event) => {
setThreshold(Number(event.target.value));
widget.setProp('threshold', event.target.value);
}}
/>
</FieldSet>
</SidePanel.Field>
<SidePanel.Field>
<FormField label="Show Badge" labelPlacement="left" labelWidth="1fr">
<ToggleSwitch
disabled={appInstance.isFree}
size="small"
checked={showBadge === 'true'}
onChange={(event) => {
setShowBadge(event.target.checked.toString());
widget.setProp('show-badge', event.target.checked.toString());
}}
/>
</FormField>
</SidePanel.Field>
<SidePanel.Field>
<FieldSet legend="Call to Action">
<Input
disabled={appInstance.isFree}
value={callToAction}
placeholder={CALL_TO_ACTION}
size="small"
onChange={(event) => {
setCallToAction(event.target.value);
widget.setProp('call-to-action', event.target.value || CALL_TO_ACTION);
}}
/>
</FieldSet>
</SidePanel.Field>
</SidePanel.Content>
</SidePanel>
);
};

export default Panel;
export default withProviders(Panel);
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>
);
};
}