diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts index a2dc6a7b03f5d0..09ee2e4a77e7a7 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts @@ -10,7 +10,12 @@ import { RecurringBookingOutput_2024_08_13, } from "@calcom/platform-types"; -@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +@ApiExtraModels( + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, + GetSeatedBookingOutput_2024_08_13, + GetRecurringSeatedBookingOutput_2024_08_13 +) export class CancelBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts index 1eb72319bb1db2..058e97f656aeba 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts @@ -10,7 +10,12 @@ import { CreateRecurringSeatedBookingOutput_2024_08_13, } from "@calcom/platform-types"; -@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +@ApiExtraModels( + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, + CreateSeatedBookingOutput_2024_08_13, + CreateRecurringSeatedBookingOutput_2024_08_13 +) export class CreateBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) @@ -21,6 +26,8 @@ export class CreateBookingOutput_2024_08_13 { oneOf: [ { $ref: getSchemaPath(BookingOutput_2024_08_13) }, { type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } }, + { $ref: getSchemaPath(CreateSeatedBookingOutput_2024_08_13) }, + { type: "array", items: { $ref: getSchemaPath(CreateRecurringSeatedBookingOutput_2024_08_13) } }, ], description: "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects", diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts index 61a2f70ef271e1..c32c52a3102c8a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts @@ -12,7 +12,12 @@ import { RecurringBookingOutput_2024_08_13, } from "@calcom/platform-types"; -@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +@ApiExtraModels( + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, + CreateSeatedBookingOutput_2024_08_13, + CreateRecurringSeatedBookingOutput_2024_08_13 +) export class RescheduleBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts index 1cc0d182645f5d..a9e361797e6251 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts @@ -162,7 +162,11 @@ export class OAuthClientUsersController { @Post("/:userId/force-refresh") @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: "Force refresh tokens" }) + @ApiOperation({ + summary: "Force refresh tokens", + description: `If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials. + Each access token is valid for 60 minutes and each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have \`calAccessToken\` and \`calRefreshToken\` columns.`, + }) @MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER]) async forceRefresh( @Param("userId") userId: number, diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts index 8e77d625df5972..565d7ac94f2f18 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts @@ -30,6 +30,7 @@ import { ApiExcludeEndpoint as DocsExcludeEndpoint, ApiBadRequestResponse as DocsBadRequestResponse, ApiHeader as DocsHeader, + ApiOperation, } from "@nestjs/swagger"; import { Response as ExpressResponse } from "express"; @@ -39,7 +40,6 @@ import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants"; path: "/v2/oauth/:clientId", version: API_VERSIONS_VALUES, }) -@DocsTags("OAuth") export class OAuthFlowController { constructor( private readonly oauthClientRepository: OAuthClientRepository, @@ -115,13 +115,18 @@ export class OAuthFlowController { @Post("/refresh") @HttpCode(HttpStatus.OK) @UseGuards(ApiAuthGuard) - @DocsTags("Managed users") + @DocsTags("Platform / Managed Users") @DocsHeader({ name: X_CAL_SECRET_KEY, description: "OAuth client secret key.", required: true, }) - async refreshAccessToken( + @ApiOperation({ + summary: "Refresh managed user tokens", + description: `If managed user access token is expired then get a new one using this endpoint. Each access token is valid for 60 minutes and + each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have \`calAccessToken\` and \`calRefreshToken\` columns.`, + }) + async refreshTokens( @Param("clientId") clientId: string, @Headers(X_CAL_SECRET_KEY) secretKey: string, @Body() body: RefreshTokenInput diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index d43e9d260dc080..3e5fcdb13610b2 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -362,6 +362,7 @@ "post": { "operationId": "OAuthClientUsersController_forceRefresh", "summary": "Force refresh tokens", + "description": "If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials.\n Each access token is valid for 60 minutes and each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have `calAccessToken` and `calRefreshToken` columns.", "parameters": [ { "name": "userId", @@ -397,6 +398,57 @@ ] } }, + "/v2/oauth/{clientId}/refresh": { + "post": { + "operationId": "OAuthFlowController_refreshTokens", + "summary": "Refresh managed user tokens", + "description": "If managed user access token is expired then get a new one using this endpoint. Each access token is valid for 60 minutes and \n each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have `calAccessToken` and `calRefreshToken` columns.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "required": true, + "in": "header", + "description": "OAuth client secret key.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + } + }, + "tags": [ + "Platform / Managed Users" + ] + } + }, "/v2/oauth-clients/{clientId}/webhooks": { "post": { "operationId": "OAuthClientWebhooksController_createOAuthClientWebhook", @@ -4439,56 +4491,6 @@ ] } }, - "/v2/oauth/{clientId}/refresh": { - "post": { - "operationId": "OAuthFlowController_refreshAccessToken", - "parameters": [ - { - "name": "clientId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "x-cal-secret-key", - "required": true, - "in": "header", - "description": "OAuth client secret key.", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefreshTokenInput" - } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeysResponseDto" - } - } - } - } - }, - "tags": [ - "OAuth", - "Managed users" - ] - } - }, "/v2/schedules": { "post": { "operationId": "SchedulesController_2024_06_11_createSchedule", @@ -6532,7 +6534,7 @@ "rolling": { "type": "boolean", "example": true, - "description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date." + "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " } }, "required": [ @@ -6560,7 +6562,7 @@ "rolling": { "type": "boolean", "example": true, - "description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date." + "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " } }, "required": [ @@ -13338,35 +13340,616 @@ "recurringBookingUid" ] }, - "CreateBookingOutput_2024_08_13": { + "SeatedAttendee": { "type": "object", "properties": { - "status": { + "name": { "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "example": "John Doe" }, - "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/BookingOutput_2024_08_13" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" - } - } + "timeZone": { + "type": "string", + "example": "America/New_York" + }, + "language": { + "type": "string", + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" ], - "description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects" + "example": "en" + }, + "absent": { + "type": "boolean", + "example": false + }, + "seatUid": { + "type": "string", + "example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.", + "example": { + "customField": "customValue" + } + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } } }, "required": [ - "status", - "data" + "name", + "timeZone", + "absent", + "seatUid" + ] + }, + "CreateSeatedBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "title": { + "type": "string", + "example": "Consultation" + }, + "description": { + "type": "string", + "example": "Learn how to integrate scheduling into marketplace." + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending" + ], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 50, + "deprecated": true, + "description": "Deprecated - rely on 'eventType' object containing the id instead." + }, + "eventType": { + "$ref": "#/components/schemas/EventType" + }, + "meetingUrl": { + "type": "string", + "description": "Deprecated - rely on 'location' field instead.", + "example": "https://example.com/recurring-meeting", + "deprecated": true + }, + "location": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + }, + "createdAt": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } + }, + "seatUid": { + "type": "string", + "example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeatedAttendee" + } + } + }, + "required": [ + "id", + "uid", + "title", + "description", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "eventType", + "absentHost", + "createdAt", + "seatUid", + "attendees" + ] + }, + "CreateRecurringSeatedBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "title": { + "type": "string", + "example": "Consultation" + }, + "description": { + "type": "string", + "example": "Learn how to integrate scheduling into marketplace." + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending" + ], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 50, + "deprecated": true, + "description": "Deprecated - rely on 'eventType' object containing the id instead." + }, + "eventType": { + "$ref": "#/components/schemas/EventType" + }, + "meetingUrl": { + "type": "string", + "description": "Deprecated - rely on 'location' field instead.", + "example": "https://example.com/recurring-meeting", + "deprecated": true + }, + "location": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + }, + "createdAt": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } + }, + "seatUid": { + "type": "string", + "example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeatedAttendee" + } + }, + "recurringBookingUid": { + "type": "string", + "example": "recurring_uid_987" + } + }, + "required": [ + "id", + "uid", + "title", + "description", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "eventType", + "absentHost", + "createdAt", + "seatUid", + "attendees", + "recurringBookingUid" + ] + }, + "CreateBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + }, + { + "$ref": "#/components/schemas/CreateSeatedBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetSeatedBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "title": { + "type": "string", + "example": "Consultation" + }, + "description": { + "type": "string", + "example": "Learn how to integrate scheduling into marketplace." + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending" + ], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 50, + "deprecated": true, + "description": "Deprecated - rely on 'eventType' object containing the id instead." + }, + "eventType": { + "$ref": "#/components/schemas/EventType" + }, + "meetingUrl": { + "type": "string", + "description": "Deprecated - rely on 'location' field instead.", + "example": "https://example.com/recurring-meeting", + "deprecated": true + }, + "location": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + }, + "createdAt": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeatedAttendee" + } + } + }, + "required": [ + "id", + "uid", + "title", + "description", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "eventType", + "absentHost", + "createdAt", + "attendees" + ] + }, + "GetRecurringSeatedBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "title": { + "type": "string", + "example": "Consultation" + }, + "description": { + "type": "string", + "example": "Learn how to integrate scheduling into marketplace." + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending" + ], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 50, + "deprecated": true, + "description": "Deprecated - rely on 'eventType' object containing the id instead." + }, + "eventType": { + "$ref": "#/components/schemas/EventType" + }, + "meetingUrl": { + "type": "string", + "description": "Deprecated - rely on 'location' field instead.", + "example": "https://example.com/recurring-meeting", + "deprecated": true + }, + "location": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + }, + "createdAt": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeatedAttendee" + } + }, + "recurringBookingUid": { + "type": "string", + "example": "recurring_uid_987" + } + }, + "required": [ + "id", + "uid", + "title", + "description", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "eventType", + "absentHost", + "createdAt", + "attendees", + "recurringBookingUid" ] }, "GetBookingOutput_2024_08_13": { diff --git a/docs/api-reference/v2/introduction.mdx b/docs/api-reference/v2/introduction.mdx index de7402cedf1fa9..4f40b7f7bd844a 100644 --- a/docs/api-reference/v2/introduction.mdx +++ b/docs/api-reference/v2/introduction.mdx @@ -5,7 +5,17 @@ description: 'Introduction to Cal.com API v2 endpoints' ## Authentication -The Cal.com API uses API keys to authenticate requests. You can view and manage your API keys in your settings page under the security tab in Cal.com. +The Cal.com API has 3 authentication methods: + +1. API key +2. Platform OAuth client credentials +3. Managed user access token + +If you are a platform customer you don't need an API key and must instead use OAuth credentials or a managed user access token. We cover when to use which below. + +### 1. API key + +You can view and manage your API keys in your settings page under the security tab in Cal.com. @@ -24,3 +34,40 @@ Authentication to the API is performed via the Authorization header. For example in your request header. All API requests must be made over HTTPS. Calls made over plain HTTP will fail. API requests without authentication will also fail. + +### 2. OAuth client credentials +You need to use OAuth credentials when: + +1. Managing managed users [API reference](https://cal.com/docs/api-reference/v2/platform-managed-users/create-a-managed-user) +2. Creating OAuth client webhooks [API reference](https://cal.com/docs/api-reference/v2/platform-webhooks/create-a-webhook) +3. Refreshing tokens of a managed user [API reference](https://cal.com/docs/api-reference/v2/oauth/post-v2oauth-refresh) +4. Teams related endpoints: Managing organization teams [API reference](https://cal.com/docs/api-reference/v2/orgs-teams/create-a-team), adding managed users as members to teams [API reference](https://cal.com/docs/api-reference/v2/orgs-teams-memberships/create-a-membership), creating team event types [API reference](https://cal.com/docs/api-reference/v2/orgs-event-types/create-an-event-type). + +OAuth credentials can be accessed in the platform dashboard https://app.cal.com/settings/platform after you have created an OAuth client. Each one has an ID and secret. You then need to pass them as request headers: + +1. `x-cal-client-id` - ID of the OAuth client. +2. `x-cal-secret-key` - secret of the OAuth client. + +### 3. Managed user access token + +After you create a managed user you will receive its access and refresh tokens. The response also includes managed user's id, so we recommend you to add new properties to your users table calAccessToken, calRefreshToken and calManagedUserId to store this information. + +You need to use access token when managing managed user's: +1. Schedules [API reference](https://cal.com/docs/api-reference/v2/schedules/create-a-schedule) +2. Event types [API reference](https://cal.com/docs/api-reference/v2/event-types/create-an-event-type) +3. Bookings - some endpoints like creating a booking is public, but some like getting all managed user's bookings require managed user's access token [API reference](https://cal.com/docs/api-reference/v2/bookings/get-all-bookings) + +It is passed as an authorization bearer request header Authorization: Bearer \. + +Validity period: access tokens are valid for 60 minutes and refresh tokens for 1 year, and tokens can be refreshed using the refresh endpoint [API reference](https://cal.com/docs/api-reference/v2/oauth/post-v2oauth-refresh). After refreshing you will receive the new access and refresh tokens that you have to store in your database. + +Recovering tokens: if you ever lose managed user's access or refresh tokens, you can force refresh them using the OAuth client credentials and store them in your database [API reference](https://cal.com/docs/api-reference/v2/platform-managed-users/force-refresh-tokens). + +## Navigating endpoints +Platform customers have the following endpoints available: + +1. Endpoints prefixed with "Platform". +2. Endpoints with no prefix e.g "Bookings", "Event Types". +3. If you are at least on the ESSENTIALS plan, then all endpoints prefixed with "Orgs" except "Orgs / Attributes". + +Non-platform customers have all the endpoints except the ones prefixed with "Platform". \ No newline at end of file diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 6e70a7e37e9410..53275302fb7db6 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -342,6 +342,7 @@ "post": { "operationId": "OAuthClientUsersController_forceRefresh", "summary": "Force refresh tokens", + "description": "If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials.\n Each access token is valid for 60 minutes and each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have `calAccessToken` and `calRefreshToken` columns.", "parameters": [ { "name": "userId", @@ -375,6 +376,55 @@ "tags": ["Platform / Managed Users"] } }, + "/v2/oauth/{clientId}/refresh": { + "post": { + "operationId": "OAuthFlowController_refreshTokens", + "summary": "Refresh managed user tokens", + "description": "If managed user access token is expired then get a new one using this endpoint. Each access token is valid for 60 minutes and \n each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have `calAccessToken` and `calRefreshToken` columns.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "required": true, + "in": "header", + "description": "OAuth client secret key.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + } + }, + "tags": ["Platform / Managed Users"] + } + }, "/v2/oauth-clients/{clientId}/webhooks": { "post": { "operationId": "OAuthClientWebhooksController_createOAuthClientWebhook", @@ -4207,53 +4257,6 @@ "tags": ["Me"] } }, - "/v2/oauth/{clientId}/refresh": { - "post": { - "operationId": "OAuthFlowController_refreshAccessToken", - "parameters": [ - { - "name": "clientId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "x-cal-secret-key", - "required": true, - "in": "header", - "description": "OAuth client secret key.", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefreshTokenInput" - } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeysResponseDto" - } - } - } - } - }, - "tags": ["OAuth", "Managed users"] - } - }, "/v2/schedules": { "post": { "operationId": "SchedulesController_2024_06_11_createSchedule", @@ -6064,7 +6067,7 @@ "rolling": { "type": "boolean", "example": true, - "description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date." + "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " } }, "required": ["type", "value"] @@ -6085,7 +6088,7 @@ "rolling": { "type": "boolean", "example": true, - "description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date." + "description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n " } }, "required": ["type", "value"] @@ -11992,30 +11995,586 @@ "recurringBookingUid" ] }, - "CreateBookingOutput_2024_08_13": { + "SeatedAttendee": { "type": "object", "properties": { - "status": { + "name": { "type": "string", - "example": "success", - "enum": ["success", "error"] + "example": "John Doe" }, - "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/BookingOutput_2024_08_13" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" - } - } + "timeZone": { + "type": "string", + "example": "America/New_York" + }, + "language": { + "type": "string", + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" ], - "description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects" + "example": "en" + }, + "absent": { + "type": "boolean", + "example": false + }, + "seatUid": { + "type": "string", + "example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.", + "example": { + "customField": "customValue" + } + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } } }, - "required": ["status", "data"] + "required": ["name", "timeZone", "absent", "seatUid"] + }, + "CreateSeatedBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "title": { + "type": "string", + "example": "Consultation" + }, + "description": { + "type": "string", + "example": "Learn how to integrate scheduling into marketplace." + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": ["cancelled", "accepted", "rejected", "pending"], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 50, + "deprecated": true, + "description": "Deprecated - rely on 'eventType' object containing the id instead." + }, + "eventType": { + "$ref": "#/components/schemas/EventType" + }, + "meetingUrl": { + "type": "string", + "description": "Deprecated - rely on 'location' field instead.", + "example": "https://example.com/recurring-meeting", + "deprecated": true + }, + "location": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + }, + "createdAt": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } + }, + "seatUid": { + "type": "string", + "example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeatedAttendee" + } + } + }, + "required": [ + "id", + "uid", + "title", + "description", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "eventType", + "absentHost", + "createdAt", + "seatUid", + "attendees" + ] + }, + "CreateRecurringSeatedBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "title": { + "type": "string", + "example": "Consultation" + }, + "description": { + "type": "string", + "example": "Learn how to integrate scheduling into marketplace." + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": ["cancelled", "accepted", "rejected", "pending"], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 50, + "deprecated": true, + "description": "Deprecated - rely on 'eventType' object containing the id instead." + }, + "eventType": { + "$ref": "#/components/schemas/EventType" + }, + "meetingUrl": { + "type": "string", + "description": "Deprecated - rely on 'location' field instead.", + "example": "https://example.com/recurring-meeting", + "deprecated": true + }, + "location": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + }, + "createdAt": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } + }, + "seatUid": { + "type": "string", + "example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeatedAttendee" + } + }, + "recurringBookingUid": { + "type": "string", + "example": "recurring_uid_987" + } + }, + "required": [ + "id", + "uid", + "title", + "description", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "eventType", + "absentHost", + "createdAt", + "seatUid", + "attendees", + "recurringBookingUid" + ] + }, + "CreateBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + }, + { + "$ref": "#/components/schemas/CreateSeatedBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects" + } + }, + "required": ["status", "data"] + }, + "GetSeatedBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "title": { + "type": "string", + "example": "Consultation" + }, + "description": { + "type": "string", + "example": "Learn how to integrate scheduling into marketplace." + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": ["cancelled", "accepted", "rejected", "pending"], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 50, + "deprecated": true, + "description": "Deprecated - rely on 'eventType' object containing the id instead." + }, + "eventType": { + "$ref": "#/components/schemas/EventType" + }, + "meetingUrl": { + "type": "string", + "description": "Deprecated - rely on 'location' field instead.", + "example": "https://example.com/recurring-meeting", + "deprecated": true + }, + "location": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + }, + "createdAt": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeatedAttendee" + } + } + }, + "required": [ + "id", + "uid", + "title", + "description", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "eventType", + "absentHost", + "createdAt", + "attendees" + ] + }, + "GetRecurringSeatedBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "title": { + "type": "string", + "example": "Consultation" + }, + "description": { + "type": "string", + "example": "Learn how to integrate scheduling into marketplace." + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": ["cancelled", "accepted", "rejected", "pending"], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 50, + "deprecated": true, + "description": "Deprecated - rely on 'eventType' object containing the id instead." + }, + "eventType": { + "$ref": "#/components/schemas/EventType" + }, + "meetingUrl": { + "type": "string", + "description": "Deprecated - rely on 'location' field instead.", + "example": "https://example.com/recurring-meeting", + "deprecated": true + }, + "location": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + }, + "createdAt": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "metadata": { + "type": "object", + "example": { + "key": "value" + } + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SeatedAttendee" + } + }, + "recurringBookingUid": { + "type": "string", + "example": "recurring_uid_987" + } + }, + "required": [ + "id", + "uid", + "title", + "description", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "eventType", + "absentHost", + "createdAt", + "attendees", + "recurringBookingUid" + ] }, "GetBookingOutput_2024_08_13": { "type": "object", diff --git a/docs/images/platform/guides/booking-fields/custom-fields/booker.png b/docs/images/platform/guides/booking-fields/custom-fields/booker.png new file mode 100644 index 00000000000000..923b6dc7342140 Binary files /dev/null and b/docs/images/platform/guides/booking-fields/custom-fields/booker.png differ diff --git a/docs/images/platform/guides/booking-fields/custom-fields/prefilled.png b/docs/images/platform/guides/booking-fields/custom-fields/prefilled.png new file mode 100644 index 00000000000000..e266b904a55cd1 Binary files /dev/null and b/docs/images/platform/guides/booking-fields/custom-fields/prefilled.png differ diff --git a/docs/images/platform/guides/booking-fields/custom-fields/request.png b/docs/images/platform/guides/booking-fields/custom-fields/request.png new file mode 100644 index 00000000000000..730eecd49c7199 Binary files /dev/null and b/docs/images/platform/guides/booking-fields/custom-fields/request.png differ diff --git a/docs/images/platform/guides/booking-fields/default-fields/booker.png b/docs/images/platform/guides/booking-fields/default-fields/booker.png new file mode 100644 index 00000000000000..34c2ddabc966de Binary files /dev/null and b/docs/images/platform/guides/booking-fields/default-fields/booker.png differ diff --git a/docs/images/platform/guides/booking-fields/read-only/prefilled.png b/docs/images/platform/guides/booking-fields/read-only/prefilled.png new file mode 100644 index 00000000000000..4a004eff55241c Binary files /dev/null and b/docs/images/platform/guides/booking-fields/read-only/prefilled.png differ diff --git a/docs/images/platform/guides/booking-fields/read-only/request.png b/docs/images/platform/guides/booking-fields/read-only/request.png new file mode 100644 index 00000000000000..bcb4b5fb96b7c8 Binary files /dev/null and b/docs/images/platform/guides/booking-fields/read-only/request.png differ diff --git a/docs/images/platform/guides/replacing-toasts/availability-settings.png b/docs/images/platform/guides/replacing-toasts/availability-settings.png new file mode 100644 index 00000000000000..eda6529f59db6d Binary files /dev/null and b/docs/images/platform/guides/replacing-toasts/availability-settings.png differ diff --git a/docs/mint.json b/docs/mint.json index 17f3e124477b4d..69c256fad13f5c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -199,8 +199,16 @@ "platform/atoms/payment-form" ] }, + { + "group": "Guides", + "icon": "book", + "pages": [ + "platform/guides/replacing-toasts", + "platform/guides/booking-fields", + "platform/guides/booking-redirects" + ] + }, "platform/quickstart", - "platform/booking-redirects", "platform/faq" ] }, diff --git a/docs/platform/guides/booking-fields.mdx b/docs/platform/guides/booking-fields.mdx new file mode 100644 index 00000000000000..c706ba634ec676 --- /dev/null +++ b/docs/platform/guides/booking-fields.mdx @@ -0,0 +1,62 @@ +--- +title: Managing booking fields +description: Prefilling and / or making them read-only. +--- + +## Prefilling default booking fields +If you want to pre-fill name and email fields you can pass them to the Booker atom's defaultFormValues prop: +```js + + +``` + +which will look like: + + +## Prefilling custom booking fields + +We created an event type for managed user with custom text and select fields. Name and email fields are created by default. Here is the request: + + +That looks in booker like: + + +Let's say you want that when someone is trying to book one of your managed users that the +booking fields should be pre-filled with some data. We see that one of the booking fields has slug `coding-language` and the other +`help-with`. To pre-fill them you pass their slugs and values to the Booker atom's defaultFormValues prop: + +```js + +``` + +which now will populat the booking fields: + + +## Making pre-filled fields read-only +`disableOnPrefill` booking field property controlls whether or not not make a booking field read only if we pass its value in the defaultFormValues prop. Here is an example request +where we create an event type with one booking field with "disableOnPrefill": true and the other with "disableOnPrefill": false: + + + +If we pass their values to the Booker atom: +```js + +``` + +then here is how it will look like in the Booker. The "What language will we peer code in" text field is read only: + diff --git a/docs/platform/booking-redirects.mdx b/docs/platform/guides/booking-redirects.mdx similarity index 62% rename from docs/platform/booking-redirects.mdx rename to docs/platform/guides/booking-redirects.mdx index 7b756c4b491f24..a7b072fbb347bc 100644 --- a/docs/platform/booking-redirects.mdx +++ b/docs/platform/guides/booking-redirects.mdx @@ -1,7 +1,6 @@ --- title: Booking redirects description: Find out how to manage the booking flow. -"icon": "clipboard" --- When creating an OAuth client you can specify: @@ -22,18 +21,25 @@ Page in the booking URL will take URL parameter provided by us and then hook to 1. Pass `my-app.com/bookings` as the redirectURI. 2. In your app, create `my-app.com/bookings/[bookingUid]` page where bookingUid will become path parameter. 3. When a booking occurs, booker will be re-directed to the redirectURI with booking UID as the bookingUid parameter aka my-app.com/bookings/[bookingUid]. -4. In the my-app.com/bookings/[bookingUid] route create a page that imports `useGetBooking` hook, then extract bookingUid from URL parameter, and uses the hook to display booking information: +4. In the my-app.com/bookings/[bookingUid] route create a page that imports `useBooking` hook, then extract bookingUid from URL parameter, and uses the hook to display booking information. Because +`useBooking` hook can return individual booking or an array of recurring bookings, you need to handle single booking and array of bookings cases separately: ```js -import { useGetBooking } from "@calcom/atoms"; +import { useBooking } from "@calcom/atoms"; export default function Bookings(props: { calUsername: string; calEmail: string }) { const router = useRouter(); - const { isLoading, data: booking, refetch } = useGetBooking((router.query.bookingUid as string) ?? ""); + const { isLoading, data: booking, refetch } = useBooking((router.query.bookingUid as string) ?? ""); return ( -

{booking.title}

+ if (!Array.isArray(booking)) { +

{booking.title}

+ } else { + {for (const recurrence of booking) { +

{recurrence.title}

+ }} + } ) }; ``` @@ -76,30 +82,47 @@ const { mutate: cancelBooking } = useCancelBooking({ }); ``` -4. Create a cancel button that invokes mutation returned by the “useCancelBooking”. You have to pass id of the booking which you can get by fetching booking using "useGetBooking" hook by uid from the query params. Provide a suitable “cancellationReason”. +4. Create a cancel button that invokes mutation returned by the “useCancelBooking”. You have to pass id of the booking which you can get by fetching booking using "useBooking" hook by uid from the query params. Provide a suitable “cancellationReason”. +Because `useBooking` hook can return individual booking or an array of recurring bookings, you need to handle single booking and array of bookings cases separately: ```js -import { useGetBooking, useCancelBooking } from "@calcom/atoms"; +import { useBooking, useCancelBooking } from "@calcom/atoms"; ... -const { isLoading, data: booking, refetch } = useGetBooking((router.query.bookingUid as string) ?? ""); +const { isLoading, data: booking, refetch } = useBooking((router.query.bookingUid as string) ?? ""); const { mutate: cancelBooking } = useCancelBooking({ onSuccess: () => { refetch(); }, }); ... - - + return ( + if (!Array.isArray(booking)) { + + } else { + {for (const recurrence of booking) { + + }} + } + ) ``` An example implementation can be found [here](https://github.com/calcom/cal.com/blob/main/packages/platform/examples/base/src/pages/booking.tsx). diff --git a/docs/platform/guides/replacing-toasts.mdx b/docs/platform/guides/replacing-toasts.mdx new file mode 100644 index 00000000000000..8d6b594c9ae7ec --- /dev/null +++ b/docs/platform/guides/replacing-toasts.mdx @@ -0,0 +1,33 @@ +--- +title: Custom toasts +description: Replace default cal.com toasts with your own +--- + +It is possible to disable default toasts for the `AvailabilitySettings` and `EventTypeSettings` atoms to then use hooks +and listen for updates to then render your own toasts. + +If we use the `AvailabilitySettings` atom, make changes and press "Save" a confirmation appears: + + + +We can disable them using `disableToasts` prop: + +```js + +``` + +Now when changes happen and Save is clicked the toast will not appear. + +You can then create functions and pass them to hooks: +```js + +``` +in case of success response is passed to your functions and in case of error and error. \ No newline at end of file diff --git a/docs/platform/introduction.mdx b/docs/platform/introduction.mdx index 1ce6d9ea6de433..d460ad5d929c10 100644 --- a/docs/platform/introduction.mdx +++ b/docs/platform/introduction.mdx @@ -13,6 +13,9 @@ icon: "presentation-screen" Customizable UI components handling scheduling on behalf of your users + + Learn how to create specific things + Learn how to use Cal.com's API to CRUD various Cal resources programmatically diff --git a/packages/platform/types/bookings/2024-08-13/outputs/get-booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/get-booking.output.ts index c0b1c45fd50313..c2851d826d18d7 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/get-booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/get-booking.output.ts @@ -10,7 +10,12 @@ import { GetRecurringSeatedBookingOutput_2024_08_13, } from "@calcom/platform-types"; -@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +@ApiExtraModels( + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, + GetSeatedBookingOutput_2024_08_13, + GetRecurringSeatedBookingOutput_2024_08_13 +) export class GetBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) diff --git a/packages/platform/types/bookings/2024-08-13/outputs/get-bookings.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/get-bookings.output.ts index 7a1122473210fb..3d8ed2e594dbd1 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/get-bookings.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/get-bookings.output.ts @@ -9,7 +9,12 @@ import { RecurringBookingOutput_2024_08_13, } from "@calcom/platform-types"; -@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +@ApiExtraModels( + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, + GetSeatedBookingOutput_2024_08_13, + GetRecurringSeatedBookingOutput_2024_08_13 +) export class GetBookingsOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-window.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-window.input.ts index 307b4ad6d10551..d3ad6704b26ebf 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-window.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-window.input.ts @@ -38,6 +38,18 @@ class BookingWindowBase { type!: BookingWindowPeriodInputType_2024_06_14; } +const rollingDescription = ` + Determines the behavior of the booking window: + - If **true**, the window is rolling. This means the number of available days will always equal the specified 'value' + and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, + a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days + becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible. + - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the + moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current + date is November 10, the booker will only see slots on November 11 because the window is restricted to the next + 3 calendar days (November 10–12). + `; + // Separate classes for different value types export class BusinessDaysWindow_2024_06_14 extends BookingWindowBase { @IsNumber() @@ -49,8 +61,7 @@ export class BusinessDaysWindow_2024_06_14 extends BookingWindowBase { @IsBoolean() @ApiPropertyOptional({ example: true, - description: - "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date.", + description: rollingDescription, }) rolling?: boolean; @@ -69,8 +80,7 @@ export class CalendarDaysWindow_2024_06_14 extends BookingWindowBase { @IsBoolean() @ApiPropertyOptional({ example: true, - description: - "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date.", + description: rollingDescription, }) rolling?: boolean;