Compare commits
13 Commits
main
...
fix/hariom
Author | SHA1 | Date |
---|---|---|
Hariom Balhara | 21ece8d9f6 | |
Hariom Balhara | c6ca411b54 | |
Hariom Balhara | d313ade54c | |
Hariom Balhara | 95d7c17e4b | |
Hariom Balhara | 721e1c0d33 | |
Hariom Balhara | 8ecaa95dc9 | |
Hariom Balhara | d563343669 | |
Hariom Balhara | c563415795 | |
Hariom Balhara | 18c8057be7 | |
Hariom Balhara | bcfd22614b | |
Hariom Balhara | 3a3c3a918b | |
Hariom Balhara | 40ad51381e | |
Hariom Balhara | 86e7ba09e4 |
|
@ -73,6 +73,13 @@
|
|||
"command": "maildev -s 587",
|
||||
"isBackground": false,
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "AppStoreWatch",
|
||||
"type": "shell",
|
||||
"command": "yarn app-store:watch",
|
||||
"isBackground": false,
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -402,7 +402,7 @@ Next make sure you have your app running `yarn dx`. Then in the slack chat type
|
|||
4. Fill in any information you want in the "App info" tab
|
||||
5. Go to tab "Auth"
|
||||
6. Now copy the Client ID and Client Secret to your .env file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
|
||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspot othercalendar/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspotothercalendar/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scope called `crm.objects.contacts`
|
||||
9. Click the "Save" button at the bottom footer.
|
||||
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
||||
|
|
|
@ -4,7 +4,7 @@ import { OptionProps } from "react-select";
|
|||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
@ -14,11 +14,11 @@ interface AdditionalCalendarSelectorProps {
|
|||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
|
||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; appId: string }>) => {
|
||||
const { data } = optionProps;
|
||||
return (
|
||||
<InstallAppButton
|
||||
type={data.type}
|
||||
slug={data.appId}
|
||||
render={(installProps) => {
|
||||
return (
|
||||
<Button {...installProps} className="w-full" color="minimal">
|
||||
|
@ -46,7 +46,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||
label: item.name,
|
||||
slug: item.slug,
|
||||
image: item.imageSrc,
|
||||
type: item.type,
|
||||
appId: item.appId,
|
||||
}));
|
||||
return (
|
||||
<Select
|
||||
|
|
|
@ -14,7 +14,7 @@ import React, { useEffect, useState } from "react";
|
|||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { App as AppType } from "@calcom/types/App";
|
||||
import { AppMeta as AppType } from "@calcom/types/App";
|
||||
import { Button, SkeletonButton } from "@calcom/ui";
|
||||
|
||||
import Shell from "@components/Shell";
|
||||
|
@ -23,6 +23,7 @@ import Badge from "@components/ui/Badge";
|
|||
export default function App({
|
||||
name,
|
||||
type,
|
||||
slug,
|
||||
logo,
|
||||
body,
|
||||
categories,
|
||||
|
@ -38,6 +39,7 @@ export default function App({
|
|||
privacy,
|
||||
}: {
|
||||
name: string;
|
||||
slug: string;
|
||||
type: AppType["type"];
|
||||
isGlobal?: AppType["isGlobal"];
|
||||
logo: string;
|
||||
|
@ -64,9 +66,9 @@ export default function App({
|
|||
const [installedApp, setInstalledApp] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
async function getInstalledApp(appCredentialType: string) {
|
||||
async function getInstalledApp(slug: string) {
|
||||
const queryParam = new URLSearchParams();
|
||||
queryParam.set("app-credential-type", appCredentialType);
|
||||
queryParam.set("app-slug", slug);
|
||||
try {
|
||||
const result = await fetch(`/api/app-store/installed?${queryParam.toString()}`, {
|
||||
method: "GET",
|
||||
|
@ -87,8 +89,9 @@ export default function App({
|
|||
}
|
||||
}
|
||||
}
|
||||
getInstalledApp(type);
|
||||
}, [type]);
|
||||
getInstalledApp(slug);
|
||||
}, [slug]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Shell large isPublic>
|
||||
|
@ -123,7 +126,7 @@ export default function App({
|
|||
: t("globally_install")}
|
||||
</Button>
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
slug={slug}
|
||||
render={(buttonProps) => (
|
||||
<Button StartIcon={PlusIcon} data-testid="install-app-button" {...buttonProps}>
|
||||
{t("add_another")}
|
||||
|
@ -133,7 +136,7 @@ export default function App({
|
|||
</div>
|
||||
) : (
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
slug={slug}
|
||||
render={(buttonProps) => (
|
||||
<Button data-testid="install-app-button" {...buttonProps}>
|
||||
{t("install_app")}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import AppCard from "./AppCard";
|
||||
|
||||
export default function AllApps({ apps }: { apps: App[] }) {
|
||||
export default function AllApps({ apps }: { apps: AppMeta[] }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import AppCard from "./AppCard";
|
||||
import Slider from "./Slider";
|
||||
|
||||
const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
|
||||
const TrendingAppsSlider = <T extends AppMeta>({ items }: { items: T[] }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
|
|
|
@ -131,7 +131,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||
key={cal.externalId}
|
||||
externalId={cal.externalId}
|
||||
title={cal.name || "Nameless calendar"}
|
||||
type={item.integration.type}
|
||||
type={item.integration.appId}
|
||||
defaultSelected={cal.isSelected}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -54,7 +54,11 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
|||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
credentials: {
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
},
|
||||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
|
|
|
@ -7,18 +7,32 @@ import { getSession } from "@lib/auth";
|
|||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
req.session = await getSession({ req });
|
||||
if (req.method === "GET" && req.session && req.session.user.id && req.query) {
|
||||
const { "app-credential-type": appCredentialType } = req.query;
|
||||
if (!appCredentialType && Array.isArray(appCredentialType)) {
|
||||
const { "app-slug": appSlug } = req.query;
|
||||
if (!appSlug || typeof appSlug !== "string") {
|
||||
return res.status(400);
|
||||
}
|
||||
|
||||
const userId = req.session.user.id;
|
||||
let where;
|
||||
if (appSlug === "giphy") {
|
||||
where = {
|
||||
userId: userId,
|
||||
type: "giphy_other",
|
||||
};
|
||||
} else if (appSlug === "slack") {
|
||||
where = {
|
||||
userId: userId,
|
||||
type: "slack_app",
|
||||
};
|
||||
} else {
|
||||
where = {
|
||||
userId: userId,
|
||||
appId: appSlug,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const installedApp = await prisma.credential.findMany({
|
||||
where: {
|
||||
type: appCredentialType as string,
|
||||
userId: userId,
|
||||
},
|
||||
where,
|
||||
});
|
||||
|
||||
if (installedApp && !!installedApp.length) {
|
||||
|
|
|
@ -63,6 +63,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
orderBy: { id: "desc" as Prisma.SortOrder },
|
||||
},
|
||||
timeZone: true,
|
||||
|
|
|
@ -113,7 +113,11 @@ const userSelect = Prisma.validator<Prisma.UserArgs>()({
|
|||
name: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
credentials: true,
|
||||
credentials: {
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
},
|
||||
bufferTime: true,
|
||||
destinationCalendar: true,
|
||||
locale: true,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { deriveAppKeyFromSlugOrType } from "@calcom/lib/deriveAppKeyFromSlugOrType";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
|
@ -13,15 +15,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
return res.status(404).json({ message: `API route not found` });
|
||||
}
|
||||
|
||||
const [_appName, apiEndpoint] = args;
|
||||
const appName = _appName.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
|
||||
|
||||
const [appName, apiEndpoint] = args;
|
||||
try {
|
||||
/* Absolute path didn't work */
|
||||
const handlerMap = (await import("@calcom/app-store/apiHandlers")).default;
|
||||
const handlers = await handlerMap[appName as keyof typeof handlerMap];
|
||||
const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler;
|
||||
const handlerMap = (await import("@calcom/app-store/apps.generated")).apiHandlers;
|
||||
|
||||
const handlerKey = deriveAppKeyFromSlugOrType(appName, handlerMap);
|
||||
const handlers = await handlerMap[handlerKey];
|
||||
const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler;
|
||||
if (typeof handler !== "function")
|
||||
throw new HttpError({ statusCode: 404, message: `API handler not found` });
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import Image from "next/image";
|
|||
import Link from "next/link";
|
||||
import path from "path";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import { getAppRegistry, getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import useMediaQuery from "@lib/hooks/useMediaQuery";
|
||||
|
@ -49,6 +49,7 @@ const components = {
|
|||
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
return (
|
||||
<App
|
||||
slug={data.slug}
|
||||
name={data.name}
|
||||
isGlobal={data.isGlobal}
|
||||
type={data.type}
|
||||
|
@ -87,8 +88,14 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||
|
||||
if (!app) return { notFound: true };
|
||||
|
||||
const singleApp = await getAppWithMetadata(app);
|
||||
|
||||
let singleApp = await getAppWithMetadata(app);
|
||||
const appStoreFromDb = await getAppRegistry();
|
||||
appStoreFromDb.forEach((appFromDb) => {
|
||||
singleApp = {
|
||||
...singleApp,
|
||||
...appFromDb,
|
||||
};
|
||||
});
|
||||
if (!singleApp) return { notFound: true };
|
||||
|
||||
const appDirname = app.dirName;
|
||||
|
@ -97,7 +104,7 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||
let source = "";
|
||||
|
||||
try {
|
||||
/* If the app doesn't have a README we fallback to the packagfe description */
|
||||
/* If the app doesn't have a README we fallback to the package description */
|
||||
source = fs.readFileSync(postFilePath).toString();
|
||||
} catch (error) {
|
||||
console.log(`No README.mdx provided for: ${appDirname}`);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { JSONObject } from "superjson/dist/types";
|
|||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import EmptyScreen from "@calcom/ui/EmptyScreen";
|
||||
|
@ -28,13 +28,13 @@ import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingT
|
|||
|
||||
function ConnectOrDisconnectIntegrationButton(props: {
|
||||
credentialIds: number[];
|
||||
type: App["type"];
|
||||
slug: AppMeta["slug"];
|
||||
isGlobal?: boolean;
|
||||
installed?: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const [credentialId] = props.credentialIds;
|
||||
const type = props.type;
|
||||
const slug = props.slug;
|
||||
const utils = trpc.useContext();
|
||||
const handleOpenChange = () => {
|
||||
utils.invalidateQueries(["viewer.integrations"]);
|
||||
|
@ -83,7 +83,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
}
|
||||
return (
|
||||
<InstallAppButton
|
||||
type={props.type}
|
||||
slug={props.slug}
|
||||
render={(buttonProps) => (
|
||||
<Button color="secondary" {...buttonProps} data-testid="integration-connection-button">
|
||||
{t("connect")}
|
||||
|
@ -95,7 +95,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
}
|
||||
|
||||
interface IntegrationsContainerProps {
|
||||
variant: App["variant"];
|
||||
variant: AppMeta["variant"];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,7 @@ const IntegrationsContainer = ({ variant, className = "" }: IntegrationsContaine
|
|||
actions={
|
||||
<ConnectOrDisconnectIntegrationButton
|
||||
credentialIds={item.credentialIds}
|
||||
type={item.type}
|
||||
slug={item.appId}
|
||||
isGlobal={item.isGlobal}
|
||||
installed
|
||||
/>
|
||||
|
|
|
@ -708,7 +708,7 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
});
|
||||
|
||||
const integrations = getApps(credentials)
|
||||
.filter((item) => item.type.endsWith("_calendar"))
|
||||
.filter((item) => item.category.includes("calendar"))
|
||||
.map((item) => omit(item, "key"));
|
||||
|
||||
// get user's credentials + their connected integrations
|
||||
|
|
|
@ -58,6 +58,9 @@ async function getUserFromSession({
|
|||
userId: true,
|
||||
appId: true,
|
||||
},
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
|
|
|
@ -619,10 +619,25 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
const { variant, onlyInstalled } = input;
|
||||
const { credentials } = user;
|
||||
|
||||
// TODO: This fn doesn't seem to be used. Verify and remove it.
|
||||
function countActive(items: { credentialIds: unknown[] }[]) {
|
||||
return items.reduce((acc, item) => acc + item.credentialIds.length, 0);
|
||||
}
|
||||
|
||||
let apps = getApps(credentials).map(
|
||||
({ credentials: _, credential: _1 /* don't leak to frontend */, ...app }) => ({
|
||||
...app,
|
||||
credentialIds: credentials.filter((c) => c.type === app.type).map((c) => c.id),
|
||||
credentialIds: credentials
|
||||
.filter((c) => {
|
||||
const slug = app.slug;
|
||||
if (slug === "giphy") {
|
||||
return c.type === "giphy_other";
|
||||
} else if (slug === "slack") {
|
||||
return c.type === "slack_app";
|
||||
}
|
||||
return c.appId === app.slug;
|
||||
})
|
||||
.map((c) => c.id),
|
||||
})
|
||||
);
|
||||
if (variant) {
|
||||
|
|
|
@ -24,7 +24,9 @@
|
|||
"docs-dev": "yarn predev && turbo run dev --scope=\"@calcom/docs\"",
|
||||
"docs-build": "turbo run build --scope=\"@calcom/docs\" --include-dependencies",
|
||||
"docs-start": "turbo run start --scope=\"@calcom/docs\"",
|
||||
"dx": "yarn predev && (git submodule update || true) && turbo run dx",
|
||||
"app-store:watch": "node packages/app-store/app-store.js --watch",
|
||||
"dx:web": "yarn predev && (git submodule update || true) && turbo run dx",
|
||||
"dx": "run-p 'dx:web' 'app-store:watch'",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"heroku-postbuild": "turbo run @calcom/web#build",
|
||||
"lint": "turbo run lint",
|
||||
|
@ -42,13 +44,15 @@
|
|||
"embed-tests": "turbo run embed-tests",
|
||||
"test-e2e": "turbo run test-e2e --concurrency=1",
|
||||
"type-check": "turbo run type-check",
|
||||
"app-store": "yarn workspace @calcom/app-store-cli cli",
|
||||
"embed-tests-prepare": "yarn workspace @calcom/prisma db-reset && yarn build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv-checker": "^1.1.5",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^12.4.1",
|
||||
"prettier": "^2.5.1"
|
||||
"prettier": "^2.5.1",
|
||||
"chokidar": "^3.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"turbo": "1.2.9"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "@calcom/app-store-cli",
|
||||
"version": "0.0.0",
|
||||
"bin": "dist/cli.js",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "ava",
|
||||
"cli": "ts-node --transpile-only src/cli.tsx"
|
||||
},
|
||||
"files": [
|
||||
"dist/cli.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"ink": "^3.2.0",
|
||||
"ink-select-input": "^4.2.1",
|
||||
"ink-text-input": "^4.0.3",
|
||||
"meow": "^9.0.0",
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "^3.0.1",
|
||||
"@types/react": "^18.0.9",
|
||||
"ava": "^4.2.0",
|
||||
"chalk": "^4.1.2",
|
||||
"eslint-config-xo-react": "^0.27.0",
|
||||
"eslint-plugin-react": "^7.30.0",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"ink-testing-library": "^2.1.0",
|
||||
"ts-node": "^10.6.0",
|
||||
"typescript": "^4.6.4",
|
||||
"xo": "^0.39.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
## Steps to create an app
|
||||
|
||||
- Create a folder in packages/app-store/{APP_NAME} = {APP}
|
||||
- Fill it with a sample app
|
||||
- Modify {APP}/_metadata.ts with the data provided
|
||||
|
||||
## Approach
|
||||
|
||||
- appType is derived from App Name(a slugify operation that makes a string that can be used as a director name, a variable name for imports and a URL path).
|
||||
- appType is then used to create the app directory. It becomes `config.type` of config.json. config.type is the value used to create an entry in App table and retrieve any apps or credentials. It also becomes App.dirName
|
||||
- dirnames that don't start with _ are considered apps in packages/app-store and based on those apps .generated.ts* files are created. This allows pre-cli apps to keep on working.
|
||||
- app directory is populated with app-store/_baseApp with newly updated config.json and package.json
|
||||
- `packages/prisma/seed-app-store.config.json` is updated with new app.
|
||||
|
||||
NOTE: After app-store-cli is live, Credential.appId and Credential.type would be same for new apps. For old apps they would remain different. Credential.type would be used to identify credentials in integrations call and Credential.appId/App.slug would be used to identify apps.
|
||||
If we rename all existing apps to their slug names, we can remove type and then use just appId to refer to things everywhere. This can be done later on.
|
||||
|
||||
## TODO
|
||||
|
||||
- Beta Release
|
||||
- Show a warning somewhere that app directory must not be renamed manually, edit command must be used.
|
||||
|
||||
- Improvements
|
||||
- Prefill fields in edit command
|
||||
- Merge app-store:watch and app-store commands, introduce app-store --watch
|
||||
- Allow inputs in non interactive way as well - That would allow easily copy pasting commands.
|
||||
- App already exists check. Ask user to run edit/regenerate command
|
||||
- An app created through CLI should be able to completely skip API validation for testing purposes. Credentials should be created with no API specified specific to the app. It would allow us to test any app end to end not worrying about the corresponding API endpoint.
|
||||
- Require assets path relative to app dir.
|
||||
|
||||
## Roadmap
|
||||
- Someone can add wrong directory name(which doesn't satisfy slug requirements) manually. How to handle it.
|
||||
- Allow editing and updating app from the cal app itself - including assets uploading when developing locally.
|
||||
- Improvements in shared code across app
|
||||
- Use baseApp/api/add.ts for all apps with configuration of credentials creation and redirection URL.
|
||||
- Delete creation side effects if App creation fails - Might make debugging difficult
|
||||
- This is so that web app doesn't break because of additional app folders or faulty db-seed
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
import child_process from "child_process";
|
||||
import fs from "fs";
|
||||
import { Box, Text, useApp, useInput, useStdin } from "ink";
|
||||
import SelectInput from "ink-select-input";
|
||||
import TextInput from "ink-text-input";
|
||||
import path from "path";
|
||||
import React, { FC, useEffect, useRef, useState } from "react";
|
||||
|
||||
const slugify = (str: string) => {
|
||||
// It is to be a valid dir name, a valid JS variable name and a valid URL path
|
||||
return str.replace(/[^a-zA-Z0-9-]/g, "_").toLowerCase();
|
||||
};
|
||||
|
||||
function getSlugFromAppName(appName: string | null): string | null {
|
||||
if (!appName) {
|
||||
return appName;
|
||||
}
|
||||
return slugify(appName);
|
||||
}
|
||||
|
||||
function getAppDirPath(slug: any) {
|
||||
return path.join(appStoreDir, `${slug}`);
|
||||
}
|
||||
|
||||
const appStoreDir = path.resolve(__dirname, "..", "..", "app-store");
|
||||
const workspaceDir = path.resolve(__dirname, "..", "..", "..");
|
||||
const execSync = (...args) => {
|
||||
const result = child_process.execSync(...args).toString();
|
||||
if (process.env.DEBUG === "1") {
|
||||
console.log(`$: ${args[0]}`);
|
||||
console.log(result);
|
||||
}
|
||||
return args[0];
|
||||
};
|
||||
function absolutePath(appRelativePath) {
|
||||
return path.join(appStoreDir, appRelativePath);
|
||||
}
|
||||
const updatePackageJson = ({ slug, appDescription, appDirPath }) => {
|
||||
const packageJsonConfig = JSON.parse(fs.readFileSync(`${appDirPath}/package.json`).toString());
|
||||
packageJsonConfig.name = `@calcom/${slug}`;
|
||||
packageJsonConfig.description = appDescription;
|
||||
// packageJsonConfig.description = `@calcom/${appName}`;
|
||||
fs.writeFileSync(`${appDirPath}/package.json`, JSON.stringify(packageJsonConfig, null, 2));
|
||||
};
|
||||
|
||||
const BaseAppFork = {
|
||||
create: function* ({
|
||||
appType,
|
||||
editMode,
|
||||
appDescription,
|
||||
appName,
|
||||
slug,
|
||||
appTitle,
|
||||
publisherName,
|
||||
publisherEmail,
|
||||
}) {
|
||||
const appDirPath = getAppDirPath(slug);
|
||||
let message = !editMode ? "Forking base app" : "Updating app";
|
||||
yield message;
|
||||
if (!editMode) {
|
||||
execSync(`mkdir -p ${appDirPath}`);
|
||||
execSync(`cp -r ${absolutePath("_baseApp/*")} ${appDirPath}`);
|
||||
}
|
||||
updatePackageJson({ slug, appDirPath, appDescription });
|
||||
|
||||
let config = {
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
name: appName,
|
||||
title: appTitle,
|
||||
// @deprecated - It shouldn't exist.
|
||||
slug: slug,
|
||||
imageSrc: `/api/app-store/${slug}/icon.svg`,
|
||||
logo: `/api/app-store/${slug}/icon.svg`,
|
||||
url: `https://cal.com/apps/${slug}`,
|
||||
variant: appType,
|
||||
publisher: publisherName,
|
||||
email: publisherEmail,
|
||||
description: appDescription,
|
||||
};
|
||||
const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
|
||||
config = {
|
||||
...currentConfig,
|
||||
...config,
|
||||
};
|
||||
fs.writeFileSync(`${appDirPath}/config.json`, JSON.stringify(config, null, 2));
|
||||
message = !editMode ? "Forked base app" : "Updated app";
|
||||
yield message;
|
||||
},
|
||||
delete: function ({ slug }) {
|
||||
const appDirPath = getAppDirPath(slug);
|
||||
execSync(`rm -rf ${appDirPath}`);
|
||||
},
|
||||
};
|
||||
|
||||
const Seed = {
|
||||
seedConfigPath: absolutePath("../prisma/seed-app-store.config.json"),
|
||||
update: function ({ slug, appType, noDbUpdate }) {
|
||||
const seedConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString());
|
||||
if (!seedConfig.find((app) => app.slug === slug)) {
|
||||
seedConfig.push({
|
||||
dirName: slug,
|
||||
categories: [appType],
|
||||
slug: slug,
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
|
||||
if (!noDbUpdate) {
|
||||
execSync(`cd ${workspaceDir} && yarn db-seed`);
|
||||
}
|
||||
},
|
||||
revert: async function ({ slug, noDbUpdate }) {
|
||||
let seedConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString());
|
||||
seedConfig = seedConfig.filter((app) => app.slug !== slug);
|
||||
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
|
||||
if (!noDbUpdate) {
|
||||
execSync(`yarn workspace @calcom/prisma delete-app ${slug}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const generateAppFiles = () => {
|
||||
execSync(`cd ${appStoreDir} && node app-store.js`);
|
||||
};
|
||||
|
||||
const CreateApp = ({ noDbUpdate, editMode = false }) => {
|
||||
// AppName
|
||||
// Type of App - Other, Calendar, Video, Payment, Messaging, Web3
|
||||
const [appInputData, setAppInputData] = useState({});
|
||||
const [inputIndex, setInputIndex] = useState(0);
|
||||
const fields = [
|
||||
{ label: "App Name", name: "appName", type: "text" },
|
||||
{ label: "App Title", name: "appTitle", type: "text" },
|
||||
{ label: "App Description", name: "appDescription", type: "text" },
|
||||
{
|
||||
label: "Type of App",
|
||||
name: "appType",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "calendar", value: "calendar" },
|
||||
{ label: "video", value: "video" },
|
||||
{ label: "payment", value: "payment" },
|
||||
{ label: "messaging", value: "messaging" },
|
||||
{ label: "web3", value: "web3" },
|
||||
{ label: "other", value: "other" },
|
||||
],
|
||||
},
|
||||
{ label: "Publisher Name", name: "publisherName", type: "text" },
|
||||
{ label: "Publisher Email", name: "publisherEmail", type: "text" },
|
||||
];
|
||||
const field = fields[inputIndex];
|
||||
const fieldLabel = field?.label || "";
|
||||
const fieldName = field?.name || "";
|
||||
const fieldValue = appInputData[fieldName] || "";
|
||||
const appName = appInputData["appName"];
|
||||
const appType = appInputData["appType"];
|
||||
const appTitle = appInputData["appTitle"];
|
||||
const appDescription = appInputData["appDescription"];
|
||||
const publisherName = appInputData["publisherName"];
|
||||
const publisherEmail = appInputData["publisherEmail"];
|
||||
const [result, setResult] = useState("...");
|
||||
const slug = getSlugFromAppName(appName);
|
||||
const allFieldsFilled = inputIndex === fields.length;
|
||||
|
||||
useEffect(() => {
|
||||
// When all fields have been filled
|
||||
if (allFieldsFilled) {
|
||||
const it = BaseAppFork.create({
|
||||
appType,
|
||||
appDescription,
|
||||
appName,
|
||||
slug,
|
||||
appTitle,
|
||||
publisherName,
|
||||
publisherEmail,
|
||||
});
|
||||
for (const item of it) {
|
||||
setResult(item);
|
||||
}
|
||||
|
||||
Seed.update({ slug, appType, noDbUpdate });
|
||||
|
||||
generateAppFiles();
|
||||
|
||||
// FIXME: Even after CLI showing this message, it is stuck doing work before exiting
|
||||
// So we ask the user to wait for some time
|
||||
setResult(
|
||||
`App has been given slug: ${slug}. Just wait for a few seconds for the process to complete and start editing ${getAppDirPath(
|
||||
slug
|
||||
)} to work on your app.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (allFieldsFilled) {
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Creating app with name "{appName}" of type "{appType}"
|
||||
</Text>
|
||||
<Text>{result}</Text>
|
||||
<Text>
|
||||
Please note that you should use cli only to rename an app directory as it needs to be updated in DB
|
||||
as well
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Hack: using field.name == "appTitle" to identify that app Name has been submitted and not being edited.
|
||||
if (!editMode && field.name === "appTitle" && slug && fs.existsSync(getAppDirPath(slug))) {
|
||||
return (
|
||||
<>
|
||||
<Text>App with slug {slug} already exists. If you want to edit it, use edit command</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Text color="green">{`${fieldLabel}:`}</Text>
|
||||
{field.type == "text" ? (
|
||||
<TextInput
|
||||
value={fieldValue}
|
||||
onSubmit={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
setInputIndex((index) => {
|
||||
return index + 1;
|
||||
});
|
||||
}}
|
||||
onChange={(value) => {
|
||||
setAppInputData((appInputData) => {
|
||||
return {
|
||||
...appInputData,
|
||||
[fieldName]: value,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SelectInput<string>
|
||||
items={field.options}
|
||||
onSelect={(item) => {
|
||||
setAppInputData((appInputData) => {
|
||||
return {
|
||||
...appInputData,
|
||||
[fieldName]: item.value,
|
||||
};
|
||||
});
|
||||
setInputIndex((index) => {
|
||||
return index + 1;
|
||||
});
|
||||
}}></SelectInput>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteApp = ({ noDbUpdate, slug }) => {
|
||||
const [confirmedAppSlug, setConfirmedAppSlug] = useState("");
|
||||
const [allowDeletion, setAllowDeletion] = useState(false);
|
||||
const [state, setState] = useState({});
|
||||
useEffect(() => {
|
||||
if (allowDeletion) {
|
||||
BaseAppFork.delete({ slug });
|
||||
Seed.revert({ slug });
|
||||
generateAppFiles();
|
||||
setState({ description: `App with slug ${slug} has been deleted`, done: true });
|
||||
}
|
||||
}, [allowDeletion, slug]);
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Confirm the slug of the app that you want to delete. Note, that it would cleanup the app directory,
|
||||
App table and Credential table
|
||||
</Text>
|
||||
{!state.done && (
|
||||
<TextInput
|
||||
value={confirmedAppSlug}
|
||||
onSubmit={(value) => {
|
||||
if (value === slug) {
|
||||
setState({ description: `Deletion started`, done: true });
|
||||
setAllowDeletion(true);
|
||||
} else {
|
||||
setState({ description: `Slug doesn't match - Should have been ${slug}`, done: true });
|
||||
}
|
||||
}}
|
||||
onChange={(val) => {
|
||||
setConfirmedAppSlug(val);
|
||||
}}></TextInput>
|
||||
)}
|
||||
<Text>{state.description}</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const App: FC<{ noDbUpdate?: boolean; command: "create" | "delete"; slug?: string }> = ({
|
||||
command,
|
||||
noDbUpdate,
|
||||
slug,
|
||||
}) => {
|
||||
if (command === "create") {
|
||||
return <CreateApp noDbUpdate={noDbUpdate} />;
|
||||
}
|
||||
if (command === "delete") {
|
||||
return <DeleteApp slug={slug} noDbUpdate={noDbUpdate} />;
|
||||
}
|
||||
if (command === "edit") {
|
||||
return <CreateApp editMode={true} noDbUpdate={noDbUpdate} />;
|
||||
}
|
||||
};
|
||||
module.exports = App;
|
||||
export default App;
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env node
|
||||
import { render } from "ink";
|
||||
import meow from "meow";
|
||||
import React from "react";
|
||||
|
||||
import App from "./CliApp";
|
||||
|
||||
const cli = meow(
|
||||
`
|
||||
Usage
|
||||
$ app-store create/delete
|
||||
|
||||
Options
|
||||
--noDbUpdate Don't update DB. Just generate files.
|
||||
`,
|
||||
{
|
||||
flags: {
|
||||
noDbUpdate: {
|
||||
type: "boolean",
|
||||
},
|
||||
slug: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
allowUnknownFlags: false,
|
||||
}
|
||||
);
|
||||
|
||||
if (cli.input.length !== 1) {
|
||||
cli.showHelp();
|
||||
}
|
||||
|
||||
const command = cli.input[0] as "create" | "delete" | "edit";
|
||||
const supportedCommands = ["create", "delete", "edit"];
|
||||
|
||||
if (!supportedCommands.includes(command)) {
|
||||
cli.showHelp();
|
||||
}
|
||||
|
||||
let slug = null;
|
||||
|
||||
if (command === "delete") {
|
||||
slug = cli.flags.slug;
|
||||
}
|
||||
render(<App slug={slug} command={command} noDbUpdate={cli.flags.noDbUpdate} />);
|
|
@ -0,0 +1 @@
|
|||
export const calRepoPrefix = "/Users/hariombalhara/www/cal.com/"
|
|
@ -0,0 +1,18 @@
|
|||
import test from "ava";
|
||||
import chalk from "chalk";
|
||||
import { render } from "ink-testing-library";
|
||||
import React from "react";
|
||||
|
||||
import App from "./CliApp";
|
||||
|
||||
test("greet unknown user", (t) => {
|
||||
const { lastFrame } = render(<App />);
|
||||
|
||||
t.is(lastFrame(), chalk`Hello, {green Stranger}`);
|
||||
});
|
||||
|
||||
test("greet user with a name", (t) => {
|
||||
const { lastFrame } = render(<App name="Jane" />);
|
||||
|
||||
t.is(lastFrame(), chalk`Hello, {green Jane}`);
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true,
|
||||
"outDir": "dist",
|
||||
"noEmitOnError": false,
|
||||
"target": "ES2020",
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"../../packages/types/*.d.ts",
|
||||
"../../packages/types/next-auth.d.ts",
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx",
|
||||
"../lib/**/*.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
*.generated.*
|
|
@ -1,10 +1,10 @@
|
|||
import prisma from "@calcom/prisma";
|
||||
import { App } from "@calcom/types/App";
|
||||
import { App, AppMeta } from "@calcom/types/App";
|
||||
|
||||
export async function getAppWithMetadata(app: { dirName: string }) {
|
||||
let appMetadata: App | null = null;
|
||||
let appMetadata: AppMeta | null = null;
|
||||
try {
|
||||
appMetadata = (await import(`./${app.dirName}/_metadata`)).default as App;
|
||||
appMetadata = (await import(`./${app.dirName}/_metadata`)).default as AppMeta;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`No metadata found for: "${app.dirName}". Message:`, error.message);
|
||||
|
@ -20,7 +20,8 @@ export async function getAppWithMetadata(app: { dirName: string }) {
|
|||
/** Mainly to use in listings for the frontend, use in getStaticProps or getServerSideProps */
|
||||
export async function getAppRegistry() {
|
||||
const dbApps = await prisma.app.findMany({ select: { dirName: true, slug: true, categories: true } });
|
||||
const apps = [] as Omit<App, "key">[];
|
||||
const apps = [] as App[];
|
||||
const a: App = null;
|
||||
for await (const dbapp of dbApps) {
|
||||
const app = await getAppWithMetadata(dbapp);
|
||||
if (!app) continue;
|
||||
|
@ -29,6 +30,7 @@ export async function getAppRegistry() {
|
|||
// if (!app.installed) return apps;
|
||||
apps.push({
|
||||
...app,
|
||||
...dbapp,
|
||||
installed:
|
||||
true /* All apps from DB are considered installed by default. @TODO: Add and filter our by `enabled` property */,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Base App - App Store CLI
|
||||
|
||||
It contains the boiler plate code for a new app. There is a one time copying of files right now.
|
||||
|
||||
You can read details of how exactly the CLI uses this base app [here](../../app-store-cli/README.md).
|
||||
|
||||
## TODO
|
||||
|
||||
- Rename it _baseApp to convey very clearly that it is not an actual app.
|
|
@ -0,0 +1 @@
|
|||
Edit README.mdx file to update it.
|
|
@ -0,0 +1,20 @@
|
|||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import config from "./config.json";
|
||||
import _package from "./package.json";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
export const metadata = {
|
||||
description: _package.description,
|
||||
category: "other",
|
||||
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
|
||||
installed: true,
|
||||
rating: 0,
|
||||
reviews: 0,
|
||||
trending: true,
|
||||
verified: true,
|
||||
...config,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
|
@ -0,0 +1,51 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
// TODO: There is a lot of code here that would be used by almost all apps
|
||||
// - Login Validation
|
||||
// - Looking up credential.
|
||||
// - Creating credential would be specific to app, so there can be just createCredential method that app can expose
|
||||
// - Redirection after successful installation can also be configured by app
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
}
|
||||
// TODO: Define appType once and import everywhere
|
||||
const slug = appConfig.slug;
|
||||
try {
|
||||
const alreadyInstalled = await prisma.credential.findFirst({
|
||||
where: {
|
||||
appId: slug,
|
||||
userId: req.session.user.id,
|
||||
},
|
||||
});
|
||||
if (alreadyInstalled) {
|
||||
throw new Error("Already installed");
|
||||
}
|
||||
const installation = await prisma.credential.create({
|
||||
data: {
|
||||
// TODO: Why do we need type in Credential? Why can't we simply use appId
|
||||
// Using slug as type for new credentials so that we keep on using type in requests.
|
||||
// `deriveAppKeyFromSlug` should be able to handle old type and new type which is equal to slug
|
||||
type: slug,
|
||||
key: {},
|
||||
userId: req.session.user.id,
|
||||
appId: slug,
|
||||
},
|
||||
});
|
||||
if (!installation) {
|
||||
throw new Error(`Unable to create user credential for ${slug}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
return res.status(500);
|
||||
}
|
||||
|
||||
return res.status(200).json({ url: "/apps/installed" });
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default as add } from "./add";
|
|
@ -0,0 +1,19 @@
|
|||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation(appConfig.slug);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export default function Icon() {
|
||||
return (
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid">
|
||||
<path
|
||||
d="M159.999 128.056a76.55 76.55 0 0 1-4.915 27.024 76.745 76.745 0 0 1-27.032 4.923h-.108c-9.508-.012-18.618-1.75-27.024-4.919A76.557 76.557 0 0 1 96 128.056v-.112a76.598 76.598 0 0 1 4.91-27.02A76.492 76.492 0 0 1 127.945 96h.108a76.475 76.475 0 0 1 27.032 4.923 76.51 76.51 0 0 1 4.915 27.02v.112zm94.223-21.389h-74.716l52.829-52.833a128.518 128.518 0 0 0-13.828-16.349v-.004a129 129 0 0 0-16.345-13.816l-52.833 52.833V1.782A128.606 128.606 0 0 0 128.064 0h-.132c-7.248.004-14.347.62-21.265 1.782v74.716L53.834 23.665A127.82 127.82 0 0 0 37.497 37.49l-.028.02A128.803 128.803 0 0 0 23.66 53.834l52.837 52.833H1.782S0 120.7 0 127.956v.088c0 7.256.615 14.367 1.782 21.289h74.716l-52.837 52.833a128.91 128.91 0 0 0 30.173 30.173l52.833-52.837v74.72a129.3 129.3 0 0 0 21.24 1.778h.181a129.15 129.15 0 0 0 21.24-1.778v-74.72l52.838 52.837a128.994 128.994 0 0 0 16.341-13.82l.012-.012a129.245 129.245 0 0 0 13.816-16.341l-52.837-52.833h74.724c1.163-6.91 1.77-14 1.778-21.24v-.186c-.008-7.24-.615-14.33-1.778-21.24z"
|
||||
fill="#FF4A00"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
export { default as Icon } from "./icon";
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1,3 @@
|
|||
export * as api from "./api";
|
||||
export * as components from "./components";
|
||||
export { metadata } from "./_metadata";
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/cli_base__app_name",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"description": "Your app description goes here.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 73.7" style="enable-background:new 0 0 122.88 73.7" xml:space="preserve"><style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style><g><path class="st0" d="M8.34,0h106.2c4.59,0,8.34,3.77,8.34,8.34v57.02c0,4.56-3.77,8.34-8.34,8.34H8.34C3.77,73.7,0,69.95,0,65.36 V8.34C0,3.75,3.75,0,8.34,0L8.34,0z M18.5,21.47h5.98c3.86,0,6.48,0.18,7.84,0.53c1.36,0.35,2.4,0.93,3.11,1.74 c0.71,0.81,1.16,1.72,1.33,2.71c0.18,0.99,0.26,2.95,0.26,5.86v10.78c0,2.76-0.13,4.6-0.4,5.53c-0.26,0.93-0.71,1.66-1.36,2.18 c-0.64,0.53-1.44,0.89-2.39,1.11c-0.95,0.21-2.38,0.31-4.3,0.31H18.5V21.47L18.5,21.47z M26.49,26.73v20.23 c1.16,0,1.87-0.23,2.13-0.69c0.27-0.46,0.4-1.71,0.4-3.78V30.55c0-1.39-0.04-2.29-0.12-2.68c-0.1-0.39-0.29-0.67-0.61-0.86 C27.96,26.82,27.37,26.73,26.49,26.73L26.49,26.73z M40.68,21.47h13.34v6.16h-5.34v5.83h5v5.86h-5v6.77h5.87v6.15H40.68V21.47 L40.68,21.47z M82.22,21.47v30.76h-6.99V31.47l-2.79,20.76h-4.96l-2.95-20.29v20.29h-6.99V21.47h10.36 c0.31,1.85,0.62,4.03,0.97,6.54l1.1,7.83l1.83-14.37H82.22L82.22,21.47z M104.38,39.48c0,3.09-0.07,5.28-0.21,6.56 c-0.15,1.29-0.6,2.46-1.36,3.53c-0.77,1.06-1.8,1.88-3.11,2.45c-1.31,0.57-2.83,0.86-4.56,0.86c-1.65,0-3.13-0.27-4.44-0.81 c-1.32-0.54-2.37-1.34-3.17-2.42c-0.8-1.08-1.28-2.25-1.43-3.51c-0.15-1.27-0.23-3.48-0.23-6.66v-5.26c0-3.09,0.07-5.28,0.23-6.58 c0.14-1.28,0.59-2.46,1.36-3.52c0.77-1.06,1.8-1.88,3.11-2.45c1.3-0.57,2.82-0.86,4.56-0.86c1.65,0,3.12,0.27,4.43,0.81 c1.31,0.54,2.37,1.35,3.17,2.42c0.79,1.08,1.27,2.25,1.42,3.52c0.15,1.26,0.23,3.48,0.23,6.66V39.48L104.38,39.48z M96.39,29.38 c0-1.44-0.08-2.35-0.24-2.75c-0.15-0.4-0.48-0.6-0.98-0.6c-0.42,0-0.74,0.16-0.96,0.49c-0.23,0.32-0.34,1.27-0.34,2.86v14.35 c0,1.8,0.07,2.9,0.22,3.31c0.15,0.42,0.49,0.62,1.02,0.62c0.55,0,0.89-0.23,1.05-0.72c0.15-0.48,0.23-1.63,0.23-3.45V29.38 L96.39,29.38z"/></g></svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -21,6 +21,6 @@ export const metadata = {
|
|||
variant: "conferencing",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -2,10 +2,12 @@ import { useMutation } from "react-query";
|
|||
|
||||
import type { IntegrationOAuthCallbackState } from "@calcom/app-store/types";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { App } from "@calcom/types/App";
|
||||
import { AppMeta } from "@calcom/types/App";
|
||||
|
||||
function useAddAppMutation(type: App["type"], options?: Parameters<typeof useMutation>[2]) {
|
||||
const appName = type.replace(/_/g, "");
|
||||
function useAddAppMutation(type: AppMeta["type"], options?: Parameters<typeof useMutation>[2]) {
|
||||
// FIXME: Ensure that existing apps keep on working
|
||||
// const appName = type.replace(/_/g, "");
|
||||
const appName = type;
|
||||
const mutation = useMutation(async () => {
|
||||
const state: IntegrationOAuthCallbackState = {
|
||||
returnTo: WEBAPP_URL + "/apps/installed" + location.search,
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
export const apiHandlers = {
|
||||
// examplevideo: import("./_example/api"),
|
||||
applecalendar: import("./applecalendar/api"),
|
||||
caldavcalendar: import("./caldavcalendar/api"),
|
||||
googlecalendar: import("./googlecalendar/api"),
|
||||
hubspotothercalendar: import("./hubspotothercalendar/api"),
|
||||
office365calendar: import("./office365calendar/api"),
|
||||
slackmessaging: import("./slackmessaging/api"),
|
||||
stripepayment: import("./stripepayment/api"),
|
||||
tandemvideo: import("./tandemvideo/api"),
|
||||
vital: import("./vital/api"),
|
||||
zoomvideo: import("@calcom/zoomvideo/api"),
|
||||
office365video: import("@calcom/office365video/api"),
|
||||
wipemycalother: import("./wipemycalother/api"),
|
||||
jitsivideo: import("./jitsivideo/api"),
|
||||
huddle01video: import("./huddle01video/api"),
|
||||
metamask: import("./metamask/api"),
|
||||
giphy: import("./giphy/api"),
|
||||
spacebookingother: import("./spacebooking/api"),
|
||||
// @todo Until we use DB slugs everywhere
|
||||
zapierother: import("./zapier/api"),
|
||||
};
|
||||
|
||||
export default apiHandlers;
|
|
@ -0,0 +1,123 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let isInWatchMode = false;
|
||||
if (process.argv[2] === "--watch") {
|
||||
isInWatchMode = true;
|
||||
}
|
||||
const chokidar = require("chokidar");
|
||||
const { debounce } = require("lodash");
|
||||
|
||||
function getAppName(candidatePath) {
|
||||
function isValidAppName(candidatePath) {
|
||||
if (!candidatePath.startsWith("_") && !candidatePath.includes("/") && !candidatePath.includes("\\")) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidAppName(candidatePath)) {
|
||||
// Already a dirname of an app
|
||||
return candidatePath;
|
||||
}
|
||||
// Get dirname of app from full path
|
||||
const dirName = path.relative(__dirname, candidatePath);
|
||||
return isValidAppName(dirName) ? dirName : null;
|
||||
}
|
||||
|
||||
function generateFiles() {
|
||||
let clientOutput = [`import dynamic from "next/dynamic"`];
|
||||
let serverOutput = [];
|
||||
const appDirs = [];
|
||||
|
||||
fs.readdirSync(`${__dirname}`).forEach(function (dir) {
|
||||
if (fs.statSync(`${__dirname}/${dir}`).isDirectory()) {
|
||||
if (!getAppName(dir)) {
|
||||
return;
|
||||
}
|
||||
appDirs.push(dir);
|
||||
}
|
||||
});
|
||||
|
||||
function forEachAppDir(callback) {
|
||||
for (let i = 0; i < appDirs.length; i++) {
|
||||
callback(appDirs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function getObjectExporter(objectName, { fileToBeImported, importBuilder, entryBuilder }) {
|
||||
const output = [];
|
||||
forEachAppDir((dirName) => {
|
||||
if (fs.existsSync(path.join(dirName, fileToBeImported))) {
|
||||
output.push(importBuilder(dirName));
|
||||
}
|
||||
});
|
||||
|
||||
output.push(`export const ${objectName} = {`);
|
||||
|
||||
forEachAppDir((dirName) => {
|
||||
if (fs.existsSync(path.join(dirName, fileToBeImported))) {
|
||||
output.push(entryBuilder(dirName));
|
||||
}
|
||||
});
|
||||
|
||||
output.push(`};`);
|
||||
return output;
|
||||
}
|
||||
|
||||
serverOutput.push(
|
||||
...getObjectExporter("appStoreMetadata", {
|
||||
fileToBeImported: "_metadata.ts",
|
||||
importBuilder: (dirName) => `import { metadata as ${dirName}_meta } from "./${dirName}/_metadata";`,
|
||||
entryBuilder: (dirName) => `${dirName}:${dirName}_meta,`,
|
||||
})
|
||||
);
|
||||
|
||||
serverOutput.push(
|
||||
...getObjectExporter("apiHandlers", {
|
||||
fileToBeImported: "api/index.ts",
|
||||
importBuilder: (dirName) => `const ${dirName}_api = import("./${dirName}/api");`,
|
||||
entryBuilder: (dirName) => `${dirName}:${dirName}_api,`,
|
||||
})
|
||||
);
|
||||
|
||||
clientOutput.push(
|
||||
...getObjectExporter("InstallAppButtonMap", {
|
||||
fileToBeImported: "components/InstallAppButton.tsx",
|
||||
importBuilder: (dirName) =>
|
||||
`const ${dirName}_installAppButton = dynamic(() =>import("./${dirName}/components/InstallAppButton"));`,
|
||||
entryBuilder: (dirName) => `${dirName}:${dirName}_installAppButton,`,
|
||||
})
|
||||
);
|
||||
|
||||
fs.writeFileSync(`${__dirname}/apps.generated.ts`, serverOutput.join("\n"));
|
||||
fs.writeFileSync(`${__dirname}/apps.components.generated.tsx`, clientOutput.join("\n"));
|
||||
console.log("Generated `apps.generated.ts` and `apps.components.generated.tsx`");
|
||||
}
|
||||
|
||||
const debouncedGenerateFiles = debounce(generateFiles);
|
||||
|
||||
if (isInWatchMode) {
|
||||
chokidar
|
||||
.watch(__dirname)
|
||||
.on("addDir", (dirPath) => {
|
||||
const appName = getAppName(dirPath);
|
||||
if (appName) {
|
||||
console.log(`Added ${appName}`);
|
||||
debouncedGenerateFiles();
|
||||
}
|
||||
})
|
||||
.on("change", (filePath) => {
|
||||
if (filePath.endsWith("config.json")) {
|
||||
console.log("Config file changed");
|
||||
debouncedGenerateFiles();
|
||||
}
|
||||
})
|
||||
.on("unlinkDir", (dirPath) => {
|
||||
const appName = getAppName(dirPath);
|
||||
if (appName) {
|
||||
console.log(`Removed ${appName}`);
|
||||
debouncedGenerateFiles();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
generateFiles();
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -20,6 +20,6 @@ export const metadata = {
|
|||
url: "https://cal.com/",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -20,6 +20,6 @@ export const metadata = {
|
|||
url: "https://cal.com/",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -20,7 +20,7 @@ export const metadata = {
|
|||
url: "https://cal.com/",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export * as api from "./api";
|
||||
export * as components from "./components";
|
||||
|
|
|
@ -1,50 +1,23 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { deriveAppKeyFromSlugOrType } from "@calcom/lib/deriveAppKeyFromSlugOrType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
import { InstallAppButtonMap } from "./apps.components.generated";
|
||||
import { InstallAppButtonProps } from "./types";
|
||||
|
||||
export const InstallAppButtonMap = {
|
||||
// examplevideo: dynamic(() => import("./_example/components/InstallAppButton")),
|
||||
applecalendar: dynamic(() => import("./applecalendar/components/InstallAppButton")),
|
||||
caldavcalendar: dynamic(() => import("./caldavcalendar/components/InstallAppButton")),
|
||||
googlecalendar: dynamic(() => import("./googlecalendar/components/InstallAppButton")),
|
||||
hubspotothercalendar: dynamic(() => import("./hubspotothercalendar/components/InstallAppButton")),
|
||||
office365calendar: dynamic(() => import("./office365calendar/components/InstallAppButton")),
|
||||
slackmessaging: dynamic(() => import("./slackmessaging/components/InstallAppButton")),
|
||||
stripepayment: dynamic(() => import("./stripepayment/components/InstallAppButton")),
|
||||
tandemvideo: dynamic(() => import("./tandemvideo/components/InstallAppButton")),
|
||||
zoomvideo: dynamic(() => import("./zoomvideo/components/InstallAppButton")),
|
||||
office365video: dynamic(() => import("./office365video/components/InstallAppButton")),
|
||||
wipemycalother: dynamic(() => import("./wipemycalother/components/InstallAppButton")),
|
||||
zapier: dynamic(() => import("./zapier/components/InstallAppButton")),
|
||||
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
||||
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
||||
metamask: dynamic(() => import("./metamask/components/InstallAppButton")),
|
||||
giphy: dynamic(() => import("./giphy/components/InstallAppButton")),
|
||||
spacebookingother: dynamic(() => import("./spacebooking/components/InstallAppButton")),
|
||||
vital: dynamic(() => import("./vital/components/InstallAppButton")),
|
||||
};
|
||||
|
||||
export const InstallAppButton = (
|
||||
props: {
|
||||
type: App["type"];
|
||||
slug: AppMeta["slug"];
|
||||
} & InstallAppButtonProps
|
||||
) => {
|
||||
const { status } = useSession();
|
||||
const { t } = useLocale();
|
||||
let appName = props.type.replace(/_/g, "");
|
||||
let InstallAppButtonComponent = InstallAppButtonMap[appName as keyof typeof InstallAppButtonMap];
|
||||
/** So we can either call it by simple name (ex. `slack`, `giphy`) instead of
|
||||
* `slackmessaging`, `giphyother` while maintaining retro-compatibility. */
|
||||
if (!InstallAppButtonComponent) {
|
||||
[appName] = props.type.split("_");
|
||||
InstallAppButtonComponent = InstallAppButtonMap[appName as keyof typeof InstallAppButtonMap];
|
||||
}
|
||||
const key = deriveAppKeyFromSlugOrType(props.slug, InstallAppButtonMap);
|
||||
const InstallAppButtonComponent = InstallAppButtonMap[key as keyof typeof InstallAppButtonMap];
|
||||
if (!InstallAppButtonComponent) return null;
|
||||
if (status === "unauthenticated")
|
||||
return (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { LocationType } from "../locations";
|
||||
import _package from "./package.json";
|
||||
|
@ -8,6 +8,8 @@ export const metadata = {
|
|||
description: _package.description,
|
||||
installed: !!process.env.DAILY_API_KEY,
|
||||
type: "daily_video",
|
||||
//TODO: Remove imports in apps.generated.tsx for uninstallable apps
|
||||
uninstallable: true,
|
||||
imageSrc: "/api/app-store/dailyvideo/icon.svg",
|
||||
variant: "conferencing",
|
||||
url: "https://daily.co",
|
||||
|
@ -25,6 +27,6 @@ export const metadata = {
|
|||
locationType: LocationType.Daily,
|
||||
locationLabel: "Cal Video",
|
||||
key: { apikey: process.env.DAILY_API_KEY },
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
export const metadata = {
|
||||
name: "Giphy",
|
||||
description: _package.description,
|
||||
installed: true,
|
||||
category: "other",
|
||||
// If using static next public folder, can then be referenced from the base URL (/).
|
||||
imageSrc: "/api/app-store/giphy/icon.svg",
|
||||
|
@ -20,6 +21,6 @@ export const metadata = {
|
|||
variant: "other",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
type: appType,
|
||||
key: {},
|
||||
userId: req.session.user.id,
|
||||
appId: "giphy",
|
||||
},
|
||||
});
|
||||
if (!installation) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { validJson } from "@calcom/lib/jsonUtils";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { LocationType } from "../locations";
|
||||
import _package from "./package.json";
|
||||
|
@ -7,6 +7,7 @@ import _package from "./package.json";
|
|||
export const metadata = {
|
||||
name: "Google Calendar",
|
||||
description: _package.description,
|
||||
uninstallable: true,
|
||||
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
|
||||
type: "google_calendar",
|
||||
title: "Google Calendar",
|
||||
|
@ -24,6 +25,6 @@ export const metadata = {
|
|||
email: "help@cal.com",
|
||||
locationType: LocationType.GoogleMeet,
|
||||
locationLabel: "Google Meet",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -33,10 +33,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
let key = "";
|
||||
|
||||
if (code) {
|
||||
const token = await oAuth2Client.getToken(code);
|
||||
// TEMPORARY: REMOVE IT
|
||||
try {
|
||||
if (code) {
|
||||
const token = await oAuth2Client.getToken(code);
|
||||
|
||||
key = token.res?.data;
|
||||
key = token.res?.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
await prisma.credential.create({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { validJson } from "@calcom/lib/jsonUtils";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { LocationType } from "../locations";
|
||||
import _package from "./package.json";
|
||||
|
@ -25,6 +25,6 @@ export const metadata = {
|
|||
email: "help@cal.com",
|
||||
locationType: LocationType.GoogleMeet,
|
||||
locationLabel: "Google Meet",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
export const metadata = {
|
||||
name: "HubSpot CRM",
|
||||
installed: !!process.env.HUBSPOT_CLIENT_ID,
|
||||
description: _package.description,
|
||||
type: "hubspot_other_calendar",
|
||||
imageSrc: "/api/app-store/hubspotothercalendar/icon.svg",
|
||||
|
@ -20,6 +21,6 @@ export const metadata = {
|
|||
title: "HubSpot CRM",
|
||||
trending: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { randomString } from "@calcom/lib/random";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { LocationType } from "../locations";
|
||||
import _package from "./package.json";
|
||||
|
@ -26,6 +26,6 @@ export const metadata = {
|
|||
locationType: LocationType.Huddle01,
|
||||
locationLabel: "Huddle01 Video",
|
||||
key: { apikey: randomString(12) },
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { LocationType } from "../locations";
|
||||
import _package from "./package.json";
|
||||
|
@ -24,6 +24,6 @@ export const metadata = {
|
|||
email: "help@cal.com",
|
||||
locationType: LocationType.Jitsi,
|
||||
locationLabel: "Jitsi Video",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -39,5 +39,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
return res.status(500);
|
||||
}
|
||||
return res.redirect("/apps/installed");
|
||||
return res.status(200).json({ url: "/apps/installed" });
|
||||
}
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import { metadata as applecalendar } from "./applecalendar/_metadata";
|
||||
import { metadata as caldavcalendar } from "./caldavcalendar/_metadata";
|
||||
import { metadata as dailyvideo } from "./dailyvideo/_metadata";
|
||||
import { metadata as giphy } from "./giphy/_metadata";
|
||||
import { metadata as googlecalendar } from "./googlecalendar/_metadata";
|
||||
import { metadata as googlevideo } from "./googlevideo/_metadata";
|
||||
import { metadata as hubspotothercalendar } from "./hubspotothercalendar/_metadata";
|
||||
import { metadata as huddle01video } from "./huddle01video/_metadata";
|
||||
import { metadata as jitsivideo } from "./jitsivideo/_metadata";
|
||||
import { metadata as metamask } from "./metamask/_metadata";
|
||||
import { metadata as office365calendar } from "./office365calendar/_metadata";
|
||||
import { metadata as office365video } from "./office365video/_metadata";
|
||||
import { metadata as slackmessaging } from "./slackmessaging/_metadata";
|
||||
import { metadata as spacebooking } from "./spacebooking/_metadata";
|
||||
import { metadata as stripepayment } from "./stripepayment/_metadata";
|
||||
import { metadata as tandemvideo } from "./tandemvideo/_metadata";
|
||||
import { metadata as vital } from "./vital/_metadata";
|
||||
import { metadata as wipemycalother } from "./wipemycalother/_metadata";
|
||||
import { metadata as zapier } from "./zapier/_metadata";
|
||||
import { metadata as zoomvideo } from "./zoomvideo/_metadata";
|
||||
|
||||
export const appStoreMetadata = {
|
||||
applecalendar,
|
||||
caldavcalendar,
|
||||
dailyvideo,
|
||||
googlecalendar,
|
||||
googlevideo,
|
||||
hubspotothercalendar,
|
||||
huddle01video,
|
||||
jitsivideo,
|
||||
office365calendar,
|
||||
office365video,
|
||||
slackmessaging,
|
||||
stripepayment,
|
||||
spacebooking,
|
||||
tandemvideo,
|
||||
vital,
|
||||
zoomvideo,
|
||||
wipemycalother,
|
||||
metamask,
|
||||
giphy,
|
||||
zapier,
|
||||
};
|
||||
|
||||
export default appStoreMetadata;
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -21,6 +21,6 @@ export const metadata = {
|
|||
variant: "other",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -35,5 +35,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
return res.status(500);
|
||||
}
|
||||
return res.redirect("/apps/installed");
|
||||
return res.status(200).json({ url: "/apps/installed" });
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -19,6 +19,6 @@ export const metadata = {
|
|||
url: "https://cal.com/",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { LocationType } from "../locations";
|
||||
import _package from "./package.json";
|
||||
|
@ -22,6 +22,6 @@ export const metadata = {
|
|||
email: "help@cal.com",
|
||||
locationType: LocationType.Teams,
|
||||
locationLabel: "MS Teams",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -14,11 +14,10 @@ export const metadata = {
|
|||
slug: "slack",
|
||||
title: "Slack App",
|
||||
trending: true,
|
||||
type: "slack_messaging",
|
||||
url: "https://slack.com/",
|
||||
variant: "conferencing",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
width="640"
|
||||
height="360"
|
||||
src="https://www.loom.com/embed/f8d2cd9b2ac74f0c916f20c4441bd1da"
|
||||
frameborder="0"
|
||||
webkitallowfullscreen
|
||||
mozallowfullscreen
|
||||
allowfullscreen></iframe>
|
||||
frameBorder="0"
|
||||
webkitallowfullscreen="true"
|
||||
mozallowfullscreen="true"
|
||||
allowFullScreen></iframe>
|
||||
|
||||
Looking to honor May 4th? Search no further. Download this app to make your booking success page resemble a long time ago in a galaxy far far away.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -21,6 +21,6 @@ export const metadata = {
|
|||
variant: "other",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -25,6 +25,6 @@ export const metadata = {
|
|||
variant: "payment",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { LocationType } from "../locations";
|
||||
import _package from "./package.json";
|
||||
|
@ -23,6 +23,6 @@ export const metadata = {
|
|||
email: "help@cal.com",
|
||||
locationType: LocationType.Tandem,
|
||||
locationLabel: "Tandem Video",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { LocationType } from "./locations";
|
||||
import { getAppRegistry } from "./_appRegistry";
|
||||
// If you import this file on any app it should produce circular dependency
|
||||
// import appStore from "./index";
|
||||
import { appStoreMetadata } from "./metadata";
|
||||
import { appStoreMetadata } from "./apps.generated";
|
||||
import { LocationType } from "./locations";
|
||||
|
||||
const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => {
|
||||
store[key] = appStoreMetadata[key as keyof typeof appStoreMetadata];
|
||||
return store;
|
||||
}, {} as Record<string, App>);
|
||||
}, {} as Record<string, AppMeta>);
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true, key: true, userId: true, appId: true },
|
||||
});
|
||||
|
||||
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
||||
type CredentialData = Omit<Prisma.CredentialGetPayload<typeof credentialData>, "type">;
|
||||
|
||||
export const ALL_APPS = Object.values(ALL_APPS_MAP);
|
||||
|
||||
|
@ -55,34 +56,35 @@ export function getLocationOptions(integrations: AppMeta, t: TFunction) {
|
|||
* This should get all available apps to the user based on his saved
|
||||
* credentials, this should also get globally available apps.
|
||||
*/
|
||||
function getApps(userCredentials: CredentialData[]) {
|
||||
const apps = ALL_APPS.map((appMeta) => {
|
||||
const credentials = userCredentials.filter((credential) => credential.type === appMeta.type);
|
||||
async function getApps(userCredentials: CredentialData[]) {
|
||||
const appsWithMeta = await getAppRegistry();
|
||||
const apps = appsWithMeta.map((app) => {
|
||||
const credentials = userCredentials.filter((credential) => credential.appId === app.slug);
|
||||
let locationOption: OptionTypeBase | null = null;
|
||||
|
||||
/** If the app is a globally installed one, let's inject it's key */
|
||||
if (appMeta.isGlobal) {
|
||||
if (app.isGlobal) {
|
||||
credentials.push({
|
||||
id: +new Date().getTime(),
|
||||
type: appMeta.type,
|
||||
key: appMeta.key!,
|
||||
key: app.key!,
|
||||
userId: +new Date().getTime(),
|
||||
appId: appMeta.slug,
|
||||
appId: app.slug,
|
||||
});
|
||||
}
|
||||
|
||||
/** Check if app has location option AND add it if user has credentials for it */
|
||||
if (credentials.length > 0 && appMeta?.locationType) {
|
||||
if (credentials.length > 0 && app?.locationType) {
|
||||
locationOption = {
|
||||
value: appMeta.locationType,
|
||||
label: appMeta.locationLabel || "No label set",
|
||||
value: app.locationType,
|
||||
label: app.locationLabel || "No label set",
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
const credential: typeof credentials[number] | null = credentials[0] || null;
|
||||
return {
|
||||
...appMeta,
|
||||
...app,
|
||||
appId: app.slug,
|
||||
/**
|
||||
* @deprecated use `credentials`
|
||||
*/
|
||||
|
@ -96,12 +98,6 @@ function getApps(userCredentials: CredentialData[]) {
|
|||
return apps;
|
||||
}
|
||||
|
||||
export type AppMeta = ReturnType<typeof getApps>;
|
||||
|
||||
export function hasIntegrationInstalled(type: App["type"]): boolean {
|
||||
return ALL_APPS.some((app) => app.type === type && !!app.installed);
|
||||
}
|
||||
|
||||
export function getLocationTypes(): string[] {
|
||||
return ALL_APPS.reduce((locations, app) => {
|
||||
if (typeof app.locationType === "string") {
|
||||
|
@ -113,13 +109,13 @@ export function getLocationTypes(): string[] {
|
|||
|
||||
export function getLocationLabels(t: TFunction) {
|
||||
const defaultLocationLabels = defaultLocations.reduce((locations, location) => {
|
||||
if(location.label === "attendee_phone_number") {
|
||||
locations[location.value] = t("your_number")
|
||||
return locations
|
||||
if (location.label === "attendee_phone_number") {
|
||||
locations[location.value] = t("your_number");
|
||||
return locations;
|
||||
}
|
||||
if(location.label === "host_phone_number") {
|
||||
locations[location.value] = `${t("phone_call")} (${t("number_provided")})`
|
||||
return locations
|
||||
if (location.label === "host_phone_number") {
|
||||
locations[location.value] = `${t("phone_call")} (${t("number_provided")})`;
|
||||
return locations;
|
||||
}
|
||||
locations[location.value] = t(location.label);
|
||||
return locations;
|
||||
|
@ -137,16 +133,4 @@ export function getAppName(name: string): string | null {
|
|||
return ALL_APPS_MAP[name as keyof typeof ALL_APPS_MAP]?.name ?? null;
|
||||
}
|
||||
|
||||
export function getAppType(name: string): string {
|
||||
const type = ALL_APPS_MAP[name as keyof typeof ALL_APPS_MAP].type;
|
||||
|
||||
if (type.endsWith("_calendar")) {
|
||||
return "Calendar";
|
||||
}
|
||||
if (type.endsWith("_payment")) {
|
||||
return "Payment";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
export default getApps;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -22,6 +22,6 @@ export const metadata = {
|
|||
variant: "other",
|
||||
verified: true,
|
||||
email: "support@tryvital.io",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -21,6 +21,6 @@ export const metadata = {
|
|||
variant: "other",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
|
@ -20,6 +20,6 @@ export const metadata = {
|
|||
variant: "other",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Toaster } from "react-hot-toast";
|
|||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { App } from "@calcom/prisma/client";
|
||||
import { Button, Loader, Tooltip } from "@calcom/ui";
|
||||
|
||||
/** TODO: Maybe extract this into a package to prevent circular dependencies */
|
||||
|
@ -29,7 +30,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
|||
|
||||
const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete");
|
||||
const zapierCredentials: { credentialIds: number[] } | undefined = integrations.data?.items.find(
|
||||
(item: { type: string }) => item.type === "zapier_other"
|
||||
(item: { appId: App["slug"] }) => item.appId === ZAPIER
|
||||
);
|
||||
const [credentialId] = zapierCredentials?.credentialIds || [false];
|
||||
const showContent = integrations.data && integrations.isSuccess && credentialId;
|
||||
|
@ -49,7 +50,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
|||
|
||||
if (integrations.isLoading) {
|
||||
return (
|
||||
<div className="flex absolute z-50 h-screen w-full items-center bg-gray-200">
|
||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-200">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
|
@ -75,7 +76,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
|||
) : (
|
||||
<>
|
||||
<div className="mt-1 text-xl">{t("your_unique_api_key")}</div>
|
||||
<div className="flex my-2 mt-3">
|
||||
<div className="my-2 mt-3 flex">
|
||||
<div className="mr-1 w-full rounded bg-gray-100 p-3 pr-5">{newApiKey}</div>
|
||||
<Tooltip content="copy to clipboard">
|
||||
<Button
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { LocationType } from "../locations";
|
||||
import _package from "./package.json";
|
||||
|
@ -22,6 +22,6 @@ export const metadata = {
|
|||
email: "help@cal.com",
|
||||
locationType: LocationType.Zoom,
|
||||
locationLabel: "Zoom Video",
|
||||
} as App;
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -17,7 +17,9 @@ export { getCalendar };
|
|||
|
||||
export const getCalendarCredentials = (credentials: Array<Credential>, userId: number) => {
|
||||
const calendarCredentials = getApps(credentials)
|
||||
.filter((app) => app.type.endsWith("_calendar"))
|
||||
.filter((app) => {
|
||||
return app.category.includes("calendar");
|
||||
})
|
||||
.flatMap((app) => {
|
||||
const credentials = app.credentials.flatMap((credential) => {
|
||||
const calendar = getCalendar(credential);
|
||||
|
|
|
@ -110,8 +110,8 @@ export default class EventManager {
|
|||
*/
|
||||
constructor(user: EventManagerUser) {
|
||||
const appCredentials = getApps(user.credentials).flatMap((app) => app.credentials);
|
||||
this.calendarCredentials = appCredentials.filter((cred) => cred.type.endsWith("_calendar"));
|
||||
this.videoCredentials = appCredentials.filter((cred) => cred.type.endsWith("_video"));
|
||||
this.calendarCredentials = appCredentials.filter((cred) => cred.app.category.includes("calendar"));
|
||||
this.videoCredentials = appCredentials.filter((cred) => cred.app.category.includes("video"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
export function deriveAppKeyFromSlugOrType(slugOrType, map) {
|
||||
const oldTypes = ["video", "other", "calendar", "web3", "payment", "messaging"];
|
||||
|
||||
// slack has a bug where in config slack_app is the type(to match Credential.type) but directory is `slackmessaging`
|
||||
// We can't derive slackmessaging from slack_app without hardcoding it.
|
||||
if (slugOrType === "slack_app") {
|
||||
return "slackmessaging";
|
||||
}
|
||||
let handlers = map[slugOrType];
|
||||
|
||||
if (handlers) {
|
||||
return slugOrType;
|
||||
}
|
||||
|
||||
// There can be two types of legacy types
|
||||
// - zoom_video
|
||||
// - zoomvideo
|
||||
// Transform `zoom_video` to `zoomvideo`;
|
||||
slugOrType = slugOrType.split("_").join("");
|
||||
handlers = map[slugOrType];
|
||||
|
||||
if (handlers) {
|
||||
return slugOrType;
|
||||
}
|
||||
|
||||
// Instead of doing a blind split at _ and using the first part, apply this hack only on strings that match legacy type.
|
||||
// Transform zoomvideo to zoom
|
||||
oldTypes.some((type) => {
|
||||
const matcher = new RegExp(`(.+)${type}$`);
|
||||
if (slugOrType.match(matcher)) {
|
||||
slugOrType = slugOrType.replace(matcher, "$1");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return slugOrType;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
export const slugify = (str: string) => {
|
||||
return str.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
|
||||
return str.replace(/[^a-zA-Z0-9-]/g, "_").toLowerCase();
|
||||
};
|
||||
|
||||
export default slugify;
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import prisma from ".";
|
||||
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
// TODO: Put some restrictions here to run it on local DB only.
|
||||
// Production DB currently doesn't support app deletion
|
||||
async function main() {
|
||||
const appId = process.argv[2];
|
||||
try {
|
||||
await prisma.app.delete({
|
||||
where: {
|
||||
slug: appId,
|
||||
},
|
||||
});
|
||||
await prisma.credential.deleteMany({
|
||||
where: {
|
||||
appId: appId,
|
||||
},
|
||||
});
|
||||
console.log(`Deleted app from DB: '${appId}'`);
|
||||
} catch (e) {
|
||||
if (e.code === "P2025") {
|
||||
console.log(`App '${appId}' already deleted from DB`);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
|
@ -16,7 +16,8 @@
|
|||
"dx": "yarn db-setup",
|
||||
"generate-schemas": "prisma generate && prisma format",
|
||||
"post-install": "yarn generate-schemas",
|
||||
"seed-app-store": "ts-node --transpile-only ./seed-app-store.ts"
|
||||
"seed-app-store": "ts-node --transpile-only ./seed-app-store.ts",
|
||||
"delete-app": "ts-node --transpile-only ./delete-app.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"npm-run-all": "^4.1.5",
|
||||
|
|
|
@ -81,11 +81,13 @@ model EventType {
|
|||
|
||||
model Credential {
|
||||
id Int @id @default(autoincrement())
|
||||
// To be dropped, it is not being used.
|
||||
type String
|
||||
key Json
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int?
|
||||
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
|
||||
// How to make it a required column?
|
||||
appId String?
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -1,4 +1,6 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import prisma from ".";
|
||||
|
||||
|
@ -20,7 +22,7 @@ async function createApp(
|
|||
update: { dirName, categories, keys },
|
||||
});
|
||||
await prisma.credential.updateMany({
|
||||
where: { type },
|
||||
where: { appId: slug },
|
||||
data: { appId: slug },
|
||||
});
|
||||
console.log(`📲 Upserted app: '${slug}'`);
|
||||
|
@ -131,6 +133,14 @@ async function main() {
|
|||
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
});
|
||||
}
|
||||
|
||||
const generatedApps = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "seed-app-store.config.json"), "utf8")
|
||||
);
|
||||
for (let i = 0; i < generatedApps.length; i++) {
|
||||
const generatedApp = generatedApps[i];
|
||||
await createApp(generatedApp.slug, generatedApp.dirName, generatedApp.categories, generatedApp.type);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import type { App as AppFromPrisma, Prisma } from "@prisma/client";
|
||||
|
||||
import type { LocationType } from "@calcom/app-store/locations";
|
||||
|
||||
|
@ -6,22 +6,13 @@ import type { LocationType } from "@calcom/app-store/locations";
|
|||
* This is the definition for an app store's app metadata.
|
||||
* This is used to display App info, categorize or hide certain apps in the app store.
|
||||
*/
|
||||
export interface App {
|
||||
export interface AppMeta {
|
||||
/**
|
||||
* @deprecated
|
||||
* Wheter if the app is installed or not. Usually we check for api keys in env
|
||||
* variables to determine if this is true or not.
|
||||
* */
|
||||
installed?: boolean;
|
||||
/** The app type */
|
||||
type:
|
||||
| `${string}_calendar`
|
||||
| `${string}_messaging`
|
||||
| `${string}_payment`
|
||||
| `${string}_video`
|
||||
| `${string}_web3`
|
||||
| `${string}_other`
|
||||
| `${string}_other_calendar`;
|
||||
/** The display name for the app, TODO settle between this or name */
|
||||
title: string;
|
||||
/** The display name for the app */
|
||||
|
@ -65,8 +56,6 @@ export interface App {
|
|||
locationLabel?: string;
|
||||
/** Needed API Keys (usually for global apps) */
|
||||
key?: Prisma.JsonValue;
|
||||
/** Needed API Keys (usually for global apps) */
|
||||
key?: Prisma.JsonValue;
|
||||
/** If not free, what kind of fees does the app have */
|
||||
feeType?: "monthly" | "usage-based" | "one-time" | "free";
|
||||
/** 0 = free. if type="usage-based" it's the price per booking */
|
||||
|
@ -74,3 +63,5 @@ export interface App {
|
|||
/** only required for "usage-based" billing. % of commission for paid bookings */
|
||||
commission?: number;
|
||||
}
|
||||
|
||||
export type App = AppMeta | AppFromPrisma;
|
||||
|
|
Loading…
Reference in New Issue