2022-03-23 22:00:30 +00:00
import { Prisma } from "@prisma/client" ;
2022-03-31 21:29:03 +00:00
import { TFunction } from "next-i18next" ;
2022-10-14 16:24:43 +00:00
import { z } from "zod" ;
2022-03-23 22:00:30 +00:00
2022-10-14 16:24:43 +00:00
import { defaultLocations , EventLocationType } from "@calcom/app-store/locations" ;
import { EventTypeModel } from "@calcom/prisma/zod" ;
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils" ;
2022-08-26 00:48:50 +00:00
import type { App , AppMeta } from "@calcom/types/App" ;
2022-03-23 22:00:30 +00:00
2022-04-15 02:04:21 +00:00
// If you import this file on any app it should produce circular dependency
// import appStore from "./index";
2022-10-14 18:51:15 +00:00
import { appStoreMetadata } from "./apps.metadata.generated" ;
2022-03-23 22:00:30 +00:00
2022-10-14 16:24:43 +00:00
export type EventTypeApps = NonNullable < NonNullable < z.infer < typeof EventTypeMetaDataSchema > > [ "apps" ] > ;
export type EventTypeAppsList = keyof EventTypeApps ;
2022-04-15 02:04:21 +00:00
const ALL_APPS_MAP = Object . keys ( appStoreMetadata ) . reduce ( ( store , key ) = > {
store [ key ] = appStoreMetadata [ key as keyof typeof appStoreMetadata ] ;
2022-09-02 19:00:41 +00:00
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
delete store [ key ] [ "/*" ] ;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
delete store [ key ] [ "__createdUsingCli" ] ;
2022-03-23 22:00:30 +00:00
return store ;
2022-08-26 00:48:50 +00:00
} , { } as Record < string , AppMeta > ) ;
2022-03-23 22:00:30 +00:00
const credentialData = Prisma . validator < Prisma.CredentialArgs > ( ) ( {
2022-05-02 20:39:35 +00:00
select : { id : true , type : true , key : true , userId : true , appId : true } ,
2022-03-23 22:00:30 +00:00
} ) ;
type CredentialData = Prisma . CredentialGetPayload < typeof credentialData > ;
2022-09-15 19:53:09 +00:00
export enum InstalledAppVariants {
"conferencing" = "conferencing" ,
"calendar" = "calendar" ,
"payment" = "payment" ,
2022-10-14 16:24:43 +00:00
"analytics" = "analytics" ,
"automation" = "automation" ,
2022-09-15 19:53:09 +00:00
"other" = "other" ,
}
2022-03-23 22:00:30 +00:00
export const ALL_APPS = Object . values ( ALL_APPS_MAP ) ;
type OptionTypeBase = {
label : string ;
2022-08-26 00:48:50 +00:00
value : EventLocationType [ "type" ] ;
2022-03-23 22:00:30 +00:00
disabled? : boolean ;
} ;
2022-03-31 21:29:03 +00:00
function translateLocations ( locations : OptionTypeBase [ ] , t : TFunction ) {
return locations . map ( ( l ) = > ( {
. . . l ,
label : t ( l . label ) ,
} ) ) ;
}
2022-08-26 00:48:50 +00:00
export function getLocationOptions ( integrations : ReturnType < typeof getApps > , t : TFunction ) {
const locations : OptionTypeBase [ ] = [ ] ;
defaultLocations . forEach ( ( l ) = > {
locations . push ( {
label : l.label ,
value : l.type ,
} ) ;
} ) ;
2022-03-23 22:00:30 +00:00
integrations . forEach ( ( app ) = > {
if ( app . locationOption ) {
2022-04-15 17:29:45 +00:00
locations . push ( app . locationOption ) ;
2022-03-23 22:00:30 +00:00
}
} ) ;
2022-04-15 17:29:45 +00:00
return translateLocations ( locations , t ) ;
2022-03-23 22:00:30 +00:00
}
2022-11-24 11:53:29 +00:00
export function getLocationGroupedOptions ( integrations : ReturnType < typeof getApps > , t : TFunction ) {
const apps : Record < string , { label : string ; value : string ; disabled ? : boolean ; icon ? : string } [ ] > = { } ;
integrations . forEach ( ( app ) = > {
if ( app . locationOption ) {
const category = app . category ;
const option = { . . . app . locationOption , icon : app.imageSrc } ;
if ( apps [ category ] ) {
apps [ category ] = [ . . . apps [ category ] , option ] ;
} else {
apps [ category ] = [ option ] ;
}
}
} ) ;
defaultLocations . forEach ( ( l ) = > {
const category = l . category ;
if ( apps [ category ] ) {
apps [ category ] = [
. . . apps [ category ] ,
{
label : l.label ,
value : l.type ,
icon : l.iconUrl ,
} ,
] ;
} else {
apps [ category ] = [
{
label : l.label ,
value : l.type ,
icon : l.iconUrl ,
} ,
] ;
}
} ) ;
const locations = [ ] ;
// Translating labels and pushing into array
for ( const category in apps ) {
const tmp = { label : category , options : apps [ category ] } ;
if ( tmp . label === "in person" ) {
tmp . options . map ( ( l ) = > ( { . . . l , label : t ( l . value ) } ) ) ;
} else {
tmp . options . map ( ( l ) = > ( {
. . . l ,
label : t ( l . label . toLowerCase ( ) . split ( " " ) . join ( "_" ) ) ,
} ) ) ;
}
tmp . label = t ( tmp . label ) ;
locations . push ( tmp ) ;
}
return locations ;
}
2022-03-23 22:00:30 +00:00
/ * *
2022-04-15 02:24:27 +00:00
* This should get all available apps to the user based on his saved
2022-03-23 22:00:30 +00:00
* 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 ) ;
let locationOption : OptionTypeBase | null = null ;
/** If the app is a globally installed one, let's inject it's key */
if ( appMeta . isGlobal ) {
credentials . push ( {
id : + new Date ( ) . getTime ( ) ,
type : appMeta . type ,
2022-10-14 16:24:43 +00:00
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2022-03-23 22:00:30 +00:00
key : appMeta.key ! ,
userId : + new Date ( ) . getTime ( ) ,
2022-05-02 20:39:35 +00:00
appId : appMeta.slug ,
2022-03-23 22:00:30 +00:00
} ) ;
}
/** Check if app has location option AND add it if user has credentials for it */
2022-08-26 00:48:50 +00:00
if ( credentials . length > 0 && appMeta ? . appData ? . location ) {
2022-03-23 22:00:30 +00:00
locationOption = {
2022-08-26 00:48:50 +00:00
value : appMeta.appData.location.type ,
label : appMeta.appData.location.label || "No label set" ,
2022-03-23 22:00:30 +00:00
disabled : false ,
} ;
}
const credential : typeof credentials [ number ] | null = credentials [ 0 ] || null ;
return {
. . . appMeta ,
/ * *
* @deprecated use ` credentials `
* /
credential ,
credentials ,
/** Option to display in `location` field while editing event types */
locationOption ,
} ;
} ) ;
return apps ;
}
export function hasIntegrationInstalled ( type : App [ "type" ] ) : boolean {
return ALL_APPS . some ( ( app ) = > app . type === type && ! ! app . installed ) ;
}
2022-04-26 04:20:13 +00:00
export function getAppName ( name : string ) : string | null {
return ALL_APPS_MAP [ name as keyof typeof ALL_APPS_MAP ] ? . name ? ? null ;
2022-03-23 22:00:30 +00:00
}
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" ;
}
2022-10-14 16:24:43 +00:00
export const getEventTypeAppData = < T extends EventTypeAppsList > (
eventType : Pick < z.infer < typeof EventTypeModel > , "price" | "currency" | "metadata" > ,
appId : T ,
forcedGet? : boolean
) : EventTypeApps [ T ] = > {
const metadata = eventType . metadata ;
const appMetadata = metadata ? . apps && metadata . apps [ appId ] ;
if ( appMetadata ) {
const allowDataGet = forcedGet ? true : appMetadata . enabled ;
return allowDataGet ? appMetadata : null ;
}
// Backward compatibility for existing event types.
// TODO: After the new AppStore EventType App flow is stable, write a migration to migrate metadata to new format which will let us remove this compatibility code
// Migration isn't being done right now, to allow a revert if needed
const legacyAppsData = {
stripe : {
enabled : eventType.price > 0 ,
// Price default is 0 in DB. So, it would always be non nullish.
price : eventType.price ,
// Currency default is "usd" in DB.So, it would also be available always
currency : eventType.currency ,
} ,
rainbow : {
enabled : ! ! ( eventType . metadata ? . smartContractAddress && eventType . metadata ? . blockchainId ) ,
smartContractAddress : eventType.metadata?.smartContractAddress || "" ,
blockchainId : eventType.metadata?.blockchainId || 0 ,
} ,
giphy : {
enabled : ! ! eventType . metadata ? . giphyThankYouPage ,
thankYouPage : eventType.metadata?.giphyThankYouPage || "" ,
} ,
} as const ;
// TODO: This assertion helps typescript hint that only one of the app's data can be returned
const legacyAppData = legacyAppsData [ appId as Extract < T , keyof typeof legacyAppsData > ] ;
const allowDataGet = forcedGet ? true : legacyAppData ? . enabled ;
return allowDataGet ? legacyAppData : null ;
} ;
2022-03-23 22:00:30 +00:00
export default getApps ;