Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b34ef206f8 | ||
|
|
1c3c661726 | ||
|
|
3dbad3113d | ||
|
|
b1173fc8c3 | ||
|
|
1c193cca86 | ||
|
|
17af50bbdc | ||
|
|
5242e419d9 | ||
|
|
5d65a0f091 | ||
|
|
4081d11fbe | ||
|
|
f883dee084 | ||
|
|
e3a9f54ba5 | ||
|
|
c9abc556ba | ||
|
|
9855176948 | ||
|
|
48cbb94af2 | ||
|
|
8238d4ffcd | ||
|
|
0a84ce5c73 | ||
|
|
7e73d675d7 | ||
|
|
648ad72a54 | ||
|
|
f00be08e8c | ||
|
|
f1e8e3fa58 | ||
|
|
4dbe044ecc | ||
|
|
f1ae5f727a | ||
|
|
7ee01c0487 | ||
|
|
05537780b8 | ||
|
|
3e29904d7d | ||
|
|
06f4b25080 | ||
|
|
79fc0c8437 | ||
|
|
a2b25fa615 | ||
|
|
14c151be76 | ||
|
|
1aae57daae | ||
|
|
12e95a1f47 | ||
|
|
60b6086221 | ||
|
|
5d65df9c05 | ||
|
|
66ce20245c | ||
|
|
c321a6c07a | ||
|
|
0eb2c15b39 | ||
|
|
9d29055808 | ||
|
|
74f3608b42 | ||
|
|
9bfa4165b9 | ||
|
|
8a96a45424 | ||
|
|
0787c4890e | ||
|
|
51e902f03e | ||
|
|
9b1bf29ecf | ||
|
|
ad65fbbd81 | ||
|
|
eef47ddefd | ||
|
|
8908a66c23 | ||
|
|
a78a3ffe39 | ||
|
|
a6a428c8cb | ||
|
|
217c6e6a76 | ||
|
|
b1eb5a2809 | ||
|
|
bd5d94a4ba | ||
|
|
3cfe295ed5 | ||
|
|
20dcef6680 | ||
|
|
11b65b295b | ||
|
|
4c73695d3a | ||
|
|
ab4eff1fe1 | ||
|
|
38b43f75ba | ||
|
|
9603831def | ||
|
|
2bc17313fd | ||
|
|
3a7122d613 | ||
|
|
21d28c9747 | ||
|
|
f8a93414a5 | ||
|
|
85e1ed3ab5 | ||
|
|
4647333839 | ||
|
|
9d4cb08c55 | ||
|
|
773fab858f | ||
|
|
ea0c92a267 | ||
|
|
2abefc412c | ||
|
|
9d4b12b8d3 | ||
|
|
c21281a55c | ||
|
|
f5a813f531 | ||
|
|
fa53f54815 | ||
|
|
f07bed1a2a | ||
|
|
227ed64b49 | ||
|
|
237d7e95c4 | ||
|
|
896dfd50fd | ||
|
|
e2119879ae | ||
|
|
7aefefc31f | ||
|
|
1b6b91b0ad | ||
|
|
50c7210d6b | ||
|
|
1bb4b206c6 | ||
|
|
6386b2ec95 | ||
|
|
68abc63fe4 | ||
|
|
bdeb4b1dc9 | ||
|
|
1541730886 | ||
|
|
4c6af5659a | ||
|
|
a9951df501 | ||
|
|
68663231d8 | ||
|
|
d3bbed01f6 | ||
|
|
9dfcb0d482 | ||
|
|
240405f849 | ||
|
|
bc9a9c0c24 | ||
|
|
f8abce44cb | ||
|
|
1368ffe55d | ||
|
|
397bf3cc0c | ||
|
|
98ec38aeaa | ||
|
|
d52e2d9fad | ||
|
|
bf348ee209 | ||
|
|
635c1febb0 |
-57
@@ -1,57 +0,0 @@
|
||||
# EAS Build ignore file for monorepo
|
||||
# Only upload the companion app folder
|
||||
|
||||
# ===========================================
|
||||
# IGNORE EVERYTHING AT ROOT LEVEL
|
||||
# ===========================================
|
||||
/*
|
||||
|
||||
# ===========================================
|
||||
# EXCEPT THE COMPANION APP
|
||||
# ===========================================
|
||||
!companion
|
||||
|
||||
# ===========================================
|
||||
# BUT IGNORE THESE INSIDE COMPANION
|
||||
# ===========================================
|
||||
|
||||
# Native folders - EAS generates these during builds (~2.1 GB)
|
||||
companion/ios
|
||||
companion/android
|
||||
|
||||
# Dependencies - EAS installs these during builds (~1.6 GB)
|
||||
companion/node_modules
|
||||
|
||||
# Expo build cache
|
||||
companion/.expo
|
||||
companion/dist
|
||||
companion/web-build
|
||||
|
||||
# Metro
|
||||
companion/.metro-health-check*
|
||||
|
||||
# WXT Chrome Extension outputs (not needed for mobile builds)
|
||||
companion/.output
|
||||
companion/.wxt
|
||||
companion/dev
|
||||
|
||||
# Kotlin cache
|
||||
companion/.kotlin
|
||||
|
||||
# Build artifacts
|
||||
companion/*.jks
|
||||
companion/*.p8
|
||||
companion/*.p12
|
||||
companion/*.key
|
||||
companion/*.mobileprovision
|
||||
|
||||
# Debug logs
|
||||
companion/*.log
|
||||
|
||||
# TypeScript build info
|
||||
companion/*.tsbuildinfo
|
||||
|
||||
# Environment files
|
||||
companion/.env
|
||||
companion/.env.*
|
||||
!companion/.env.example
|
||||
@@ -209,11 +209,13 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
|
||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_TEAM_ANNUAL_PRICE_ID=
|
||||
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
|
||||
ORG_MONTHLY_CREDITS=
|
||||
STRIPE_TEAM_PRODUCT_ID=
|
||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_ANNUAL_PRICE_ID=
|
||||
STRIPE_ORG_PRODUCT_ID=
|
||||
# Used to pass trial days for orgs during the Stripe checkout session
|
||||
STRIPE_ORG_TRIAL_DAYS=
|
||||
|
||||
@@ -37,3 +37,11 @@
|
||||
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
||||
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
||||
/packages/ui/**/* @calcom/Consumer @calcom/Foundation
|
||||
|
||||
# Exclude test files from requiring foundation/consumer approval
|
||||
**/*.spec.*
|
||||
**/*.e2e.*
|
||||
**/*.e2e-spec.*
|
||||
**/*.test.*
|
||||
**/*.test-suite.*
|
||||
**/*.integration-test.*
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
name: Companion (iOS/Android/Extension)
|
||||
about: Report bugs or request features for mobile apps and browser extension
|
||||
title: ""
|
||||
labels: ["companion", "🚨 needs approval"]
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
### Issue Type
|
||||
|
||||
<!--
|
||||
Select one:
|
||||
- Bug Report
|
||||
- Feature Request
|
||||
-->
|
||||
|
||||
- [ ] 🐛 Bug Report
|
||||
- [ ] ✨ Feature Request
|
||||
|
||||
### Platform
|
||||
|
||||
<!--
|
||||
Select all that apply:
|
||||
-->
|
||||
|
||||
- [ ] 📱 iOS App
|
||||
- [ ] 🤖 Android App
|
||||
- [ ] 🧩 Browser Extension
|
||||
|
||||
### Platform Details
|
||||
|
||||
<!--
|
||||
Fill in the details for your selected platform(s).
|
||||
Delete sections that don't apply.
|
||||
-->
|
||||
|
||||
#### iOS
|
||||
- **iOS Version**: (e.g., 17.2)
|
||||
- **Device Model**: (e.g., iPhone 15 Pro)
|
||||
- **App Version**: (e.g., 1.0.0)
|
||||
|
||||
#### Android
|
||||
- **Android Version**: (e.g., 14)
|
||||
- **Device Model**: (e.g., Pixel 8)
|
||||
- **App Version**: (e.g., 1.0.0)
|
||||
|
||||
#### Browser Extension
|
||||
- **Browser & Version**: (e.g., Chrome 120, Firefox 121)
|
||||
- **Extension Version**: (e.g., 1.0.0)
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
<!--
|
||||
Provide a clear and concise description of the issue or feature.
|
||||
-->
|
||||
|
||||
(Write your description here.)
|
||||
|
||||
### Steps to Reproduce (Bug Reports Only)
|
||||
|
||||
<!--
|
||||
If this is a bug report, provide steps to reproduce the issue.
|
||||
Delete this section for feature requests.
|
||||
-->
|
||||
|
||||
1. Go to...
|
||||
2. Click on...
|
||||
3. See error...
|
||||
|
||||
### Expected Behavior
|
||||
|
||||
<!--
|
||||
What did you expect to happen?
|
||||
-->
|
||||
|
||||
(Describe what you expected.)
|
||||
|
||||
### Actual Behavior (Bug Reports Only)
|
||||
|
||||
<!--
|
||||
What actually happened? Delete this section for feature requests.
|
||||
-->
|
||||
|
||||
(Describe what actually happened.)
|
||||
|
||||
### Proposed Solution (Feature Requests Only)
|
||||
|
||||
<!--
|
||||
If this is a feature request, describe your proposed solution.
|
||||
Delete this section for bug reports.
|
||||
-->
|
||||
|
||||
(Describe your proposed solution.)
|
||||
|
||||
### Screenshots / Evidence
|
||||
|
||||
<!--
|
||||
Add screenshots, screen recordings, or console logs if applicable.
|
||||
You can use tools like Bird Eats Bug for recordings.
|
||||
-->
|
||||
|
||||
(Attach evidence here.)
|
||||
|
||||
### Additional Context
|
||||
|
||||
<!--
|
||||
Any other information that might be helpful.
|
||||
-->
|
||||
|
||||
(Add any additional context here.)
|
||||
|
||||
---
|
||||
|
||||
##### House rules
|
||||
|
||||
- If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves the request by removing this label.
|
||||
- Your ideas are invaluable! However, they undergo review to ensure alignment with the product's direction.
|
||||
- Follow best practices in our [Contributor Docs](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Companion Builds
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
EXPO_PUBLIC_CAL_API_KEY: ${{ secrets.CI_EXPO_PUBLIC_CAL_API_KEY || vars.CI_EXPO_PUBLIC_CAL_API_KEY || vars.EXPO_PUBLIC_CAL_API_KEY }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Companion App & Extension
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.3.0
|
||||
cache: true
|
||||
cache-dependency-path: companion/bun.lock
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: companion
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build Expo web app
|
||||
working-directory: companion
|
||||
env:
|
||||
EXPO_NO_TELEMETRY: 1
|
||||
CI: true
|
||||
run: bunx expo export --platform web
|
||||
|
||||
- name: Build browser extension
|
||||
working-directory: companion
|
||||
run: bun run ext:build
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Companion Lint
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Companion App
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.3.0
|
||||
cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: companion
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run lint
|
||||
working-directory: companion
|
||||
run: bun run lint:all
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Companion Type Check
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Type Check Companion App
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
- uses: ./.github/actions/cache-checkout
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.3.0
|
||||
cache: true
|
||||
cache-dependency-path: companion/bun.lock
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: companion
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
working-directory: companion
|
||||
run: bun run typecheck:all
|
||||
@@ -167,7 +167,6 @@ jobs:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter-exclusions.outputs.has-files-requiring-all-checks }}
|
||||
has-companion: ${{ steps.filter-inclusions.outputs.has-companion }}
|
||||
has-api-v2-changes: ${{ steps.filter-inclusions.outputs.has-api-v2-changes }}
|
||||
has-prisma-changes: ${{ steps.filter-inclusions.outputs.has-prisma-changes }}
|
||||
has-agents-changes: ${{ steps.filter-inclusions.outputs.has-agents-changes }}
|
||||
@@ -197,14 +196,13 @@ jobs:
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- '**'
|
||||
- '!companion/**'
|
||||
- '!.vscode/**'
|
||||
- '!**/*.md'
|
||||
- '!**/*.mdx'
|
||||
- '!.github/CODEOWNERS'
|
||||
- '!docs/**'
|
||||
- '!help/**'
|
||||
- '!apps/web/public/static/locales/**/common.json'
|
||||
- '!packages/i18n/locales/**/common.json'
|
||||
- '!i18n.lock'
|
||||
- name: Check for specific path changes
|
||||
uses: dorny/paths-filter@v3
|
||||
@@ -213,8 +211,6 @@ jobs:
|
||||
filters: |
|
||||
has-agents-changes:
|
||||
- "agents/**"
|
||||
has-companion:
|
||||
- "companion/**"
|
||||
has-api-v2-changes:
|
||||
- "apps/api/v2/**"
|
||||
- "packages/platform-constants/**"
|
||||
@@ -387,27 +383,6 @@ jobs:
|
||||
uses: ./.github/workflows/docs-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-companion:
|
||||
name: Companion builds
|
||||
needs: [prepare]
|
||||
if: needs.prepare.outputs.has-companion == 'true'
|
||||
uses: ./.github/workflows/companion-build.yml
|
||||
secrets: inherit
|
||||
|
||||
typecheck-companion:
|
||||
name: Type Checks
|
||||
needs: [prepare]
|
||||
if: needs.prepare.outputs.has-companion == 'true'
|
||||
uses: ./.github/workflows/companion-typecheck.yml
|
||||
secrets: inherit
|
||||
|
||||
lint-companion:
|
||||
name: Linters
|
||||
needs: [prepare]
|
||||
if: needs.prepare.outputs.has-companion == 'true'
|
||||
uses: ./.github/workflows/companion-lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production builds
|
||||
needs: [prepare]
|
||||
@@ -489,9 +464,6 @@ jobs:
|
||||
build-api-v2,
|
||||
build-atoms,
|
||||
build-docs,
|
||||
build-companion,
|
||||
typecheck-companion,
|
||||
lint-companion,
|
||||
setup-db,
|
||||
e2e,
|
||||
e2e-api-v2,
|
||||
@@ -540,14 +512,6 @@ jobs:
|
||||
needs.e2e-app-store.result != 'success'
|
||||
)
|
||||
) ||
|
||||
(
|
||||
needs.prepare.outputs.has-companion == 'true' &&
|
||||
(
|
||||
needs.build-companion.result != 'success' ||
|
||||
needs.typecheck-companion.result != 'success' ||
|
||||
needs.lint-companion.result != 'success'
|
||||
)
|
||||
) ||
|
||||
(
|
||||
needs.prepare.outputs.has-agents-changes == 'true' &&
|
||||
needs.validate-agents-format.result != 'success'
|
||||
|
||||
@@ -12,6 +12,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: false
|
||||
name: Sync to Devin Knowledge
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
|
||||
@@ -4,4 +4,10 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
# fast-xml-parser 4.4.1 via @boxyhq/saml-jackson → @aws-sdk/core@3.816.0 (transitive).
|
||||
# Only parses trusted AWS API responses, not user input. No practical attack vector.
|
||||
# Upstream fix pending: ory/polis (saml-jackson) has bumped to @aws-sdk@3.994.0 on main but hasn't released yet.
|
||||
npmAuditIgnoreAdvisories:
|
||||
- "1113407"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||
|
||||
@@ -12,7 +12,7 @@ You are a senior Cal.com engineer working in a Yarn/Turbo monorepo. You prioriti
|
||||
- Create PRs in draft mode by default
|
||||
- Run `yarn type-check:ci --force` before concluding CI failures are unrelated to your changes
|
||||
- Import directly from source files, not barrel files (e.g., `@calcom/ui/components/button` not `@calcom/ui`)
|
||||
- Add translations to `apps/web/public/static/locales/en/common.json` for all UI strings
|
||||
- Add translations to `packages/i18n/locales/en/common.json` for all UI strings
|
||||
- Use `date-fns` or native `Date` instead of Day.js when timezone awareness isn't needed
|
||||
- Put permission checks in `page.tsx`, never in `layout.tsx`
|
||||
- Use `ast-grep` for searching if available; otherwise use `rg` (ripgrep), then fall back to `grep`
|
||||
@@ -126,7 +126,7 @@ packages/lib/ # Shared utilities
|
||||
- Routes: `apps/web/app/` (App Router)
|
||||
- Database schema: `packages/prisma/schema.prisma`
|
||||
- tRPC routers: `packages/trpc/server/routers/`
|
||||
- Translations: `apps/web/public/static/locales/en/common.json`
|
||||
- Translations: `packages/i18n/locales/en/common.json`
|
||||
- Workflow constants: `packages/features/ee/workflows/lib/constants.ts`
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -149,6 +149,13 @@ Here is what you need to be able to run Cal.com.
|
||||
- Duplicate `.env.example` to `.env`
|
||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
|
||||
> **Windows users:** Replace the `packages/prisma/.env` symlink with a real copy to avoid a Prisma error (`unexpected character / in variable name`):
|
||||
>
|
||||
> ```sh
|
||||
> # Git Bash / WSL
|
||||
> rm packages/prisma/.env && cp .env packages/prisma/.env
|
||||
> ```
|
||||
|
||||
5. Setup Node
|
||||
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
||||
@@ -987,12 +994,6 @@ An example of good readme is [atoms readme](https://github.com/calcom/cal.com/bl
|
||||
1. Follow semantic versioning when using changesets.
|
||||
2. Mark breaking changes using `❗️Breaking change`
|
||||
|
||||
<!-- LICENSE -->
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the [AGPLv3 License](https://github.com/calcom/cal.com/blob/main/LICENSE). See `LICENSE` for more information.
|
||||
|
||||
<!-- ACKNOWLEDGEMENTS -->
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -33,7 +33,7 @@ tags: reference, navigation, file-locations
|
||||
|
||||
## Translations
|
||||
|
||||
- English: `apps/web/public/static/locales/en/common.json`
|
||||
- English: `packages/i18n/locales/en/common.json`
|
||||
|
||||
## App Store
|
||||
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
---
|
||||
name: calcom-api
|
||||
description: Interact with the Cal.com API v2 to manage scheduling, bookings, event types, availability, and calendars. Use this skill when building integrations that need to create or manage bookings, check availability, configure event types, or sync calendars with Cal.com's scheduling infrastructure.
|
||||
env:
|
||||
CAL_API_KEY:
|
||||
description: "Cal.com API key (prefixed with cal_live_ or cal_test_). Required for all API requests."
|
||||
required: true
|
||||
CAL_CLIENT_ID:
|
||||
description: "OAuth client ID for platform integrations managing users on behalf of others. Sent as x-cal-client-id header."
|
||||
required: false
|
||||
CAL_SECRET_KEY:
|
||||
description: "OAuth client secret for platform integrations. Sent as x-cal-secret-key header."
|
||||
required: false
|
||||
CAL_WEBHOOK_SECRET:
|
||||
description: "Secret used to verify webhook payload signatures via X-Cal-Signature-256 header."
|
||||
required: false
|
||||
---
|
||||
|
||||
# Cal.com API v2
|
||||
@@ -14,6 +27,15 @@ All API requests should be made to:
|
||||
https://api.cal.com/v2
|
||||
```
|
||||
|
||||
## Required Credentials
|
||||
|
||||
| Environment Variable | Required | Description |
|
||||
|---------------------|----------|-------------|
|
||||
| `CAL_API_KEY` | Yes | Cal.com API key (prefixed with `cal_live_` or `cal_test_`). Used as Bearer token for all API requests. Generate from Settings > Developer > API Keys. |
|
||||
| `CAL_CLIENT_ID` | No | OAuth client ID for platform integrations that manage users on behalf of others. Sent as `x-cal-client-id` header. |
|
||||
| `CAL_SECRET_KEY` | No | OAuth client secret for platform integrations. Sent as `x-cal-secret-key` header. |
|
||||
| `CAL_WEBHOOK_SECRET` | No | Secret for verifying webhook payload signatures via the `X-Cal-Signature-256` header. |
|
||||
|
||||
## Authentication
|
||||
|
||||
All API requests require authentication via Bearer token:
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
const path = require("node:path");
|
||||
const i18nConfig = require("@calcom/config/next-i18next.config");
|
||||
|
||||
/** @type {import("next-i18next").UserConfig} */
|
||||
const config = {
|
||||
...i18nConfig,
|
||||
localePath: path.resolve("../../web/public/static/locales"),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -82,6 +82,7 @@ async function handler(req: NextApiRequest) {
|
||||
return await handleCancelBooking({
|
||||
bookingData: { id, allRemainingBookings, cancellationReason, cancelledBy },
|
||||
userId: req.userId,
|
||||
actionSource: "API_V1",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// TODO: Fix tests (These test were never running due to the vitest workspace config)
|
||||
import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
@@ -174,7 +173,7 @@ vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType", () =>
|
||||
getAllWorkflowsFromEventType: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
vi.mock("@calcom/i18n/server", () => {
|
||||
const mockT = (key: string, options?: any) => {
|
||||
if (key === "event_between_users") {
|
||||
return `${options?.eventName} between ${options?.host} and ${options?.attendeeName}`;
|
||||
|
||||
@@ -14,7 +14,7 @@ import handler from "../../../pages/api/users/_post";
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
vi.mock("@calcom/i18n/server", () => {
|
||||
return {
|
||||
getTranslation: (key: string) => {
|
||||
return () => key;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const TRIGGER_VERSION = '20260212.1';
|
||||
export const TRIGGER_VERSION = '20260216.1';
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require("node:path");
|
||||
const i18nConfig = require("@calcom/config/next-i18next.config");
|
||||
|
||||
/** @type {import("next-i18next").UserConfig} */
|
||||
const config = {
|
||||
...i18nConfig,
|
||||
localePath: path.resolve("../../web/public/static/locales"),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -62,7 +62,7 @@
|
||||
"@sentry/node": "9.46.0",
|
||||
"@sentry/profiling-node": "9.46.0",
|
||||
"@snyk/protect": "latest",
|
||||
"axios": "1.13.2",
|
||||
"axios": "1.13.5",
|
||||
"body-parser": "1.20.3",
|
||||
"bull": "4.15.1",
|
||||
"class-transformer": "0.5.1",
|
||||
|
||||
@@ -279,6 +279,7 @@ export class BookingsController_2024_04_15 {
|
||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
|
||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||
actionSource: "API_V2",
|
||||
});
|
||||
if (!res.onlyRemovedAttendee && res.isPlatformManagedUserBooking) {
|
||||
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { BookingAttendeesController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-attendees.controller";
|
||||
import { BookingGuestsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-guests.controller";
|
||||
import { BookingLocationController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-location.controller";
|
||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingAttendeesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-attendees.service";
|
||||
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
|
||||
import { BookingLocationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location.service";
|
||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||
@@ -12,17 +15,18 @@ import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-vid
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { CalendarsCacheService } from "@/ee/calendars/services/calendars-cache.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module";
|
||||
import { InstantBookingModule } from "@/lib/modules/instant-booking.module";
|
||||
import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module";
|
||||
import { RegularBookingModule } from "@/lib/modules/regular-booking.module";
|
||||
@@ -50,8 +54,6 @@ import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -81,6 +83,7 @@ import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.m
|
||||
OAuthClientRepository,
|
||||
OAuthClientUsersService,
|
||||
BookingsService_2024_08_13,
|
||||
BookingAttendeesService_2024_08_13,
|
||||
BookingGuestsService_2024_08_13,
|
||||
InputBookingsService_2024_08_13,
|
||||
OutputBookingsService_2024_08_13,
|
||||
@@ -110,6 +113,7 @@ import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.m
|
||||
],
|
||||
controllers: [
|
||||
BookingsController_2024_08_13,
|
||||
BookingAttendeesController_2024_08_13,
|
||||
BookingGuestsController_2024_08_13,
|
||||
BookingLocationController_2024_08_13,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { AddAttendeeInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Body, Controller, HttpCode, HttpStatus, Param, Post, UseGuards } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
|
||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||
import { AddAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output";
|
||||
import { BookingAttendeesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-attendees.service";
|
||||
import { VERSION_2024_08_13, VERSION_2024_08_13_VALUE } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/bookings/:bookingUid/attendees",
|
||||
version: VERSION_2024_08_13_VALUE,
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Bookings / Attendees")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to ${VERSION_2024_08_13}. This header is required as this endpoint does not exist in older API versions.`,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
})
|
||||
export class BookingAttendeesController_2024_08_13 {
|
||||
constructor(private readonly bookingAttendeesService: BookingAttendeesService_2024_08_13) {}
|
||||
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
|
||||
@Throttle({
|
||||
limit: 5,
|
||||
ttl: 60000,
|
||||
blockDuration: 60000,
|
||||
name: "booking_attendees_add",
|
||||
})
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Add an attendee to a booking",
|
||||
description: `Add a new attendee to an existing booking by its UID.
|
||||
|
||||
**Side effects:**
|
||||
- The booking's attendee list is updated in the database
|
||||
- The calendar event is updated on connected calendars (Google Calendar, Outlook, etc.) to include the new attendee
|
||||
- An email notification is sent to the new attendee with the booking details
|
||||
|
||||
**Permissions:**
|
||||
- The authenticated user must be either the booking organizer, an existing attendee, or have the \`booking.update\` permission for the team
|
||||
|
||||
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>
|
||||
`,
|
||||
})
|
||||
async addAttendee(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: AddAttendeeInput_2024_08_13,
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<AddAttendeeOutput_2024_08_13> {
|
||||
const attendee = await this.bookingAttendeesService.addAttendee(bookingUid, body, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: attendee,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
AttendeeAddGuestsEmail,
|
||||
AttendeeScheduledEmail,
|
||||
OrganizerAddGuestsEmail,
|
||||
} from "@calcom/platform-libraries/emails";
|
||||
import type { BookingOutput_2024_08_13, CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import type { Team, User } from "@calcom/prisma/client";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { mockThrottlerGuard } from "test/utils/withNoThrottler";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { bootstrap } from "@/bootstrap";
|
||||
import { AddAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
|
||||
const attendeeAddGuestsEmailSpy = jest
|
||||
.spyOn(AttendeeAddGuestsEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
const organizerAddGuestsEmailSpy = jest
|
||||
.spyOn(OrganizerAddGuestsEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
const attendeeScheduledEmailSpy = jest
|
||||
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
type TestUser = {
|
||||
user: User;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
type TestSetup = {
|
||||
organizer: TestUser;
|
||||
unrelatedUser: TestUser;
|
||||
eventTypeId: number;
|
||||
bookingUid: string;
|
||||
bookingId: number;
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 add attendee", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
|
||||
let testSetup: TestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
mockThrottlerGuard();
|
||||
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `add-attendee-2024-08-13-organization-${randomString()}`,
|
||||
});
|
||||
|
||||
await setupTestData();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
const oAuthClient = await createOAuthClient(organization.id, true);
|
||||
|
||||
const organizerUser = await userRepositoryFixture.create({
|
||||
email: `add-attendee-2024-08-13-organizer-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const unrelatedUserData = await userRepositoryFixture.create({
|
||||
email: `add-attendee-2024-08-13-unrelated-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id);
|
||||
const unrelatedUserTokens = await tokensRepositoryFixture.createTokens(
|
||||
unrelatedUserData.id,
|
||||
oAuthClient.id
|
||||
);
|
||||
|
||||
const schedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `add-attendee-2024-08-13-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(organizerUser.id, schedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `add-attendee-2024-08-13-event-type-${randomString()}`,
|
||||
slug: `add-attendee-2024-08-13-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
},
|
||||
organizerUser.id
|
||||
);
|
||||
|
||||
testSetup = {
|
||||
organizer: {
|
||||
user: organizerUser,
|
||||
accessToken: organizerTokens.accessToken,
|
||||
refreshToken: organizerTokens.refreshToken,
|
||||
},
|
||||
unrelatedUser: {
|
||||
user: unrelatedUserData,
|
||||
accessToken: unrelatedUserTokens.accessToken,
|
||||
refreshToken: unrelatedUserTokens.refreshToken,
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
bookingUid: "",
|
||||
bookingId: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
areEmailsEnabled: emailsEnabled,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
attendeeAddGuestsEmailSpy.mockClear();
|
||||
organizerAddGuestsEmailSpy.mockClear();
|
||||
attendeeScheduledEmailSpy.mockClear();
|
||||
});
|
||||
|
||||
describe("POST /v2/bookings/:bookingUid/attendees", () => {
|
||||
beforeAll(async () => {
|
||||
const createBookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 10, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: testSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Original Attendee",
|
||||
email: "original.attendee@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
const createBookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(createBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body;
|
||||
expect(createBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
if (!responseDataIsBooking(createBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
testSetup.bookingUid = createBookingResponseBody.data.uid;
|
||||
testSetup.bookingId = createBookingResponseBody.data.id;
|
||||
});
|
||||
|
||||
describe("Authentication", () => {
|
||||
it("should return 401 when adding attendee without authentication", async () => {
|
||||
const addAttendeeBody = {
|
||||
email: "unauthenticated.attendee@example.com",
|
||||
name: "Unauthenticated Attendee",
|
||||
timeZone: "Europe/London",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authorization", () => {
|
||||
it("should allow booking organizer to add an attendee", async () => {
|
||||
const addAttendeeBody = {
|
||||
email: "new.attendee@example.com",
|
||||
name: "New Attendee",
|
||||
timeZone: "Europe/London",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: AddAttendeeOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.email).toEqual(addAttendeeBody.email);
|
||||
expect(responseBody.data.name).toEqual(addAttendeeBody.name);
|
||||
expect(responseBody.data.timeZone).toEqual(addAttendeeBody.timeZone);
|
||||
expect(responseBody.data.bookingId).toEqual(testSetup.bookingId);
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return 403 when unrelated user tries to add attendee", async () => {
|
||||
const addAttendeeBody = {
|
||||
email: "unauthorized.attendee@example.com",
|
||||
name: "Unauthorized Attendee",
|
||||
timeZone: "America/New_York",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Response format", () => {
|
||||
it("should return attendee with correct structure", async () => {
|
||||
const addAttendeeBody = {
|
||||
email: "structure.test@example.com",
|
||||
name: "Structure Test Attendee",
|
||||
timeZone: "Asia/Tokyo",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: AddAttendeeOutput_2024_08_13 = response.body;
|
||||
|
||||
expect(responseBody).toHaveProperty("status", SUCCESS_STATUS);
|
||||
expect(responseBody).toHaveProperty("data");
|
||||
expect(responseBody.data).toHaveProperty("id");
|
||||
expect(responseBody.data).toHaveProperty("bookingId");
|
||||
expect(responseBody.data).toHaveProperty("name");
|
||||
expect(responseBody.data).toHaveProperty("email");
|
||||
expect(responseBody.data).toHaveProperty("timeZone");
|
||||
|
||||
expect(typeof responseBody.data.id).toBe("number");
|
||||
expect(typeof responseBody.data.bookingId).toBe("number");
|
||||
expect(typeof responseBody.data.name).toBe("string");
|
||||
expect(typeof responseBody.data.email).toBe("string");
|
||||
expect(typeof responseBody.data.timeZone).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validation", () => {
|
||||
it("should return 400 when email is missing", async () => {
|
||||
const addAttendeeBody = {
|
||||
name: "No Email Attendee",
|
||||
timeZone: "Europe/London",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return 400 when email is invalid", async () => {
|
||||
const addAttendeeBody = {
|
||||
email: "invalid-email",
|
||||
name: "Invalid Email Attendee",
|
||||
timeZone: "Europe/London",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return 400 when name is missing", async () => {
|
||||
const addAttendeeBody = {
|
||||
email: "no.name@example.com",
|
||||
timeZone: "Europe/London",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return 400 when timeZone is missing", async () => {
|
||||
const addAttendeeBody = {
|
||||
email: "no.timezone@example.com",
|
||||
name: "No TimeZone Attendee",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return 400 when timeZone is invalid", async () => {
|
||||
const addAttendeeBody = {
|
||||
email: "invalid.timezone@example.com",
|
||||
name: "Invalid TimeZone Attendee",
|
||||
timeZone: "Invalid/TimeZone",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return 403 when booking does not exist", async () => {
|
||||
const addAttendeeBody = {
|
||||
email: "nonexistent.booking@example.com",
|
||||
name: "Nonexistent Booking Attendee",
|
||||
timeZone: "Europe/London",
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/non-existent-booking-uid/attendees`)
|
||||
.send(addAttendeeBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.organizer.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.user.email);
|
||||
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
testSetup.organizer.user.id,
|
||||
testSetup.organizer.user.email
|
||||
);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data;
|
||||
}
|
||||
});
|
||||
@@ -1053,7 +1053,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.message).toEqual("Cancellation reason is required when you are the host");
|
||||
expect(response.body.message).toEqual("Cancellation reason is required");
|
||||
});
|
||||
|
||||
it("should cancel seated booking", async () => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingAttendee } from "@calcom/platform-types";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsNumber, ValidateNested } from "class-validator";
|
||||
|
||||
export class BookingAttendeeOutput_2024_08_13 extends BookingAttendee {
|
||||
@ApiProperty({ type: Number, example: 251 })
|
||||
@IsNumber()
|
||||
@Expose()
|
||||
id!: number;
|
||||
|
||||
@ApiProperty({ type: Number, example: 313 })
|
||||
@IsNumber()
|
||||
@Expose()
|
||||
bookingId!: number;
|
||||
}
|
||||
|
||||
export class AddAttendeeOutput_2024_08_13 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({ type: BookingAttendeeOutput_2024_08_13 })
|
||||
@ValidateNested()
|
||||
@Type(() => BookingAttendeeOutput_2024_08_13)
|
||||
data!: BookingAttendeeOutput_2024_08_13;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { BookingAttendeesService } from "@calcom/platform-libraries/bookings";
|
||||
import type { AddAttendeeInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { HttpException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { BookingAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import type { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
|
||||
@Injectable()
|
||||
export class BookingAttendeesService_2024_08_13 {
|
||||
private bookingAttendeesService: BookingAttendeesService;
|
||||
|
||||
constructor(
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly platformBookingsService: PlatformBookingsService
|
||||
) {
|
||||
this.bookingAttendeesService = new BookingAttendeesService();
|
||||
}
|
||||
|
||||
async addAttendee(
|
||||
bookingUid: string,
|
||||
input: AddAttendeeInput_2024_08_13,
|
||||
user: ApiAuthGuardUser
|
||||
): Promise<BookingAttendeeOutput_2024_08_13> {
|
||||
const booking = await this.bookingsRepository.getByUidWithEventType(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
|
||||
}
|
||||
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
const createdAttendee = await this.bookingAttendeesService.addAttendee({
|
||||
bookingId: booking.id,
|
||||
attendee: {
|
||||
email: input.email,
|
||||
name: input.name,
|
||||
timeZone: input.timeZone,
|
||||
phoneNumber: input.phoneNumber,
|
||||
language: input.language,
|
||||
},
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
organizationId: user.organizationId,
|
||||
uuid: user.uuid,
|
||||
},
|
||||
emailsEnabled,
|
||||
actionSource: "API_V2",
|
||||
});
|
||||
|
||||
return plainToClass(
|
||||
BookingAttendeeOutput_2024_08_13,
|
||||
{
|
||||
id: createdAttendee.id,
|
||||
bookingId: createdAttendee.bookingId,
|
||||
name: createdAttendee.name,
|
||||
email: createdAttendee.email,
|
||||
displayEmail: this.getDisplayEmail(createdAttendee.email),
|
||||
timeZone: createdAttendee.timeZone,
|
||||
language: createdAttendee.locale ?? undefined,
|
||||
absent: false,
|
||||
phoneNumber: createdAttendee.phoneNumber ?? undefined,
|
||||
},
|
||||
{ strategy: "excludeAll" }
|
||||
);
|
||||
}
|
||||
|
||||
private getDisplayEmail(email: string): string {
|
||||
return email.replace(/\+[a-zA-Z0-9]{25}/, "");
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ import { TaskerService } from "@/lib/services/tasker.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module, Scope } from "@nestjs/common";
|
||||
|
||||
import { getWebhookProducer } from "@calcom/platform-libraries/bookings";
|
||||
|
||||
import { WEBHOOK_PRODUCER } from "./regular-booking.tokens";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [
|
||||
@@ -37,6 +41,10 @@ import { Module, Scope } from "@nestjs/common";
|
||||
},
|
||||
scope: Scope.TRANSIENT,
|
||||
},
|
||||
{
|
||||
provide: WEBHOOK_PRODUCER,
|
||||
useFactory: () => getWebhookProducer(),
|
||||
},
|
||||
BookingAuditProducerService,
|
||||
BookingEventHandlerService,
|
||||
CheckBookingAndDurationLimitsService,
|
||||
@@ -51,6 +59,6 @@ import { Module, Scope } from "@nestjs/common";
|
||||
TaskerService,
|
||||
RegularBookingService,
|
||||
],
|
||||
exports: [RegularBookingService],
|
||||
exports: [RegularBookingService, WEBHOOK_PRODUCER],
|
||||
})
|
||||
export class RegularBookingModule {}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Injection token for IWebhookProducerService.
|
||||
* Used to bridge the Cal.com DI container into NestJS.
|
||||
*
|
||||
* Defined in a separate file to avoid a circular import between
|
||||
* regular-booking.module.ts and regular-booking.service.ts.
|
||||
*/
|
||||
export const WEBHOOK_PRODUCER = "WEBHOOK_PRODUCER";
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
RegularBookingService as BaseRegularBookingService,
|
||||
type IWebhookProducerService,
|
||||
} from "@calcom/platform-libraries/bookings";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { WEBHOOK_PRODUCER } from "@/lib/modules/regular-booking.tokens";
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
@@ -7,10 +14,6 @@ import { HashedLinkService } from "@/lib/services/hashed-link.service";
|
||||
import { LuckyUserService } from "@/lib/services/lucky-user.service";
|
||||
import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-sms-tasker.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { RegularBookingService as BaseRegularBookingService } from "@calcom/platform-libraries/bookings";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
export class RegularBookingService extends BaseRegularBookingService {
|
||||
@@ -23,7 +26,8 @@ export class RegularBookingService extends BaseRegularBookingService {
|
||||
userRepository: PrismaUserRepository,
|
||||
bookingEmailAndSmsTasker: BookingEmailAndSmsTasker,
|
||||
featuresRepository: PrismaFeaturesRepository,
|
||||
bookingEventHandler: BookingEventHandlerService
|
||||
bookingEventHandler: BookingEventHandlerService,
|
||||
@Inject(WEBHOOK_PRODUCER) webhookProducer: IWebhookProducerService
|
||||
) {
|
||||
super({
|
||||
checkBookingAndDurationLimitsService,
|
||||
@@ -35,6 +39,7 @@ export class RegularBookingService extends BaseRegularBookingService {
|
||||
bookingEmailAndSmsTasker,
|
||||
featuresRepository,
|
||||
bookingEventHandler,
|
||||
webhookProducer,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getTasker, type Tasker } from "@calcom/platform-libraries/tasker";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { getTasker, type Tasker } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class TaskerService {
|
||||
private readonly tasker: Tasker;
|
||||
@@ -14,5 +13,3 @@ export class TaskerService {
|
||||
return this.tasker;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { generateSecret } from "@calcom/platform-libraries";
|
||||
import type { Membership, Team, User } from "@calcom/prisma/client";
|
||||
import { AccessScope, OAuthClientType } from "@calcom/prisma/enums";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { AccessScope, OAuthClientStatus, OAuthClientType } from "@calcom/prisma/enums";
|
||||
import type { INestApplication } from "@nestjs/common";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import type { TestingModule } from "@nestjs/testing";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuth2ClientRepositoryFixture } from "test/fixtures/repository/oauth2-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
@@ -21,20 +22,16 @@ import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
|
||||
// Mock next-auth/jwt getToken for ApiAuthStrategy NEXT_AUTH authentication
|
||||
// Mock next-auth/jwt getToken so the NextAuth fallback in ApiAuthStrategy doesn't crash in tests
|
||||
jest.mock("next-auth/jwt", () => ({
|
||||
getToken: jest.fn(),
|
||||
getToken: jest.fn().mockResolvedValue(null),
|
||||
}));
|
||||
const mockGetToken = getToken as jest.MockedFunction<typeof getToken>;
|
||||
|
||||
describe("OAuth2 Controller Endpoints", () => {
|
||||
describe("User Not Authenticated", () => {
|
||||
let appWithoutAuth: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Mock getToken to return null for unauthenticated tests
|
||||
mockGetToken.mockResolvedValue(null);
|
||||
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter, ZodExceptionFilter],
|
||||
imports: [AppModule, UsersModule, AuthModule, PrismaModule],
|
||||
@@ -69,20 +66,23 @@ describe("OAuth2 Controller Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("User Authenticated", () => {
|
||||
describe("User Authenticated (non-owner)", () => {
|
||||
let app: INestApplication;
|
||||
let moduleRef: TestingModule;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let oAuthClientFixture: OAuth2ClientRepositoryFixture;
|
||||
let oAuthService: OAuthService;
|
||||
|
||||
let user: User;
|
||||
let clientOwner: User;
|
||||
let authenticatedUser: User;
|
||||
let team: Team;
|
||||
let membership: Membership;
|
||||
let oAuthClient: { clientId: string };
|
||||
let apiKeyString: string;
|
||||
let refreshToken: string;
|
||||
|
||||
const testClientId = `test-oauth-client-${randomString()}`;
|
||||
@@ -96,7 +96,7 @@ describe("OAuth2 Controller Endpoints", () => {
|
||||
): Promise<string> {
|
||||
const result = await oAuthService.generateAuthorizationCode(
|
||||
testClientId,
|
||||
user.id,
|
||||
authenticatedUser.id,
|
||||
testRedirectUri,
|
||||
scopes,
|
||||
undefined,
|
||||
@@ -107,12 +107,6 @@ describe("OAuth2 Controller Endpoints", () => {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const userEmail = `oauth2-e2e-user-${randomString()}@api.com`;
|
||||
|
||||
// Mock getToken to return the user email for authenticated tests
|
||||
// This is needed because ApiAuthGuard uses ApiAuthStrategy which calls getToken from next-auth/jwt
|
||||
mockGetToken.mockResolvedValue({ email: userEmail });
|
||||
|
||||
moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter, ZodExceptionFilter],
|
||||
imports: [AppModule, UsersModule, AuthModule, PrismaModule],
|
||||
@@ -125,20 +119,28 @@ describe("OAuth2 Controller Endpoints", () => {
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
oAuthClientFixture = new OAuth2ClientRepositoryFixture(moduleRef);
|
||||
oAuthService = moduleRef.get(OAuthService);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
clientOwner = await userRepositoryFixture.create({
|
||||
email: `oauth2-e2e-owner-${randomString()}@api.com`,
|
||||
});
|
||||
|
||||
authenticatedUser = await userRepositoryFixture.create({
|
||||
email: `oauth2-e2e-user-${randomString()}@api.com`,
|
||||
});
|
||||
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(authenticatedUser.id, null);
|
||||
apiKeyString = keyString;
|
||||
|
||||
team = await teamRepositoryFixture.create({
|
||||
name: `oauth2-e2e-team-${randomString()}`,
|
||||
slug: `oauth2-e2e-team-${randomString()}`,
|
||||
});
|
||||
|
||||
membership = await membershipRepositoryFixture.create({
|
||||
user: { connect: { id: user.id } },
|
||||
user: { connect: { id: authenticatedUser.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
role: "OWNER",
|
||||
accepted: true,
|
||||
@@ -151,6 +153,7 @@ describe("OAuth2 Controller Endpoints", () => {
|
||||
redirectUri: testRedirectUri,
|
||||
clientSecret: hashedSecret,
|
||||
clientType: OAuthClientType.CONFIDENTIAL,
|
||||
userId: clientOwner.id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,6 +162,7 @@ describe("OAuth2 Controller Endpoints", () => {
|
||||
it("should return OAuth client info for valid client ID", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/auth/oauth2/clients/${testClientId}`)
|
||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe("success");
|
||||
@@ -174,6 +178,7 @@ describe("OAuth2 Controller Endpoints", () => {
|
||||
it("should return 404 for non-existent client ID", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get("/api/v2/auth/oauth2/clients/non-existent-client-id")
|
||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
@@ -434,7 +439,238 @@ describe("OAuth2 Controller Endpoints", () => {
|
||||
await oAuthClientFixture.delete(oAuthClient.clientId);
|
||||
await membershipRepositoryFixture.delete(membership.id);
|
||||
await teamRepositoryFixture.delete(team.id);
|
||||
await userRepositoryFixture.delete(user.id);
|
||||
await userRepositoryFixture.delete(authenticatedUser.id);
|
||||
await userRepositoryFixture.delete(clientOwner.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Owner can use non-approved OAuth client", () => {
|
||||
let app: INestApplication;
|
||||
let moduleRef: TestingModule;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let oAuthClientFixture: OAuth2ClientRepositoryFixture;
|
||||
let oAuthService: OAuthService;
|
||||
|
||||
let owner: User;
|
||||
let team: Team;
|
||||
let membership: Membership;
|
||||
let pendingClient: { clientId: string };
|
||||
let rejectedClient: { clientId: string };
|
||||
let apiKeyString: string;
|
||||
|
||||
const pendingClientId = `test-pending-client-${randomString()}`;
|
||||
const rejectedClientId = `test-rejected-client-${randomString()}`;
|
||||
const testClientSecret = "test-secret-456";
|
||||
const testRedirectUri = "https://example.com/callback";
|
||||
|
||||
/** Generate a user-scoped authorization code (no teamSlug) so the owner's userId is in the token. */
|
||||
async function generateAuthCodeForClient(
|
||||
clientId: string,
|
||||
scopes: AccessScope[] = [AccessScope.READ_BOOKING]
|
||||
): Promise<string> {
|
||||
const result = await oAuthService.generateAuthorizationCode(
|
||||
clientId,
|
||||
owner.id,
|
||||
testRedirectUri,
|
||||
scopes
|
||||
);
|
||||
const redirectUrl = new URL(result.redirectUrl);
|
||||
return redirectUrl.searchParams.get("code") as string;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter, ZodExceptionFilter],
|
||||
imports: [AppModule, UsersModule, AuthModule, PrismaModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
await app.init();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
oAuthClientFixture = new OAuth2ClientRepositoryFixture(moduleRef);
|
||||
oAuthService = moduleRef.get(OAuthService);
|
||||
|
||||
owner = await userRepositoryFixture.create({
|
||||
email: `oauth2-owner-e2e-${randomString()}@api.com`,
|
||||
});
|
||||
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(owner.id, null);
|
||||
apiKeyString = keyString;
|
||||
|
||||
team = await teamRepositoryFixture.create({
|
||||
name: `oauth2-owner-team-${randomString()}`,
|
||||
slug: `oauth2-owner-team-${randomString()}`,
|
||||
});
|
||||
|
||||
membership = await membershipRepositoryFixture.create({
|
||||
user: { connect: { id: owner.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
role: "OWNER",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const [hashedSecret] = generateSecret(testClientSecret);
|
||||
|
||||
pendingClient = await oAuthClientFixture.create({
|
||||
clientId: pendingClientId,
|
||||
name: "Pending OAuth Client",
|
||||
redirectUri: testRedirectUri,
|
||||
clientSecret: hashedSecret,
|
||||
clientType: OAuthClientType.CONFIDENTIAL,
|
||||
status: OAuthClientStatus.PENDING,
|
||||
userId: owner.id,
|
||||
});
|
||||
|
||||
rejectedClient = await oAuthClientFixture.create({
|
||||
clientId: rejectedClientId,
|
||||
name: "Rejected OAuth Client",
|
||||
redirectUri: testRedirectUri,
|
||||
clientSecret: hashedSecret,
|
||||
clientType: OAuthClientType.CONFIDENTIAL,
|
||||
status: OAuthClientStatus.REJECTED,
|
||||
userId: owner.id,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return PENDING client info when authenticated as owner", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/auth/oauth2/clients/${pendingClientId}`)
|
||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toBe("success");
|
||||
expect(response.body.data.client_id).toBe(pendingClientId);
|
||||
expect(response.body.data.name).toBe("Pending OAuth Client");
|
||||
});
|
||||
|
||||
it("should exchange authorization code for tokens with PENDING client owned by user", async () => {
|
||||
const code = await generateAuthCodeForClient(pendingClientId);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/auth/oauth2/token")
|
||||
.type("form")
|
||||
.send({
|
||||
client_id: pendingClientId,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_secret: testClientSecret,
|
||||
redirect_uri: testRedirectUri,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.access_token).toBeDefined();
|
||||
expect(response.body.refresh_token).toBeDefined();
|
||||
expect(response.body.token_type).toBe("bearer");
|
||||
});
|
||||
|
||||
it("should refresh tokens with PENDING client owned by user", async () => {
|
||||
const code = await generateAuthCodeForClient(pendingClientId);
|
||||
|
||||
const tokenResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/auth/oauth2/token")
|
||||
.type("form")
|
||||
.send({
|
||||
client_id: pendingClientId,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_secret: testClientSecret,
|
||||
redirect_uri: testRedirectUri,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/auth/oauth2/token")
|
||||
.type("form")
|
||||
.send({
|
||||
client_id: pendingClientId,
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: tokenResponse.body.refresh_token,
|
||||
client_secret: testClientSecret,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.access_token).toBeDefined();
|
||||
expect(response.body.refresh_token).toBeDefined();
|
||||
expect(response.body.token_type).toBe("bearer");
|
||||
});
|
||||
|
||||
it("should reject authorization code generation for REJECTED client even as owner", async () => {
|
||||
await expect(generateAuthCodeForClient(rejectedClientId)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should reject token exchange when client becomes rejected", async () => {
|
||||
const code = await generateAuthCodeForClient(pendingClientId);
|
||||
|
||||
await oAuthClientFixture.updateStatus(pendingClientId, OAuthClientStatus.REJECTED);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/auth/oauth2/token")
|
||||
.type("form")
|
||||
.send({
|
||||
client_id: pendingClientId,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_secret: testClientSecret,
|
||||
redirect_uri: testRedirectUri,
|
||||
})
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.error).toBe("unauthorized_client");
|
||||
expect(response.body.error_description).toBe("client_rejected");
|
||||
|
||||
await oAuthClientFixture.updateStatus(pendingClientId, OAuthClientStatus.PENDING);
|
||||
});
|
||||
|
||||
it("should reject token refresh when client becomes rejected", async () => {
|
||||
const code = await generateAuthCodeForClient(pendingClientId);
|
||||
|
||||
const tokenResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/auth/oauth2/token")
|
||||
.type("form")
|
||||
.send({
|
||||
client_id: pendingClientId,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_secret: testClientSecret,
|
||||
redirect_uri: testRedirectUri,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await oAuthClientFixture.updateStatus(pendingClientId, OAuthClientStatus.REJECTED);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/auth/oauth2/token")
|
||||
.type("form")
|
||||
.send({
|
||||
client_id: pendingClientId,
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: tokenResponse.body.refresh_token,
|
||||
client_secret: testClientSecret,
|
||||
})
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.error).toBe("unauthorized_client");
|
||||
expect(response.body.error_description).toBe("client_rejected");
|
||||
|
||||
await oAuthClientFixture.updateStatus(pendingClientId, OAuthClientStatus.PENDING);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oAuthClientFixture.delete(pendingClient.clientId);
|
||||
await oAuthClientFixture.delete(rejectedClient.clientId);
|
||||
await membershipRepositoryFixture.delete(membership.id);
|
||||
await teamRepositoryFixture.delete(team.id);
|
||||
await userRepositoryFixture.delete(owner.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,15 +16,16 @@ export class OAuth2HttpExceptionFilter implements ExceptionFilter<OAuth2HttpExce
|
||||
const requestId = request.headers["x-request-id"] ?? "unknown-request-id";
|
||||
response.setHeader("X-Request-Id", requestId.toString());
|
||||
|
||||
const userContext = extractUserContext(request);
|
||||
const { userEmail: _userEmail, ...safeUserContext } = extractUserContext(request);
|
||||
const { Authorization: _auth, ...safeHeaders } = filterReqHeaders(request.headers);
|
||||
this.logger.error(`OAuth2 Http Exception: ${exception.oAuthErrorData.error}`, {
|
||||
exception,
|
||||
body: "[REDACTED]",
|
||||
headers: filterReqHeaders(request.headers),
|
||||
headers: safeHeaders,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
...userContext,
|
||||
...safeUserContext,
|
||||
});
|
||||
|
||||
response.status(exception.getStatus()).json(exception.oAuthErrorData);
|
||||
|
||||
+4
-4
@@ -515,7 +515,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
|
||||
it("should do a booking for seated event and slot should show attendees count and bookingUid", async () => {
|
||||
const startTime = "2050-09-05T11:00:00.000Z";
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${seatedEventTypeId}`,
|
||||
uid: `booking-uid-seated-${seatedEventTypeId}-${randomString()}`,
|
||||
title: "booking title",
|
||||
startTime,
|
||||
endTime: "2050-09-05T12:00:00.000Z",
|
||||
@@ -549,7 +549,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
|
||||
});
|
||||
|
||||
bookingSeatsRepositoryFixture.create({
|
||||
referenceUid: randomString(),
|
||||
referenceUid: `seat-${randomString()}`,
|
||||
data: {},
|
||||
booking: {
|
||||
connect: {
|
||||
@@ -598,7 +598,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
|
||||
it("should do a booking for seated event and slot should show attendees count and bookingUid in range format", async () => {
|
||||
const startTime = "2050-09-05T11:00:00.000Z";
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${seatedEventTypeId}`,
|
||||
uid: `booking-uid-seated-${seatedEventTypeId}-${randomString()}`,
|
||||
title: "booking title",
|
||||
startTime,
|
||||
endTime: "2050-09-05T12:00:00.000Z",
|
||||
@@ -632,7 +632,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
|
||||
});
|
||||
|
||||
bookingSeatsRepositoryFixture.create({
|
||||
referenceUid: randomString(),
|
||||
referenceUid: `seat-${randomString()}`,
|
||||
data: {},
|
||||
booking: {
|
||||
connect: {
|
||||
|
||||
+10
-9
@@ -832,7 +832,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
it("should do a booking for seated event and slot should show attendees count and bookingUid", async () => {
|
||||
const startTime = "2050-09-05T11:00:00.000Z";
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${seatedEventType.id}`,
|
||||
uid: `booking-uid-seated-${seatedEventType.id}-${randomString()}`,
|
||||
title: "booking title",
|
||||
startTime,
|
||||
endTime: "2050-09-05T12:00:00.000Z",
|
||||
@@ -866,7 +866,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
});
|
||||
|
||||
bookingSeatsRepositoryFixture.create({
|
||||
referenceUid: randomString(),
|
||||
referenceUid: `seat-${randomString()}`,
|
||||
data: {},
|
||||
booking: {
|
||||
connect: {
|
||||
@@ -961,7 +961,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
it("should do a booking for seated event and slot should show attendees count and bookingUid and return range format", async () => {
|
||||
const startTime = "2050-09-05T11:00:00.000Z";
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${seatedEventType.id}`,
|
||||
uid: `booking-uid-seated-range-${seatedEventType.id}-${randomString()}`,
|
||||
title: "booking title",
|
||||
startTime,
|
||||
endTime: "2050-09-05T12:00:00.000Z",
|
||||
@@ -995,7 +995,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
});
|
||||
|
||||
bookingSeatsRepositoryFixture.create({
|
||||
referenceUid: randomString(),
|
||||
referenceUid: `seat-${randomString()}`,
|
||||
data: {},
|
||||
booking: {
|
||||
connect: {
|
||||
@@ -1348,7 +1348,8 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
advanceTo(newDate);
|
||||
|
||||
const slotDuration = 60;
|
||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||
// Use 2050-09-06 to avoid overlap with other tests that reserve 2050-09-05
|
||||
const slotStartTime = "2050-09-06T09:00:00.000Z";
|
||||
const reserveResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/slots/reservations`)
|
||||
.send({
|
||||
@@ -1377,7 +1378,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/slots?eventTypeId=${variableLengthEventType.id}&start=2050-09-05&end=2050-09-09&duration=60`
|
||||
`/v2/slots?eventTypeId=${variableLengthEventType.id}&start=2050-09-05&end=2050-09-10&duration=60`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(200);
|
||||
@@ -1390,10 +1391,10 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
const days = Object.keys(slots);
|
||||
expect(days.length).toEqual(5);
|
||||
|
||||
const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter(
|
||||
const expectedSlotsUTC2050_09_06 = expectedSlotsUTC["2050-09-06"].filter(
|
||||
(slot) => slot.start !== slotStartTime
|
||||
);
|
||||
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
|
||||
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-06": expectedSlotsUTC2050_09_06 });
|
||||
|
||||
const dbSlot = await selectedSlotRepositoryFixture.getByUid(
|
||||
responseReservedVariableSlot.reservationUid
|
||||
@@ -1409,7 +1410,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
});
|
||||
|
||||
it("request slot contains already existing reserved slot", async () => {
|
||||
// Try to reserve 9:45-12:45 when 10:00-11:00 is taken
|
||||
// Try to reserve 8:45-11:45 when 09:00-10:00 on 2050-09-06 is taken
|
||||
const newSlotStart = DateTime.fromISO(responseReservedVariableSlot.slotStart)
|
||||
.minus({ minutes: 15 })
|
||||
.toISO();
|
||||
|
||||
@@ -114,7 +114,11 @@ export class WorkflowsRepository {
|
||||
) {
|
||||
await updateWorkflow({
|
||||
ctx: {
|
||||
user: { ...user, locale: user?.locale ?? "en" },
|
||||
user: {
|
||||
...user,
|
||||
locale: user?.locale ?? "en",
|
||||
organizationId: user.profiles?.[0]?.organization?.id ?? null,
|
||||
},
|
||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||
},
|
||||
input: data,
|
||||
@@ -132,7 +136,11 @@ export class WorkflowsRepository {
|
||||
) {
|
||||
await updateWorkflow({
|
||||
ctx: {
|
||||
user: { ...user, locale: user?.locale ?? "en" },
|
||||
user: {
|
||||
...user,
|
||||
locale: user?.locale ?? "en",
|
||||
organizationId: user.profiles?.[0]?.organization?.id ?? null,
|
||||
},
|
||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||
},
|
||||
input: data,
|
||||
|
||||
@@ -21,6 +21,7 @@ export class OAuth2ClientRepositoryFixture {
|
||||
status?: OAuthClientStatus;
|
||||
logo?: string;
|
||||
isTrusted?: boolean;
|
||||
userId?: number;
|
||||
}) {
|
||||
return this.prismaWriteClient.oAuthClient.create({
|
||||
data: {
|
||||
@@ -32,10 +33,18 @@ export class OAuth2ClientRepositoryFixture {
|
||||
status: data.status || OAuthClientStatus.APPROVED,
|
||||
logo: data.logo,
|
||||
isTrusted: data.isTrusted || false,
|
||||
...(data.userId && { user: { connect: { id: data.userId } } }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateStatus(clientId: string, status: OAuthClientStatus) {
|
||||
return this.prismaWriteClient.oAuthClient.update({
|
||||
where: { clientId },
|
||||
data: { status },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(clientId: string) {
|
||||
return this.prismaWriteClient.oAuthClient.delete({
|
||||
where: { clientId },
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const TRIGGER_VERSION = '20260212.1';
|
||||
export const TRIGGER_VERSION = '20260216.1';
|
||||
|
||||
@@ -3,7 +3,7 @@ import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { PageProps as ServerPageProps } from "app/_types";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { generateMeetingMetadata } from "app/_utils";
|
||||
import { headers, cookies } from "next/headers";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
|
||||
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { PageProps as ServerPageProps } from "app/_types";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -3,7 +3,7 @@ import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { PageProps as ServerPageProps } from "app/_types";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { generateMeetingMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
|
||||
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
|
||||
|
||||
@@ -3,7 +3,7 @@ import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||
import type { PageProps as ServerPageProps } from "app/_types";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getOrgFullOrigin, orgDomainConfig } from "@calcom/features/ee/organizat
|
||||
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
|
||||
import type { TeamData } from "@calcom/features/ee/teams/lib/getTeamData";
|
||||
import { shouldHideBrandingForTeamEvent } from "@calcom/features/profile/lib/hideBranding";
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { BookingStatus, RedirectType } from "@calcom/prisma/enums";
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { FieldTypes } from "@calcom/app-store/routing-forms/lib/FieldTypes";
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { LearnMoreLink } from "@calcom/features/eventtypes/components/LearnMoreLink";
|
||||
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
@@ -14,10 +15,10 @@ import {
|
||||
SelectField,
|
||||
TextField,
|
||||
} from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
|
||||
import SingleForm from "@components/apps/routing-forms/SingleForm";
|
||||
import { ChevronDownIcon, MenuIcon } from "@coss/ui/icons";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
@@ -132,7 +133,13 @@ function Field({
|
||||
<TextField
|
||||
disabled={!!router}
|
||||
label={t("identifier_url_parameter")}
|
||||
hint={t("identifier_url_parameter_hint")}
|
||||
hint={
|
||||
<LearnMoreLink
|
||||
t={t}
|
||||
i18nKey="identifier_url_parameter_hint"
|
||||
href="https://cal.com/help/routing/connect-routing-form-to-booking-questions"
|
||||
/>
|
||||
}
|
||||
name={`${hookFieldNamespace}.identifier`}
|
||||
required
|
||||
placeholder={t("identifies_name_field")}
|
||||
@@ -164,8 +171,8 @@ function Field({
|
||||
"h-8 w-full justify-between text-left text-sm",
|
||||
!!router && "bg-subtle cursor-not-allowed"
|
||||
)}>
|
||||
<span className="text-default">{defaultValue?.label || "Select field type"}</span>
|
||||
<Icon name="chevron-down" className="text-default h-4 w-4" />
|
||||
<span className="text-default">{defaultValue?.label || t("select_field_type")}</span>
|
||||
<ChevronDownIcon className="text-default h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -333,7 +340,7 @@ const FormEdit = ({
|
||||
{/* Icon card - Top */}
|
||||
<div className="bg-default border-subtle z-30 col-start-1 col-end-1 row-start-1 row-end-1 h-10 w-10 transform rounded-md border shadow-sm">
|
||||
<div className="text-emphasis flex h-full items-center justify-center">
|
||||
<Icon name="menu" className="text-emphasis h-4 w-4" />
|
||||
<MenuIcon className="text-emphasis h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Left fanned card */}
|
||||
|
||||
+6
-11
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import type z from "zod";
|
||||
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { WhenToWriteToRecord, SalesforceFieldType } from "@calcom/app-store/salesforce/lib/enums";
|
||||
import { SalesforceFieldType, WhenToWriteToRecord } from "@calcom/app-store/salesforce/lib/enums";
|
||||
import type { writeToRecordDataSchema as salesforceWriteToRecordDataSchema } from "@calcom/app-store/salesforce/zod";
|
||||
import { routingFormIncompleteBookingDataSchema as salesforceRoutingFormIncompleteBookingDataSchema } from "@calcom/app-store/salesforce/zod";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -12,15 +9,13 @@ import { IncompleteBookingActionType } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import { InputField } from "@calcom/ui/components/form";
|
||||
import { Select } from "@calcom/ui/components/form";
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { InputField, Label, Select, Switch } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
|
||||
|
||||
import SingleForm from "@components/apps/routing-forms/SingleForm";
|
||||
import { GlobeIcon } from "@coss/ui/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import type z from "zod";
|
||||
|
||||
function Page({ form }: { form: RoutingFormWithResponseCount }) {
|
||||
const { t } = useLocale();
|
||||
@@ -105,7 +100,7 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="border-subtle rounded-lg border p-1">
|
||||
<Icon name="globe" className="text-subtle h-4 w-4" />
|
||||
<GlobeIcon className="text-subtle h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-emphasis ml-2 text-sm font-medium">
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute";
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import LicenseRequired from "~/ee/common/components/LicenseRequired";
|
||||
import { FilterResults } from "~/filters/components/FilterResults";
|
||||
import { TeamsFilter } from "~/filters/components/TeamsFilter";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
@@ -21,22 +15,33 @@ import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { List, ListLinkItem } from "@calcom/ui/components/list";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import type {
|
||||
SetNewFormDialogState,
|
||||
NewFormDialogState,
|
||||
SetNewFormDialogState,
|
||||
} from "@calcom/web/components/apps/routing-forms/FormActions";
|
||||
import {
|
||||
FormAction,
|
||||
FormActionsDropdown,
|
||||
FormActionsProvider,
|
||||
} from "@calcom/web/components/apps/routing-forms/FormActions";
|
||||
|
||||
import {
|
||||
ChartBarIcon,
|
||||
CircleCheckIcon,
|
||||
DownloadIcon,
|
||||
FileTextIcon,
|
||||
MailIcon,
|
||||
ShuffleIcon,
|
||||
} from "@coss/ui/icons";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useHasPaidPlan } from "~/billing/hooks/useHasPaidPlan";
|
||||
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
|
||||
import LicenseRequired from "~/ee/common/components/LicenseRequired";
|
||||
import { CreateButtonWithTeamsList } from "~/ee/teams/components/createButton/CreateButtonWithTeamsList";
|
||||
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
|
||||
import { ShellMain } from "~/shell/Shell";
|
||||
import { UpgradeTip } from "~/shell/UpgradeTip";
|
||||
|
||||
@@ -92,32 +97,32 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) {
|
||||
const forms = queryRes.data?.filtered;
|
||||
const features = [
|
||||
{
|
||||
icon: <Icon name="file-text" className="h-5 w-5 text-orange-500" />,
|
||||
icon: <FileTextIcon className="h-5 w-5 text-orange-500" />,
|
||||
title: t("create_your_first_form"),
|
||||
description: t("create_your_first_form_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="shuffle" className="h-5 w-5 text-lime-500" />,
|
||||
icon: <ShuffleIcon className="h-5 w-5 text-lime-500" />,
|
||||
title: t("create_your_first_route"),
|
||||
description: t("route_to_the_right_person"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="chart-bar" className="h-5 w-5 text-blue-500" />,
|
||||
icon: <ChartBarIcon className="h-5 w-5 text-blue-500" />,
|
||||
title: t("reporting"),
|
||||
description: t("reporting_feature"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="circle-check" className="h-5 w-5 text-teal-500" />,
|
||||
icon: <CircleCheckIcon className="h-5 w-5 text-teal-500" />,
|
||||
title: t("test_routing_form"),
|
||||
description: t("test_preview_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="mail" className="h-5 w-5 text-yellow-500" />,
|
||||
icon: <MailIcon className="h-5 w-5 text-yellow-500" />,
|
||||
title: t("routing_forms_send_email_owner"),
|
||||
description: t("routing_forms_send_email_owner_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="download" className="h-5 w-5 text-violet-500" />,
|
||||
icon: <DownloadIcon className="h-5 w-5 text-violet-500" />,
|
||||
title: t("download_responses"),
|
||||
description: t("download_responses_description"),
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { XIcon } from "@coss/ui/icons";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
@@ -61,7 +61,7 @@ const ServerPage = async ({ searchParams }: PageProps) => {
|
||||
<AuthContainer>
|
||||
<div>
|
||||
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<Icon name="x" className="h-6 w-6 text-red-600" />
|
||||
<XIcon className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
import React, { Suspense } from "react";
|
||||
|
||||
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { LoaderIcon } from "@coss/ui/icons";
|
||||
|
||||
export default function TroubleshooterLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-1 *:flex-1">
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<Icon name="loader" />}>{children}</Suspense>
|
||||
<Suspense fallback={<LoaderIcon />}>{children}</Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { RefreshCcwIcon, UserPlusIcon, UsersIcon } from "@coss/ui/icons";
|
||||
|
||||
import { UpgradeTip } from "~/shell/UpgradeTip";
|
||||
|
||||
@@ -22,17 +22,17 @@ export default function UpgradeTipWrapper({ children }: { children: React.ReactN
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Icon name="users" className="h-5 w-5 text-red-500" />,
|
||||
icon: <UsersIcon className="h-5 w-5 text-red-500" />,
|
||||
title: t("view_bookings_across"),
|
||||
description: t("view_bookings_across_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="refresh-ccw" className="h-5 w-5 text-blue-500" />,
|
||||
icon: <RefreshCcwIcon className="h-5 w-5 text-blue-500" />,
|
||||
title: t("identify_booking_trends"),
|
||||
description: t("identify_booking_trends_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="user-plus" className="h-5 w-5 text-green-500" />,
|
||||
icon: <UserPlusIcon className="h-5 w-5 text-green-500" />,
|
||||
title: t("spot_popular_event_types"),
|
||||
description: t("spot_popular_event_types_description"),
|
||||
},
|
||||
|
||||
@@ -124,7 +124,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
||||
<div className="col-span-2 mb-6">
|
||||
{date.locale(i18n.language ?? "en").format("dddd, DD MMMM YYYY")}
|
||||
<br />
|
||||
{date.format(is24h ? "H:mm" : "h:mma")} - {props.eventType.length} mins{" "}
|
||||
{date.format(is24h ? "H:mm" : "h:mma")} - {props.eventType.length} {t("minute_timeUnit")}{" "}
|
||||
<span className="text-subtle">({timezone})</span>
|
||||
</div>
|
||||
{props.booking.location && (
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
import { useReactTable, getCoreRowModel, createColumnHelper } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { ColumnFilterType, DataTableProvider } from "@calcom/features/data-table";
|
||||
import { ColumnFilterType } from "@calcom/features/data-table";
|
||||
import { DataTableProvider } from "~/data-table/DataTableProvider";
|
||||
import type { DateRangeFilterOptions } from "@calcom/features/data-table/lib/types";
|
||||
import { DateRangeFilter } from "~/data-table/components/filters/DateRangeFilter";
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { Calendar } from "@calcom/web/modules/calendars/weeklyview/components/Calendar";
|
||||
import { Calendar } from "@calcom/features/calendars/weeklyview/components/Calendar";
|
||||
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
|
||||
import type { CalendarComponentProps, Hours } from "@calcom/features/calendars/weeklyview/types/state";
|
||||
|
||||
|
||||
+16
-17
@@ -31,6 +31,7 @@ import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ArrowLeftIcon, ChevronDownIcon, ChevronRightIcon } from "@coss/ui/icons";
|
||||
import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
|
||||
import { VerticalTabItem } from "@calcom/ui/components/navigation";
|
||||
import { Skeleton } from "@calcom/ui/components/skeleton";
|
||||
@@ -153,9 +154,10 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
|
||||
trackingMetadata: { section: "developer", page: "api_keys" },
|
||||
},
|
||||
{
|
||||
name: "admin_api",
|
||||
href: "/settings/organizations/admin-api",
|
||||
trackingMetadata: { section: "developer", page: "admin_api" },
|
||||
name: "api_docs",
|
||||
href: "https://cal.com/docs/api-reference/v2/introduction",
|
||||
isExternalLink: true,
|
||||
trackingMetadata: { section: "developer", page: "api_docs" },
|
||||
},
|
||||
// TODO: Add profile level for embeds
|
||||
// { name: "embeds", href: "/v2/settings/developer/embeds" },
|
||||
@@ -205,10 +207,10 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
|
||||
trackingMetadata: { section: "organization", page: "directory_sync" },
|
||||
},
|
||||
{
|
||||
name: "admin_api",
|
||||
href: "https://cal.com/docs/enterprise-features/api/api-reference/bookings#admin-access",
|
||||
name: "api_docs",
|
||||
href: "https://cal.com/docs/api-reference/v2/introduction",
|
||||
isExternalLink: true,
|
||||
trackingMetadata: { section: "organization", page: "admin_api" },
|
||||
trackingMetadata: { section: "organization", page: "api_docs" },
|
||||
},
|
||||
...(HAS_ORG_OPT_IN_FEATURES
|
||||
? [
|
||||
@@ -449,7 +451,7 @@ const useTabs = ({
|
||||
return { ...tab, children: filtered };
|
||||
} else if (tab.href === "/settings/developer") {
|
||||
const filtered = tab?.children?.filter(
|
||||
(childTab) => permissions?.canUpdateOrganization || childTab.name !== "admin_api"
|
||||
(childTab) => permissions?.canUpdateOrganization || childTab.name !== "api_docs"
|
||||
);
|
||||
return { ...tab, children: filtered };
|
||||
}
|
||||
@@ -475,10 +477,7 @@ const BackButtonInSidebar = ({ name }: { name: string }) => {
|
||||
href="/event-types"
|
||||
className="hover:bg-subtle todesktop:mt-10 [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default text-emphasis group my-6 flex h-6 max-h-6 w-full flex-row items-center rounded-md px-3 py-2 text-sm font-medium leading-4 transition"
|
||||
data-testid={`vertical-tab-${name}`}>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0"
|
||||
/>
|
||||
<ArrowLeftIcon className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0" />
|
||||
<Skeleton title={name} as="p" className="min-h-4 max-w-36 truncate" loadingClassName="ms-3">
|
||||
{name}
|
||||
</Skeleton>
|
||||
@@ -602,9 +601,9 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
||||
}`}>
|
||||
<div className="me-3">
|
||||
{teamMenuState[index].teamMenuOpen ? (
|
||||
<Icon name="chevron-down" className="h-4 w-4" />
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<Icon name="chevron-right" className="h-4 w-4" />
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
{}
|
||||
@@ -826,7 +825,7 @@ const SettingsSidebarContainer = ({
|
||||
href={child.href || "/"}
|
||||
trackingMetadata={child.trackingMetadata}
|
||||
textClassNames="text-emphasis font-medium text-sm"
|
||||
className={`px-2! py-1! min-h-7 h-auto w-fit ${
|
||||
className={`px-2! py-1! min-h-7 h-auto w-full ${
|
||||
tab.children && index === tab.children?.length - 1 && "mb-3!"
|
||||
}`}
|
||||
disableChevron
|
||||
@@ -949,9 +948,9 @@ const SettingsSidebarContainer = ({
|
||||
}`}>
|
||||
<div className="me-3">
|
||||
{otherTeamMenuState[index].teamMenuOpen ? (
|
||||
<Icon name="chevron-down" className="h-4 w-4" />
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<Icon name="chevron-right" className="h-4 w-4" />
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
{}
|
||||
@@ -1036,7 +1035,7 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) =>
|
||||
<button
|
||||
className="hover:bg-emphasis flex items-center space-x-2 rounded-md px-3 py-1 rtl:space-x-reverse"
|
||||
onClick={() => router.back()}>
|
||||
<Icon name="arrow-left" className="text-default h-4 w-4" />
|
||||
<ArrowLeftIcon className="text-default h-4 w-4" />
|
||||
<p className="text-emphasis font-semibold">{t("settings")}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ const Page = async () => {
|
||||
<SettingsHeader
|
||||
title={t("billing")}
|
||||
description={t("manage_billing_description")}
|
||||
borderInShellHeader={false}>
|
||||
borderInShellHeader={true}>
|
||||
<BillingView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ const Page = async () => {
|
||||
<SettingsHeader
|
||||
title={t("billing")}
|
||||
description={t("manage_billing_description")}
|
||||
borderInShellHeader={false}>
|
||||
borderInShellHeader={true}>
|
||||
<BillingView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
|
||||
+3
-29
@@ -1,33 +1,7 @@
|
||||
import { getTranslate, _generateMetadata } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
import { AdminAPIView } from "~/ee/organizations/admin-api";
|
||||
|
||||
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => `${t("admin")} ${t("api_reference")}`,
|
||||
(t) => t("leverage_our_api"),
|
||||
undefined,
|
||||
undefined,
|
||||
"/settings/organizations/admin-api"
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
|
||||
await validateUserHasOrg();
|
||||
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={`${t("admin")} ${t("api_reference")}`}
|
||||
description={t("leverage_our_api")}
|
||||
borderInShellHeader={false}>
|
||||
<AdminAPIView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
const Page = () => {
|
||||
redirect("https://cal.com/docs/api-reference/v2/introduction");
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
+3
-4
@@ -11,8 +11,8 @@ import {
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Checkbox, Label } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { ChevronRightIcon, InfoIcon } from "@coss/ui/icons";
|
||||
|
||||
import { usePermissions } from "./usePermissions";
|
||||
|
||||
@@ -98,8 +98,7 @@ export function AdvancedPermissionGroup({
|
||||
}
|
||||
}}>
|
||||
<div className="flex items-center gap-1.5" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
<ChevronRightIcon
|
||||
className={classNames(
|
||||
"h-4 w-4 transition-transform",
|
||||
isExpanded && !isAllResources ? "rotate-90" : ""
|
||||
@@ -173,7 +172,7 @@ export function AdvancedPermissionGroup({
|
||||
</span>
|
||||
{isAutoEnabled && (
|
||||
<Tooltip content={t("read_permission_auto_enabled_tooltip")}>
|
||||
<Icon name="info" className="text-muted-foreground h-3 w-3" />
|
||||
<InfoIcon className="text-muted-foreground h-3 w-3" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+5
-5
@@ -7,8 +7,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { LockIcon, ShieldCheckIcon, ShieldIcon, UsersIcon } from "@coss/ui/icons";
|
||||
|
||||
import { FingerprintAnimation } from "./FingerprintAnimation";
|
||||
|
||||
@@ -59,7 +59,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
|
||||
<div className="bg-subtle border-subtle w-full stack-y-px overflow-hidden rounded-xl border">
|
||||
<div className="bg-default flex gap-3 p-3">
|
||||
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
|
||||
<Icon name="shield-check" className="text-subtle h-5 w-5" />
|
||||
<ShieldCheckIcon className="text-subtle h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-emphasis text-sm font-semibold">{t("pbac_opt_in_custom_roles_title")}</p>
|
||||
@@ -69,7 +69,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
|
||||
|
||||
<div className="bg-default flex gap-3 p-3">
|
||||
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
|
||||
<Icon name="lock" className="text-subtle h-5 w-5" />
|
||||
<LockIcon className="text-subtle h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-emphasis text-sm font-semibold">
|
||||
@@ -81,7 +81,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
|
||||
|
||||
<div className="bg-default flex gap-3 p-3">
|
||||
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
|
||||
<Icon name="users" className="text-subtle h-5 w-5" />
|
||||
<UsersIcon className="text-subtle h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-emphasis text-sm font-semibold">
|
||||
@@ -93,7 +93,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
|
||||
|
||||
<div className="bg-default flex gap-3 p-3">
|
||||
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
|
||||
<Icon name="shield" className="text-subtle h-5 w-5" />
|
||||
<ShieldIcon className="text-subtle h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-emphasis text-sm font-semibold">
|
||||
|
||||
@@ -7,7 +7,7 @@ import { constructAppImage, constructGenericImage, constructMeetingImage } from
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
||||
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { getTranslation } from "@calcom/i18n/server";
|
||||
import { truncateOnWord } from "@calcom/lib/text";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -17,11 +17,7 @@ async function handler(req: NextRequest) {
|
||||
return NextResponse.json({ message: "email is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
let ip = getIP(req) ?? email.data;
|
||||
const forwardedFor = req.headers.get("x-forwarded-for") as string;
|
||||
if (!ip && forwardedFor) {
|
||||
ip = forwardedFor?.split(",").at(0) ?? email.data;
|
||||
}
|
||||
const ip = getIP(req) ?? email.data;
|
||||
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import type { Mock } from "vitest";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import type { MockResponse } from "@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks";
|
||||
|
||||
import {
|
||||
prismaMock,
|
||||
resetPrismaMock,
|
||||
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks";
|
||||
import {
|
||||
createMockTeam,
|
||||
createMockFoundToken,
|
||||
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
|
||||
import type { SignupBody } from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
|
||||
import {
|
||||
createMockFoundToken,
|
||||
createMockTeam,
|
||||
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
|
||||
import type { Mock } from "vitest";
|
||||
import { vi } from "vitest";
|
||||
|
||||
const mockFindTokenByToken: Mock = vi.fn();
|
||||
const mockValidateAndGetCorrectedUsernameForTeam: Mock = vi.fn();
|
||||
@@ -77,6 +75,22 @@ vi.mock("@calcom/features/watchlist/lib/telemetry", () => ({ sentrySpan: {} }));
|
||||
vi.mock("@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller", () => ({
|
||||
checkIfEmailIsBlockedInWatchlistController: vi.fn().mockResolvedValue(false),
|
||||
}));
|
||||
vi.mock("@calcom/features/di/containers/FeatureRepository", () => ({
|
||||
getFeatureRepository: vi.fn().mockReturnValue({
|
||||
checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(false),
|
||||
}),
|
||||
}));
|
||||
vi.mock("@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository", () => {
|
||||
return {
|
||||
GlobalWatchlistRepository: class {
|
||||
findBlockedEmail = vi.fn().mockResolvedValue(null);
|
||||
createEntry = vi.fn().mockResolvedValue({});
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@calcom/features/watchlist/lib/utils/normalization", () => ({
|
||||
normalizeEmail: vi.fn((e: string) => e.toLowerCase()),
|
||||
}));
|
||||
vi.mock("@calcom/web/lib/buildLegacyCtx", () => ({ buildLegacyRequest: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/signup/utils/organization", () => ({ joinAnyChildTeamOnOrgInvite: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/signup/utils/token", () => ({
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import process from "node:process";
|
||||
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { getLocaleFromRequest } from "@calcom/features/auth/lib/getLocaleFromRequest";
|
||||
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants";
|
||||
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
||||
import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization";
|
||||
import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar";
|
||||
import {
|
||||
findTokenByToken,
|
||||
throwIfTokenExpired,
|
||||
validateAndGetCorrectedUsernameForTeam,
|
||||
} from "@calcom/features/auth/signup/utils/token";
|
||||
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/features/auth/signup/utils/validateUsername";
|
||||
import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository";
|
||||
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { GlobalWatchlistRepository } from "@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository";
|
||||
import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry";
|
||||
import { normalizeEmail } from "@calcom/features/watchlist/lib/utils/normalization";
|
||||
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
import { hashPassword } from "@calcom/lib/auth/hashPassword";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
@@ -19,19 +28,17 @@ import type { CustomNextApiHandler } from "@calcom/lib/server/username";
|
||||
import { usernameHandler } from "@calcom/lib/server/username";
|
||||
import { getTrackingFromCookies } from "@calcom/lib/tracking";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import {
|
||||
CreationSource,
|
||||
IdentityProvider,
|
||||
WatchlistAction,
|
||||
WatchlistSource,
|
||||
WatchlistType,
|
||||
} from "@calcom/prisma/enums";
|
||||
import { signupSchema } from "@calcom/prisma/zod-utils";
|
||||
import { buildLegacyRequest } from "@calcom/web/lib/buildLegacyCtx";
|
||||
|
||||
import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization";
|
||||
import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants";
|
||||
|
||||
import {
|
||||
findTokenByToken,
|
||||
throwIfTokenExpired,
|
||||
validateAndGetCorrectedUsernameForTeam,
|
||||
} from "@calcom/features/auth/signup/utils/token";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["signupCalcomHandler"] });
|
||||
|
||||
@@ -280,15 +287,42 @@ const handler: CustomNextApiHandler = async (body, usernameStatus, query) => {
|
||||
if (process.env.AVATARAPI_USERNAME && process.env.AVATARAPI_PASSWORD) {
|
||||
await prefillAvatar({ email });
|
||||
}
|
||||
// Only send verification email for non-premium usernames
|
||||
// Premium usernames will get a magic link after payment in paymentCallback
|
||||
if (!checkoutSessionId) {
|
||||
sendEmailVerification({
|
||||
email,
|
||||
language: await getLocaleFromRequest(buildLegacyRequest(await headers(), await cookies())),
|
||||
username: username || "",
|
||||
}
|
||||
|
||||
const featureRepository = getFeatureRepository();
|
||||
const signupWatchlistReviewEnabled =
|
||||
await featureRepository.checkIfFeatureIsEnabledGlobally("signup-watchlist-review");
|
||||
|
||||
if (signupWatchlistReviewEnabled && !token) {
|
||||
const globalWatchlistRepo = new GlobalWatchlistRepository(prisma);
|
||||
const normalizedEmail = normalizeEmail(email);
|
||||
const existing = await globalWatchlistRepo.findBlockedEmail(normalizedEmail);
|
||||
|
||||
if (!existing) {
|
||||
await globalWatchlistRepo.createEntry({
|
||||
type: WatchlistType.EMAIL,
|
||||
value: normalizedEmail,
|
||||
action: WatchlistAction.BLOCK,
|
||||
source: WatchlistSource.SIGNUP,
|
||||
description: "Auto-added during signup review",
|
||||
});
|
||||
}
|
||||
|
||||
const userRepository = new UserRepository(prisma);
|
||||
await userRepository.lockByEmail({ email });
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Created user", stripeCustomerId: customer.stripeCustomerId, accountUnderReview: true },
|
||||
{ status: 201 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!checkoutSessionId && !token) {
|
||||
sendEmailVerification({
|
||||
email,
|
||||
language: await getLocaleFromRequest(buildLegacyRequest(await headers(), await cookies())),
|
||||
username: username || "",
|
||||
});
|
||||
}
|
||||
|
||||
if (checkoutSessionId) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { sendOrganizerRequestReminderEmail } from "@calcom/emails/email-manager"
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj";
|
||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { getTranslation } from "@calcom/i18n/server";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { BookingStatus, ReminderType } from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendMonthlyDigestEmail } from "@calcom/emails/workflow-email-service";
|
||||
import { EventsInsights } from "@calcom/features/insights/server/events";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { getTranslation } from "@calcom/i18n/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NextResponse } from "next/server";
|
||||
|
||||
import renderEmail from "@calcom/emails/src/renderEmail";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { getTranslation } from "@calcom/i18n/server";
|
||||
|
||||
/**
|
||||
* This API endpoint is used for development purposes to preview email templates
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import getIP from "@calcom/lib/getIP";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const requestorIp = getIP(req);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
|
||||
import type { NextRequest } from "next/server";
|
||||
import type { Mock } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockConfirmHandler = confirmHandler as unknown as Mock<typeof confirmHandler>;
|
||||
|
||||
@@ -82,9 +82,9 @@ vi.mock("@calcom/features/booking-audit/lib/makeActor", () => ({
|
||||
makeUserActor: vi.fn().mockReturnValue({ type: "user", id: "test-uuid" }),
|
||||
}));
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
// Import after mocks are set up
|
||||
import { GET } from "../route";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const createMockRequest = (url: string): NextRequest => {
|
||||
const urlObj = new URL(url);
|
||||
@@ -97,13 +97,16 @@ const createMockRequest = (url: string): NextRequest => {
|
||||
} as unknown as NextRequest;
|
||||
};
|
||||
|
||||
// Vitest sets NEXT_PUBLIC_WEBAPP_URL to http://app.cal.local:3000 (see vitest.config.mts)
|
||||
const EXPECTED_REDIRECT_ORIGIN = "http://app.cal.local:3000";
|
||||
|
||||
describe("link route", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("GET handler - redirect URL construction", () => {
|
||||
it("should redirect to booking page with the same origin as the request", async () => {
|
||||
it("should redirect to booking page using WEBAPP_URL (fixes localhost redirect when behind proxy)", async () => {
|
||||
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
|
||||
const req = createMockRequest(baseUrl);
|
||||
|
||||
@@ -113,11 +116,11 @@ describe("link route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://app.example.com");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(redirectUrl.pathname).toBe("/booking/test-booking-uid");
|
||||
});
|
||||
|
||||
it("should preserve custom domain origin in redirect URL", async () => {
|
||||
it("should use WEBAPP_URL for redirects, not request.url (avoids localhost when proxy sends localhost)", async () => {
|
||||
const baseUrl = "https://custom-domain.company.com/api/link?action=accept&token=encrypted-token";
|
||||
const req = createMockRequest(baseUrl);
|
||||
|
||||
@@ -127,11 +130,11 @@ describe("link route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://custom-domain.company.com");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(location).not.toContain("localhost");
|
||||
});
|
||||
|
||||
it("should preserve self-hosted domain origin in redirect URL", async () => {
|
||||
it("should use WEBAPP_URL for self-hosted deployments", async () => {
|
||||
const baseUrl = "https://calcom.internal.company.net/api/link?action=reject&token=encrypted-token";
|
||||
const req = createMockRequest(baseUrl);
|
||||
|
||||
@@ -141,11 +144,11 @@ describe("link route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://calcom.internal.company.net");
|
||||
expect(location).not.toContain("localhost");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(redirectUrl.pathname).toBe("/booking/test-booking-uid");
|
||||
});
|
||||
|
||||
it("should construct redirect URLs relative to the request URL for various origins", async () => {
|
||||
it("should construct redirect URLs using WEBAPP_URL regardless of request origin", async () => {
|
||||
const testOrigins = [
|
||||
"https://app.cal.com",
|
||||
"https://acme.cal.com",
|
||||
@@ -164,7 +167,7 @@ describe("link route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe(origin);
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(redirectUrl.pathname).toBe("/booking/test-booking-uid");
|
||||
}
|
||||
});
|
||||
@@ -174,7 +177,6 @@ describe("link route", () => {
|
||||
it("should redirect with error message when confirmHandler throws a TRPCError", async () => {
|
||||
const { TRPCError } = await import("@trpc/server");
|
||||
|
||||
// Mock confirmHandler to throw a TRPCError
|
||||
mockConfirmHandler.mockRejectedValueOnce(
|
||||
new TRPCError({ code: "BAD_REQUEST", message: "Custom error" })
|
||||
);
|
||||
@@ -188,15 +190,14 @@ describe("link route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://app.example.com");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(redirectUrl.pathname).toBe("/booking/test-booking-uid");
|
||||
expect(redirectUrl.searchParams.get("error")).toBe("Custom error");
|
||||
});
|
||||
|
||||
it("should preserve origin in error redirect URL", async () => {
|
||||
it("should use WEBAPP_URL for error redirects (not localhost when behind proxy)", async () => {
|
||||
const { TRPCError } = await import("@trpc/server");
|
||||
|
||||
// Mock confirmHandler to throw a TRPCError
|
||||
mockConfirmHandler.mockRejectedValueOnce(new TRPCError({ code: "INTERNAL_SERVER_ERROR" }));
|
||||
|
||||
const baseUrl = "https://self-hosted.company.org/api/link?action=accept&token=encrypted-token";
|
||||
@@ -208,7 +209,7 @@ describe("link route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://self-hosted.company.org");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(location).not.toContain("localhost");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import process from "node:process";
|
||||
import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { distributedTracing } from "@calcom/lib/tracing/factory";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
enum DirectAction {
|
||||
ACCEPT = "accept",
|
||||
@@ -99,11 +100,11 @@ async function handler(request: NextRequest) {
|
||||
let message = "Error confirming booking";
|
||||
if (e instanceof TRPCError) message = (e as TRPCError).message;
|
||||
return NextResponse.redirect(
|
||||
new URL(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`, request.url)
|
||||
new URL(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`, WEBAPP_URL)
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.redirect(new URL(`/booking/${bookingUid}`, request.url));
|
||||
return NextResponse.redirect(new URL(`/booking/${bookingUid}`, WEBAPP_URL));
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -42,6 +42,7 @@ const mockQueuedFormResponse = {
|
||||
disabled: false,
|
||||
},
|
||||
chosenRouteId: "mock-chosen-route-id",
|
||||
fallbackAction: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
response: {},
|
||||
@@ -126,6 +127,57 @@ describe("queuedResponseHandler", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should pass fallbackAction to onSubmissionOfFormResponse when present", async () => {
|
||||
const fallbackAction = {
|
||||
type: "eventTypeRedirectUrl" as const,
|
||||
value: "team/fallback-event",
|
||||
eventTypeId: 42,
|
||||
};
|
||||
|
||||
vi.mocked(mockRoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue({
|
||||
...mockQueuedFormResponse,
|
||||
fallbackAction,
|
||||
});
|
||||
|
||||
vi.mocked(getSerializableForm).mockResolvedValue({
|
||||
id: "mock-form-id",
|
||||
name: "Test Form",
|
||||
description: "Test Form Description",
|
||||
fields: [],
|
||||
routes: [],
|
||||
userId: 1,
|
||||
teamId: null,
|
||||
position: 1,
|
||||
updatedById: null,
|
||||
disabled: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
settings: {},
|
||||
} as unknown as Awaited<ReturnType<typeof getSerializableForm>>);
|
||||
|
||||
vi.mocked(getResponseToStore).mockReturnValue({} as ReturnType<typeof getResponseToStore>);
|
||||
|
||||
vi.mocked(onSubmissionOfFormResponse).mockResolvedValue(undefined as unknown as Awaited<
|
||||
ReturnType<typeof onSubmissionOfFormResponse>
|
||||
>);
|
||||
|
||||
vi.mocked(mockRoutingFormResponseRepository.recordFormResponse).mockResolvedValue({
|
||||
id: 1,
|
||||
response: {},
|
||||
} as Awaited<ReturnType<typeof mockRoutingFormResponseRepository.recordFormResponse>>);
|
||||
|
||||
await queuedResponseHandler({
|
||||
queuedFormResponseId: "1",
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(onSubmissionOfFormResponse).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fallbackAction,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("if no queued form response is found, should return early", async () => {
|
||||
vi.mocked(mockRoutingFormResponseRepository.getQueuedFormResponseFromId).mockResolvedValue(null);
|
||||
const response = await queuedResponseHandler({
|
||||
|
||||
@@ -72,6 +72,12 @@ export const queuedResponseHandler = async ({
|
||||
}
|
||||
|
||||
const chosenRoute = serializableForm.routes?.find((r) => r.id === queuedFormResponse.chosenRouteId);
|
||||
const fallbackAction = queuedFormResponse.fallbackAction as {
|
||||
type: "customPageMessage" | "externalRedirectUrl" | "eventTypeRedirectUrl";
|
||||
value: string;
|
||||
eventTypeId?: number;
|
||||
} | null;
|
||||
|
||||
await onSubmissionOfFormResponse({
|
||||
form: {
|
||||
...queuedFormResponse.form,
|
||||
@@ -79,6 +85,7 @@ export const queuedResponseHandler = async ({
|
||||
},
|
||||
formResponseInDb: formResponse,
|
||||
chosenRouteAction: chosenRoute ? ("action" in chosenRoute ? chosenRoute.action : null) : null,
|
||||
fallbackAction,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
|
||||
import type { NextRequest } from "next/server";
|
||||
import type { Mock } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockConfirmHandler = confirmHandler as unknown as Mock<typeof confirmHandler>;
|
||||
|
||||
vi.mock("app/api/defaultResponderForAppDir", () => ({
|
||||
@@ -80,6 +81,9 @@ function setMockRequestBody(body: Record<string, unknown>) {
|
||||
mockRequestBody = body;
|
||||
}
|
||||
|
||||
// Vitest sets NEXT_PUBLIC_WEBAPP_URL to http://app.cal.local:3000 (see vitest.config.mts)
|
||||
const EXPECTED_REDIRECT_ORIGIN = "http://app.cal.local:3000";
|
||||
|
||||
function expectErrorRedirect(res: Response, path: string, error: string) {
|
||||
const location = res.headers.get("location");
|
||||
expect(location).toBeTruthy();
|
||||
@@ -88,8 +92,10 @@ function expectErrorRedirect(res: Response, path: string, error: string) {
|
||||
expect(redirectUrl.searchParams.get("error")).toBe(error);
|
||||
}
|
||||
|
||||
import process from "node:process";
|
||||
// Import after mocks are set up
|
||||
import { GET, POST } from "../route";
|
||||
|
||||
const DB = {
|
||||
bookings: {} as Record<
|
||||
string,
|
||||
@@ -179,7 +185,22 @@ describe("verify-booking-token route", () => {
|
||||
});
|
||||
|
||||
describe("GET handler", () => {
|
||||
it("should redirect to booking page with error when action is reject (requires POST)", async () => {
|
||||
it("should process rejection via GET (for email clients that don't support POST)", async () => {
|
||||
createMockBooking({
|
||||
id: 1,
|
||||
uid: "abc123",
|
||||
oneTimePassword: "test-token",
|
||||
recurringEventId: null,
|
||||
});
|
||||
createMockUser({
|
||||
id: 42,
|
||||
uuid: "user-uuid",
|
||||
email: "test@example.com",
|
||||
username: "testuser",
|
||||
role: "USER",
|
||||
destinationCalendar: null,
|
||||
});
|
||||
|
||||
const baseUrl =
|
||||
"https://app.example.com/api/verify-booking-token?action=reject&token=test-token&bookingUid=abc123&userId=42";
|
||||
const req = createMockRequest(baseUrl, "GET");
|
||||
@@ -189,9 +210,18 @@ describe("verify-booking-token route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://app.example.com");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(redirectUrl.pathname).toBe("/booking/abc123");
|
||||
expect(redirectUrl.searchParams.get("error")).toBe("Rejection requires POST method");
|
||||
expect(redirectUrl.searchParams.get("error")).toBeNull();
|
||||
expect(mockConfirmHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: expect.objectContaining({
|
||||
bookingId: 1,
|
||||
confirmed: false,
|
||||
actionSource: "MAGIC_LINK",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should redirect with error when query params are invalid (missing action)", async () => {
|
||||
@@ -205,7 +235,7 @@ describe("verify-booking-token route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://app.example.com");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(redirectUrl.pathname).toBe("/booking/abc123");
|
||||
expect(redirectUrl.searchParams.get("error")).toBe("Error confirming booking");
|
||||
});
|
||||
@@ -221,12 +251,12 @@ describe("verify-booking-token route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://app.example.com");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(redirectUrl.pathname).toBe("/booking/abc123");
|
||||
expect(redirectUrl.searchParams.get("error")).toBe("Error confirming booking");
|
||||
});
|
||||
|
||||
it("should preserve the request origin in redirect URL (not hardcode localhost)", async () => {
|
||||
it("should use WEBAPP_URL for redirects (fixes localhost when behind proxy)", async () => {
|
||||
const baseUrl =
|
||||
"https://custom-domain.example.org/api/verify-booking-token?action=reject&token=t&bookingUid=booking-uid&userId=1";
|
||||
const req = createMockRequest(baseUrl, "GET");
|
||||
@@ -237,7 +267,7 @@ describe("verify-booking-token route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://custom-domain.example.org");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(location).not.toContain("localhost");
|
||||
});
|
||||
|
||||
@@ -270,12 +300,12 @@ describe("verify-booking-token route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://app.example.com");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(redirectUrl.pathname).toBe("/booking/abc123");
|
||||
expect(redirectUrl.searchParams.get("error")).toBe("Error confirming booking");
|
||||
});
|
||||
|
||||
it("should preserve the request origin in POST redirect URL", async () => {
|
||||
it("should use WEBAPP_URL for POST redirects (fixes localhost when behind proxy)", async () => {
|
||||
const baseUrl =
|
||||
"https://self-hosted.company.com/api/verify-booking-token?bookingUid=uid123&token=t&userId=1&action=reject";
|
||||
const req = createMockRequest(baseUrl, "POST");
|
||||
@@ -286,13 +316,13 @@ describe("verify-booking-token route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe("https://self-hosted.company.com");
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(location).not.toContain("localhost");
|
||||
});
|
||||
});
|
||||
|
||||
describe("redirect URL construction", () => {
|
||||
it("should construct redirect URLs relative to the request URL, not hardcoded origins", async () => {
|
||||
it("should construct redirect URLs using WEBAPP_URL regardless of request origin", async () => {
|
||||
const testOrigins = [
|
||||
"https://app.cal.com",
|
||||
"https://acme.cal.com",
|
||||
@@ -311,7 +341,7 @@ describe("verify-booking-token route", () => {
|
||||
expect(location).toBeTruthy();
|
||||
const redirectUrl = new URL(location!);
|
||||
|
||||
expect(redirectUrl.origin).toBe(origin);
|
||||
expect(redirectUrl.origin).toBe(EXPECTED_REDIRECT_ORIGIN);
|
||||
expect(redirectUrl.pathname).toBe("/booking/test-uid");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { distributedTracing } from "@calcom/lib/tracing/factory";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { parseRequestData } from "app/api/parseRequestData";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { distributedTracing } from "@calcom/lib/tracing/factory";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";
|
||||
|
||||
enum DirectAction {
|
||||
ACCEPT = "accept",
|
||||
REJECT = "reject",
|
||||
@@ -28,21 +28,11 @@ async function getHandler(request: NextRequest) {
|
||||
try {
|
||||
const { action, token, bookingUid, userId } = querySchema.parse(queryParams);
|
||||
|
||||
if (action === DirectAction.REJECT) {
|
||||
// Rejections should use POST method
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
`/booking/${bookingUid}?error=${encodeURIComponent("Rejection requires POST method")}`,
|
||||
request.url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return await handleBookingAction(action, token, bookingUid, userId, request, undefined);
|
||||
} catch {
|
||||
const bookingUid = queryParams.bookingUid || "";
|
||||
return NextResponse.redirect(
|
||||
new URL(`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`, request.url)
|
||||
new URL(`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`, WEBAPP_URL)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +49,7 @@ async function postHandler(request: NextRequest) {
|
||||
} catch {
|
||||
const bookingUid = queryParams.bookingUid || "";
|
||||
return NextResponse.redirect(
|
||||
new URL(`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`, request.url),
|
||||
new URL(`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`, WEBAPP_URL),
|
||||
{ status: 303 }
|
||||
);
|
||||
}
|
||||
@@ -70,7 +60,7 @@ async function handleBookingAction(
|
||||
token: string,
|
||||
bookingUid: string,
|
||||
userId: string,
|
||||
request: NextRequest,
|
||||
_request: NextRequest,
|
||||
reason?: string
|
||||
) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
@@ -79,7 +69,7 @@ async function handleBookingAction(
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.redirect(
|
||||
new URL(`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`, request.url),
|
||||
new URL(`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`, WEBAPP_URL),
|
||||
{ status: 303 }
|
||||
);
|
||||
}
|
||||
@@ -124,7 +114,7 @@ async function handleBookingAction(
|
||||
let message = "Error confirming booking";
|
||||
if (e instanceof TRPCError) message = (e as TRPCError).message;
|
||||
return NextResponse.redirect(
|
||||
new URL(`/booking/${booking.uid}?error=${encodeURIComponent(message)}`, request.url),
|
||||
new URL(`/booking/${booking.uid}?error=${encodeURIComponent(message)}`, WEBAPP_URL),
|
||||
{ status: 303 }
|
||||
);
|
||||
}
|
||||
@@ -134,7 +124,7 @@ async function handleBookingAction(
|
||||
data: { oneTimePassword: null },
|
||||
});
|
||||
|
||||
return NextResponse.redirect(new URL(`/booking/${booking.uid}`, request.url), { status: 303 });
|
||||
return NextResponse.redirect(new URL(`/booking/${booking.uid}`, WEBAPP_URL), { status: 303 });
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import { loadTranslations } from "@calcom/i18n/server";
|
||||
import { IconSprites } from "@calcom/ui/components/icon";
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
import { dir } from "i18next";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { XIcon } from "@coss/ui/icons";
|
||||
|
||||
export default function AddToHomescreen() {
|
||||
const { t } = useLocale();
|
||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
||||
type="button"
|
||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||
<span className="sr-only">{t("dismiss")}</span>
|
||||
<Icon name="x" className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
|
||||
<XIcon className="text-inverted h-6 w-6 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ChartBarIcon, CreditCardIcon, GlobeIcon, LockIcon, PaintbrushIcon, UsersIcon } from "@coss/ui/icons";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
import { UpgradeTip } from "~/shell/UpgradeTip";
|
||||
@@ -13,32 +13,32 @@ export default function EnterprisePage() {
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Icon name="globe" className="h-5 w-5 text-red-500" />,
|
||||
icon: <GlobeIcon className="h-5 w-5 text-red-500" />,
|
||||
title: t("branded_subdomain"),
|
||||
description: t("branded_subdomain_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="chart-bar" className="h-5 w-5 text-blue-500" />,
|
||||
icon: <ChartBarIcon className="h-5 w-5 text-blue-500" />,
|
||||
title: t("org_insights"),
|
||||
description: t("org_insights_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="paintbrush" className="h-5 w-5 text-pink-500" />,
|
||||
icon: <PaintbrushIcon className="h-5 w-5 text-pink-500" />,
|
||||
title: t("extensive_whitelabeling"),
|
||||
description: t("extensive_whitelabeling_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="users" className="h-5 w-5 text-orange-500" />,
|
||||
icon: <UsersIcon className="h-5 w-5 text-orange-500" />,
|
||||
title: t("unlimited_teams"),
|
||||
description: t("unlimited_teams_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="credit-card" className="h-5 w-5 text-green-500" />,
|
||||
icon: <CreditCardIcon className="h-5 w-5 text-green-500" />,
|
||||
title: t("unified_billing"),
|
||||
description: t("advanced_managed_events_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="lock" className="h-5 w-5 text-purple-500" />,
|
||||
icon: <LockIcon className="h-5 w-5 text-purple-500" />,
|
||||
title: t("advanced_managed_events"),
|
||||
description: t("advanced_managed_events_description"),
|
||||
},
|
||||
|
||||
@@ -19,7 +19,15 @@ import type { App as AppType } from "@calcom/types/App";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import {
|
||||
BookOpenIcon,
|
||||
CircleAlertIcon,
|
||||
ExternalLinkIcon,
|
||||
FileIcon,
|
||||
FlagIcon,
|
||||
MailIcon,
|
||||
ShieldIcon,
|
||||
} from "@coss/ui/icons";
|
||||
import { SkeletonButton, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
@@ -385,7 +393,7 @@ export const AppPage = ({
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="text-info flex items-start">
|
||||
<div>
|
||||
<Icon name="circle-alert" className="mr-2 mt-1 font-semibold" />
|
||||
<CircleAlertIcon className="mr-2 mt-1 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">{t("msteams_calendar_warning_body")}</span>
|
||||
@@ -445,7 +453,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||
href={docs}>
|
||||
<Icon name="book-open" className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||
<BookOpenIcon className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -457,7 +465,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={website}>
|
||||
<Icon name="external-link" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<ExternalLinkIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -469,7 +477,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={`mailto:${email}`}>
|
||||
<Icon name="mail" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<MailIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
|
||||
{email}
|
||||
</a>
|
||||
@@ -482,7 +490,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={tos}>
|
||||
<Icon name="file" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<FileIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -494,7 +502,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<Icon name="shield" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<ShieldIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -505,7 +513,7 @@ export const AppPage = ({
|
||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||
</span>
|
||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||
<Icon name="flag" className="inline h-3 w-3" /> {t("report_app")}
|
||||
<FlagIcon className="inline h-3 w-3" /> {t("report_app")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { InfoIcon } from "@coss/ui/icons";
|
||||
|
||||
export default function AlbySetup(props: IAlbySetupProps) {
|
||||
const params = useCompatSearchParams();
|
||||
@@ -165,7 +165,7 @@ function AlbySetupPage(props: IAlbySetupProps) {
|
||||
|
||||
{/* TODO: remove when invoices are generated using user identifier */}
|
||||
<div className="mt-4 rounded bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-950 dark:text-blue-300">
|
||||
<Icon name="info" className="mb-0.5 inline-flex h-4 w-4" /> Your Alby lightning address will be
|
||||
<InfoIcon className="mb-0.5 inline-flex h-4 w-4" /> Your Alby lightning address will be
|
||||
used to generate invoices. If you update your lightning address, please disconnect and setup the
|
||||
Alby app again.
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,8 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { LoaderIcon } from "@coss/ui/icons";
|
||||
|
||||
export default function BTCPaySetup(props: IBTCPaySetupProps) {
|
||||
const params = useCompatSearchParams();
|
||||
@@ -324,7 +324,7 @@ function BTCPaySetupPage(props: IBTCPaySetupProps) {
|
||||
disabled={loading || validating || !updatable}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Icon name="loader" className="mr-2 animate-spin" />
|
||||
<LoaderIcon className="mr-2 animate-spin" />
|
||||
{t("connecting")}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { InfoIcon } from "@coss/ui/icons";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
export default function HitPaySetup(props: IHitPaySetupProps) {
|
||||
@@ -205,7 +205,7 @@ function HitPaySetupPage(props: IHitPaySetupProps) {
|
||||
{showContent ? (
|
||||
<div className="flex w-full w-full max-w-[43em] flex-col items-center justify-center stack-y-4 p-4 lg:stack-y-5">
|
||||
<div className="rounded bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-950 dark:text-blue-300">
|
||||
<Icon name="info" className="mb-0.5 inline-flex h-4 w-4" /> Create or connect to an existing
|
||||
<InfoIcon className="mb-0.5 inline-flex h-4 w-4" /> Create or connect to an existing
|
||||
HitPay account to receive payments for your paid bookings.
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Alert } from "@calcom/ui/components/alert";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Form } from "@calcom/ui/components/form";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { PlusIcon, TrashIcon } from "@coss/ui/icons";
|
||||
|
||||
export default function ICSFeedSetup() {
|
||||
const { t } = useLocale();
|
||||
@@ -78,7 +78,7 @@ export default function ICSFeedSetup() {
|
||||
type="button"
|
||||
className="mb-2 h-min text-sm"
|
||||
onClick={() => setUrls((urls) => urls.filter((_, ii) => i !== ii))}>
|
||||
<Icon name="trash" size={16} />
|
||||
<TrashIcon size={16} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@ export default function ICSFeedSetup() {
|
||||
onClick={() => {
|
||||
setUrls((urls) => urls.concat(""));
|
||||
}}>
|
||||
{t("add")} <Icon className="inline" name="plus" size={16} />
|
||||
{t("add")} <PlusIcon className="inline" size={16} />
|
||||
</button>
|
||||
|
||||
{errorMessage && (
|
||||
|
||||
@@ -8,9 +8,9 @@ import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { ClipboardIcon } from "@coss/ui/icons";
|
||||
|
||||
const MAKE = "make";
|
||||
|
||||
@@ -164,7 +164,7 @@ const CopyApiKey = ({ apiKey }: { apiKey: string }) => {
|
||||
}}
|
||||
type="button"
|
||||
className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
|
||||
<Icon name="clipboard" className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
<ClipboardIcon className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{t("copy")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { CircleAlertIcon } from "@coss/ui/icons";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
export default function PayPalSetup() {
|
||||
@@ -15,10 +15,17 @@ export default function PayPalSetup() {
|
||||
const [newSecretKey, setNewSecretKey] = useState("");
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
const integrations = trpc.viewer.apps.integrations.useQuery({ variant: "payment", appId: "paypal" });
|
||||
|
||||
const integrations = trpc.viewer.apps.integrations.useQuery({
|
||||
variant: "payment",
|
||||
appId: "paypal",
|
||||
});
|
||||
|
||||
const [paypalPaymentAppCredentials] = integrations.data?.items || [];
|
||||
const [credentialId] = paypalPaymentAppCredentials?.userCredentialIds || [-1];
|
||||
|
||||
const showContent = !!integrations.data && integrations.isSuccess && !!credentialId;
|
||||
|
||||
const saveKeysMutation = trpc.viewer.apps.updateAppCredentials.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("keys_have_been_saved"), "success");
|
||||
@@ -39,12 +46,13 @@ export default function PayPalSetup() {
|
||||
<div className="bg-default border-subtle m-auto max-w-[43em] overflow-auto rounded border pb-10 md:p-10">
|
||||
<div className="ml-2 ltr:mr-2 rtl:ml-2 md:ml-5">
|
||||
<div className="invisible md:visible">
|
||||
<img className="h-11" src="/api/app-store/paypal/icon.svg" alt="Paypal Payment Logo" />
|
||||
<p className="text-default mt-5 text-lg">Paypal</p>
|
||||
<img className="h-11" src="/api/app-store/paypal/icon.svg" alt={t("paypal_payment_logo")} />
|
||||
<p className="text-default mt-5 text-lg">{t("paypal")}</p>
|
||||
</div>
|
||||
|
||||
<form autoComplete="off" className="mt-5">
|
||||
<TextField
|
||||
label="Client Id"
|
||||
label={t("client_id")}
|
||||
type="text"
|
||||
name="client_id"
|
||||
id="client_id"
|
||||
@@ -55,7 +63,7 @@ export default function PayPalSetup() {
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Secret Key"
|
||||
label={t("secret_key")}
|
||||
type="password"
|
||||
name="access_token"
|
||||
id="access_token"
|
||||
@@ -65,7 +73,6 @@ export default function PayPalSetup() {
|
||||
onChange={(e) => setNewSecretKey(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Button to submit */}
|
||||
<div className="mt-5 flex flex-row justify-end">
|
||||
<Button
|
||||
color="primary"
|
||||
@@ -82,43 +89,39 @@ export default function PayPalSetup() {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<p className="text-lgf text-default mt-5 font-bold">Getting started with Paypal APP</p>
|
||||
<p className="text-default font-semi mt-2">
|
||||
Here in Cal.com we offer Paypal as one of our payment gateway. You can use your own Paypal
|
||||
Business account to receive payments from your customers enabling and setting up price and
|
||||
currency for each of your event types.
|
||||
</p>
|
||||
<p className="text-lgf text-default mt-5 font-bold">{t("paypal_getting_started")}</p>
|
||||
|
||||
<p className="text-default font-semi mt-2">{t("paypal_description")}</p>
|
||||
|
||||
<p className="text-lgf text-default mt-5 inline-flex font-bold">
|
||||
<Icon name="circle-alert" className="mr-2 mt-1 h-4 w-4" /> Important requirements:
|
||||
<CircleAlertIcon className="mr-2 mt-1 h-4 w-4" />
|
||||
{t("important_requirements")}
|
||||
</p>
|
||||
|
||||
<ul className="text-default ml-1 mt-2 list-disc pl-2">
|
||||
<li>Paypal Business account</li>
|
||||
<li>Paypal Developer account</li>
|
||||
<li>{t("paypal_business_account")}</li>
|
||||
<li>{t("paypal_developer_account")}</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-default mb-2 mt-5 font-bold">Resources:</p>
|
||||
<p className="text-default mb-2 mt-5 font-bold">{t("resources")}</p>
|
||||
|
||||
<a
|
||||
className="text-orange-600 underline"
|
||||
target="_blank"
|
||||
href="https://developer.paypal.com/api/rest/#link-getclientidandclientsecret"
|
||||
rel="noreferrer">
|
||||
Link to Paypal developer API REST Setup Guide:
|
||||
https://developer.paypal.com/api/rest/#link-getclientidandclientsecret
|
||||
{t("paypal_rest_setup_guide")}
|
||||
</a>
|
||||
|
||||
<p className="text-lgf text-default mt-5 font-bold">Setup instructions</p>
|
||||
<p className="text-default font-semi mt-2">
|
||||
Remember to only proceed with the following steps if your account has already been upgraded to
|
||||
a business account. Also keep in mind that some of the following steps might be different
|
||||
since Paypal offers different experiences based on your country.
|
||||
</p>
|
||||
<p className="text-lgf text-default mt-5 font-bold">{t("paypal_setup_instructions")}</p>
|
||||
|
||||
<p className="text-default font-semi mt-2">{t("paypal_setup_note")}</p>
|
||||
|
||||
<ol className="text-default ml-1 mt-5 list-decimal pl-2">
|
||||
{/* @TODO: translate */}
|
||||
<li>
|
||||
Log into your Paypal Developer account and create a new app{" "}
|
||||
{t("paypal_step_1")}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://developer.paypal.com/dashboard/applications/live"
|
||||
@@ -128,39 +131,38 @@ export default function PayPalSetup() {
|
||||
</a>
|
||||
.
|
||||
</li>
|
||||
<li>Choose a name for your application.</li>
|
||||
<li>Click on the Create App button</li>
|
||||
|
||||
<li>{t("paypal_step_2")}</li>
|
||||
<li>{t("paypal_step_3")}</li>
|
||||
|
||||
<li>
|
||||
Go back to{" "}
|
||||
{t("paypal_step_4_part_1")}{" "}
|
||||
<a
|
||||
className="text-orange-600 underline"
|
||||
href="https://developer.paypal.com/dashboard/applications/live">
|
||||
dashboard
|
||||
{t("dashboard")}
|
||||
</a>
|
||||
, click on new app created.
|
||||
, {t("paypal_step_4_part_2")}
|
||||
</li>
|
||||
<li>Copy the Client ID and Secret Key using copy buttons one by one.</li>
|
||||
<li>Paste them on the required field and save them.</li>
|
||||
<li>You should be all setup after this.</li>
|
||||
|
||||
<li>{t("paypal_step_5")}</li>
|
||||
<li>{t("paypal_step_6")}</li>
|
||||
<li>{t("paypal_step_7")}</li>
|
||||
</ol>
|
||||
|
||||
<p className="text-default mt-5 inline-flex font-bold">
|
||||
<Icon name="circle-alert" className="mr-2 mt-1 h-4 w-4" />
|
||||
Reminder:
|
||||
</p>
|
||||
<p className="text-default mt-2">
|
||||
Our integration creates a specific webhook on your Paypal account that we use to report back
|
||||
transactions to our system. If you delete this webhook, we will not be able to report back and
|
||||
you should Uninstall and Install the app again for this to work again. Uninstalling the app
|
||||
won't delete your current event type price/currency configuration but you would not be
|
||||
able to receive bookings.
|
||||
<CircleAlertIcon className="mr-2 mt-1 h-4 w-4" />
|
||||
{t("reminder")}:
|
||||
</p>
|
||||
|
||||
<p className="text-default mt-2">{t("paypal_webhook_reminder")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<AppNotInstalledMessage appName="paypal" />
|
||||
)}
|
||||
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { RoutingFormWithResponseCount } from "@calcom/routing-forms/types/t
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DropdownMenuSeparator } from "@calcom/ui/components/dropdown";
|
||||
import { ToggleGroup } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { MenuIcon, PencilIcon, WaypointsIcon } from "@coss/ui/icons";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
import { FormAction, FormActionsDropdown } from "./FormActions";
|
||||
@@ -298,7 +298,7 @@ export function Header({
|
||||
color="minimal"
|
||||
onClick={() => setIsEditing(true)}
|
||||
CustomStartIcon={
|
||||
<Icon name="pencil" className="text-subtle group-hover:text-default h-3 w-3" />
|
||||
<PencilIcon className="text-subtle group-hover:text-default h-3 w-3" />
|
||||
}>
|
||||
<span className="sr-only">Edit</span>
|
||||
</Button>
|
||||
@@ -319,13 +319,13 @@ export function Header({
|
||||
{
|
||||
value: "form-edit",
|
||||
label: <span className="sr-only sm:not-sr-only">{t("form")}</span>,
|
||||
iconLeft: <Icon name="menu" className="h-3 w-3" />,
|
||||
iconLeft: <MenuIcon className="h-3 w-3" />,
|
||||
dataTestId: "toggle-group-item-form-edit",
|
||||
},
|
||||
{
|
||||
value: "route-builder",
|
||||
label: <span className="sr-only sm:not-sr-only">{t("routing")}</span>,
|
||||
iconLeft: <Icon name="waypoints" className="h-3 w-3" />,
|
||||
iconLeft: <WaypointsIcon className="h-3 w-3" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -355,12 +355,12 @@ export function Header({
|
||||
{
|
||||
value: "form-edit",
|
||||
label: <span className="sr-only sm:not-sr-only">{t("form")}</span>,
|
||||
iconLeft: <Icon name="menu" className="h-3 w-3" />,
|
||||
iconLeft: <MenuIcon className="h-3 w-3" />,
|
||||
},
|
||||
{
|
||||
value: "route-builder",
|
||||
label: <span className="sr-only sm:not-sr-only">{t("routing")}</span>,
|
||||
iconLeft: <Icon name="waypoints" className="h-3 w-3" />,
|
||||
iconLeft: <WaypointsIcon className="h-3 w-3" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ClockIcon } from "@coss/ui/icons";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
interface IConfirmDialogWipe {
|
||||
@@ -83,7 +83,7 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
|
||||
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="flex h-10 w-10 shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<Icon name="clock" className="m-auto h-5 w-5" />
|
||||
<ClockIcon className="m-auto h-5 w-5" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title="Wipe My Calendar" />
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
||||
import { isCancellationReasonRequired } from "@calcom/features/bookings/lib/cancellationReason";
|
||||
import { shouldChargeNoShowCancellationFee } from "@calcom/features/bookings/lib/payment/shouldChargeNoShowCancellationFee";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRefreshData } from "@calcom/lib/hooks/useRefreshData";
|
||||
import type { CancellationReasonRequirement } from "@calcom/prisma/enums";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Label, Select, TextArea, CheckboxField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { CheckboxField, Label, Select, TextArea } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { InfoIcon, XIcon } from "@coss/ui/icons";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
interface InternalNotePresetsSelectProps {
|
||||
internalNotePresets: { id: number; name: string }[];
|
||||
@@ -46,6 +47,7 @@ const InternalNotePresetsSelect = ({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col">
|
||||
<Label>{t("internal_booking_note")}</Label>
|
||||
@@ -109,6 +111,7 @@ type Props = {
|
||||
internalNotePresets: { id: number; name: string; cancellationReason: string | null }[];
|
||||
renderContext: "booking-single-view" | "dialog";
|
||||
eventTypeMetadata?: Record<string, unknown> | null;
|
||||
requiresCancellationReason?: CancellationReasonRequirement | null;
|
||||
showErrorAsToast?: boolean;
|
||||
onCanceled?: () => void;
|
||||
};
|
||||
@@ -162,11 +165,18 @@ export default function CancelBooking(props: Props) {
|
||||
const isCancellationUserHost =
|
||||
props.isHost || bookingCancelledEventProps.organizer.email === currentUserEmail;
|
||||
|
||||
const hostMissingCancellationReason =
|
||||
isCancellationUserHost &&
|
||||
(!cancellationReason?.trim() || (props.internalNotePresets.length > 0 && !internalNote?.id));
|
||||
const isReasonRequired = isCancellationReasonRequired(
|
||||
props.requiresCancellationReason,
|
||||
isCancellationUserHost
|
||||
);
|
||||
|
||||
const missingRequiredReason = isReasonRequired && !cancellationReason?.trim();
|
||||
const hostMissingInternalNote =
|
||||
isCancellationUserHost && props.internalNotePresets.length > 0 && !internalNote?.id;
|
||||
const cancellationNoShowFeeNotAcknowledged =
|
||||
!props.isHost && cancellationNoShowFeeWarning && !acknowledgeCancellationNoShowFee;
|
||||
const canCancel =
|
||||
!missingRequiredReason && !hostMissingInternalNote && !cancellationNoShowFeeNotAcknowledged;
|
||||
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
||||
if (node !== null) {
|
||||
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- CancelBooking is not usually used in embed mode
|
||||
@@ -177,12 +187,69 @@ export default function CancelBooking(props: Props) {
|
||||
|
||||
const isRenderedAsCancelDialog = props.renderContext === "dialog";
|
||||
|
||||
const handleCancel =async()=>{
|
||||
try{
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch("/api/csrf?sameSite=none", { cache: "no-store" });
|
||||
const { csrfToken } = await response.json();
|
||||
|
||||
const res = await fetch("/api/cancel", {
|
||||
body: JSON.stringify({
|
||||
uid: booking?.uid,
|
||||
cancellationReason: cancellationReason,
|
||||
allRemainingBookings,
|
||||
// @NOTE: very important this shouldn't cancel with number ID use uid instead
|
||||
seatReferenceUid,
|
||||
cancelledBy: currentUserEmail,
|
||||
internalNote: internalNote,
|
||||
csrfToken,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const bookingWithCancellationReason = {
|
||||
...(bookingCancelledEventProps.booking as object),
|
||||
cancellationReason,
|
||||
} as unknown;
|
||||
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
sdkActionManager?.fire("bookingCancelled", {
|
||||
...bookingCancelledEventProps,
|
||||
booking: bookingWithCancellationReason,
|
||||
});
|
||||
refreshData();
|
||||
if (props.onCanceled) {
|
||||
props.onCanceled();
|
||||
}
|
||||
} else {
|
||||
const data = await res.json();
|
||||
const errorMessage =
|
||||
data.message ||
|
||||
`${t("error_with_status_code_occured", { status: res.status })} ${t(
|
||||
"please_try_again"
|
||||
)}`;
|
||||
|
||||
if (props.showErrorAsToast) {
|
||||
showToast(errorMessage, "error");
|
||||
} else {
|
||||
setError(errorMessage);
|
||||
}
|
||||
}
|
||||
} finally{
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && !props.showErrorAsToast && (
|
||||
<div className="mt-8">
|
||||
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<Icon name="x" className="h-6 w-6 text-red-600" />
|
||||
<XIcon className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
@@ -217,7 +284,7 @@ export default function CancelBooking(props: Props) {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Label>{isCancellationUserHost ? t("cancellation_reason_host") : t("cancellation_reason")}</Label>
|
||||
<Label>{t(isReasonRequired ? "cancellation_reason" : "cancellation_reason_optional_label")}</Label>
|
||||
|
||||
<TextArea
|
||||
data-testid="cancel_reason"
|
||||
@@ -230,7 +297,7 @@ export default function CancelBooking(props: Props) {
|
||||
/>
|
||||
{isCancellationUserHost ? (
|
||||
<div className="-mt-2 mb-4 flex items-center gap-2">
|
||||
<Icon name="info" className="text-subtle h-4 w-4" />
|
||||
<InfoIcon className="text-subtle h-4 w-4" />
|
||||
<p className="text-subtle text-sm leading-none">
|
||||
{t("notify_attendee_cancellation_reason_warning")}
|
||||
</p>
|
||||
@@ -267,63 +334,8 @@ export default function CancelBooking(props: Props) {
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="confirm_cancel"
|
||||
disabled={hostMissingCancellationReason || cancellationNoShowFeeNotAcknowledged}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
// telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
|
||||
|
||||
const response = await fetch("/api/csrf?sameSite=none", { cache: "no-store" });
|
||||
const { csrfToken } = await response.json();
|
||||
|
||||
const res = await fetch("/api/cancel", {
|
||||
body: JSON.stringify({
|
||||
uid: booking?.uid,
|
||||
cancellationReason: cancellationReason,
|
||||
allRemainingBookings,
|
||||
// @NOTE: very important this shouldn't cancel with number ID use uid instead
|
||||
seatReferenceUid,
|
||||
cancelledBy: currentUserEmail,
|
||||
internalNote: internalNote,
|
||||
csrfToken,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const bookingWithCancellationReason = {
|
||||
...(bookingCancelledEventProps.booking as object),
|
||||
cancellationReason,
|
||||
} as unknown;
|
||||
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
// tested by apps/web/playwright/booking-pages.e2e.ts
|
||||
sdkActionManager?.fire("bookingCancelled", {
|
||||
...bookingCancelledEventProps,
|
||||
booking: bookingWithCancellationReason,
|
||||
});
|
||||
refreshData();
|
||||
if (props.onCanceled) {
|
||||
props.onCanceled();
|
||||
}
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setLoading(false);
|
||||
const errorMessage =
|
||||
data.message ||
|
||||
`${t("error_with_status_code_occured", { status: res.status })} ${t(
|
||||
"please_try_again"
|
||||
)}`;
|
||||
|
||||
if (props.showErrorAsToast) {
|
||||
showToast(errorMessage, "error");
|
||||
} else {
|
||||
setError(errorMessage);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={!canCancel}
|
||||
onClick={handleCancel}
|
||||
loading={loading}>
|
||||
{props.allRemainingBookings ? t("cancel_all_remaining") : t("cancel_event")}
|
||||
</Button>
|
||||
|
||||
@@ -8,8 +8,8 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import { MultiEmail } from "@calcom/ui/components/address";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { TriangleAlertIcon, UserPlusIcon } from "@coss/ui/icons";
|
||||
|
||||
interface IAddGuestsDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -59,7 +59,7 @@ export const AddGuestsDialog = (props: IAddGuestsDialog) => {
|
||||
<DialogContent enableOverflow>
|
||||
<div className="flex flex-row md:space-x-3">
|
||||
<div className="bg-subtle hidden h-10 w-10 shrink-0 justify-center rounded-full md:flex">
|
||||
<Icon name="user-plus" className="m-auto h-6 w-6" />
|
||||
<UserPlusIcon className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full md:pt-1">
|
||||
<DialogHeader title={t("additional_guests")} />
|
||||
@@ -75,7 +75,7 @@ export const AddGuestsDialog = (props: IAddGuestsDialog) => {
|
||||
{isInvalidEmail && (
|
||||
<div className="my-4 flex text-sm text-red-700">
|
||||
<div className="shrink-0">
|
||||
<Icon name="triangle-alert" className="h-5 w-5" />
|
||||
<TriangleAlertIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { CancellationReasonRequirement } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { DialogContent, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
@@ -48,6 +49,7 @@ interface ICancelBookingDialog {
|
||||
isHost: boolean;
|
||||
internalNotePresets?: { id: number; name: string; cancellationReason: string | null }[];
|
||||
eventTypeMetadata?: Record<string, unknown> | null;
|
||||
requiresCancellationReason?: CancellationReasonRequirement | null;
|
||||
}
|
||||
|
||||
export const CancelBookingDialog = (props: ICancelBookingDialog) => {
|
||||
@@ -67,6 +69,7 @@ export const CancelBookingDialog = (props: ICancelBookingDialog) => {
|
||||
isHost,
|
||||
internalNotePresets = [],
|
||||
eventTypeMetadata,
|
||||
requiresCancellationReason,
|
||||
} = props;
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
@@ -113,6 +116,7 @@ export const CancelBookingDialog = (props: ICancelBookingDialog) => {
|
||||
isHost={isHost}
|
||||
internalNotePresets={internalNotePresets}
|
||||
eventTypeMetadata={eventTypeMetadata}
|
||||
requiresCancellationReason={requiresCancellationReason}
|
||||
showErrorAsToast={true}
|
||||
onCanceled={handleCanceled}
|
||||
/>
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader, DialogClose } from "@calcom/ui/components/dialog";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { CreditCardIcon, TriangleAlertIcon } from "@coss/ui/icons";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -45,7 +45,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className=" bg-subtle flex h-10 w-10 shrink-0 justify-center rounded-full">
|
||||
<Icon name="credit-card" className="m-auto h-6 w-6" />
|
||||
<CreditCardIcon className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("charge_card")} />
|
||||
@@ -53,7 +53,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
|
||||
{chargeError && (
|
||||
<div className="mt-4 flex text-red-500">
|
||||
<Icon name="triangle-alert" className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||
<TriangleAlertIcon className="mr-2 h-5 w-5" />
|
||||
<p className="text-sm">{chargeError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
import { Form, Input } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { MapPinIcon } from "@coss/ui/icons";
|
||||
|
||||
import { QueryCell } from "../../lib/QueryCell";
|
||||
|
||||
@@ -258,7 +258,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
}}>
|
||||
<div className="flex flex-row md:space-x-3">
|
||||
<div className="bg-subtle hidden h-10 w-10 shrink-0 justify-center rounded-full md:flex">
|
||||
<Icon name="map-pin" className="m-auto h-6 w-6" />
|
||||
<MapPinIcon className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full md:pt-1">
|
||||
<DialogHeader title={t("edit_location")} />
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
ConfirmationDialogContent,
|
||||
} from "@calcom/ui/components/dialog";
|
||||
import { TextAreaField, Form, Label, Input } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { RadioAreaGroup as RadioArea } from "@calcom/ui/components/radio";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { CheckIcon, LoaderIcon } from "@coss/ui/icons";
|
||||
|
||||
enum ReassignType {
|
||||
AUTO = "auto",
|
||||
@@ -287,7 +287,7 @@ export const ReassignDialog = ({
|
||||
{isFetching && teamMemberOptions.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Icon name="loader" className="text-subtle h-5 w-5 animate-spin" />
|
||||
<LoaderIcon className="text-subtle h-5 w-5 animate-spin" />
|
||||
<p className="text-subtle text-sm">{t("loading")}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,7 +328,7 @@ export const ReassignDialog = ({
|
||||
<span className="text-emphasis w-full text-sm">{member.label}</span>
|
||||
{watchedTeamMemberId === member.value && (
|
||||
<div className="place-self-end pr-2">
|
||||
<Icon name="check" className="text-emphasis h-4 w-4" />
|
||||
<CheckIcon className="text-emphasis h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
import { TextArea } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { ClockIcon } from "@coss/ui/icons";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -39,7 +39,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent enableOverflow data-testid="reschedule-dialog">
|
||||
<div className="flex flex-row md:space-x-3">
|
||||
<div className="bg-subtle hidden h-10 w-10 shrink-0 justify-center rounded-full md:flex">
|
||||
<Icon name="clock" className="m-auto h-6 w-6" />
|
||||
<ClockIcon className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full md:pt-1">
|
||||
<DialogHeader title={t("send_reschedule_request")} />
|
||||
|
||||
@@ -10,8 +10,8 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { ArrowRightIcon, CircleAlertIcon } from "@coss/ui/icons";
|
||||
|
||||
interface IAppConnectionItem {
|
||||
title: string;
|
||||
@@ -79,7 +79,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="flex items-start">
|
||||
<div>
|
||||
<Icon name="circle-alert" className="mr-2 mt-1 font-semibold" />
|
||||
<CircleAlertIcon className="mr-2 mt-1 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-semibold">
|
||||
@@ -99,7 +99,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||
<span className="mr-1">
|
||||
{t("connect_app", { dependencyName: dependency.name })}
|
||||
</span>
|
||||
<Icon name="arrow-right" className="inline-block h-3 w-3" />
|
||||
<ArrowRightIcon className="inline-block h-3 w-3" />
|
||||
</Link>
|
||||
</>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ShieldIcon } from "@coss/ui/icons";
|
||||
|
||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="bg-brand/5 text-brandcontrast dark:bg-darkmodebrand/5 dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||
<Icon name="shield" className="text-inverted h-6 w-6" />
|
||||
<ShieldIcon className="text-inverted h-6 w-6" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Label, TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { InfoIcon } from "@coss/ui/icons";
|
||||
|
||||
type OAuthClientFormProps = {
|
||||
defaultValues?: Partial<FormValues>;
|
||||
@@ -227,7 +227,7 @@ export const OAuthClientForm = ({
|
||||
className="max-w-[400px] whitespace-normal"
|
||||
content="If enabled and the managed user has calendar connected, an event in the calendar will be created. By default true. Disable it if you want to create events in the calendar manually.">
|
||||
<div className="ml-1">
|
||||
<Icon name="info" className="h-4 w-4 text-gray-500" aria-hidden="true" />
|
||||
<InfoIcon className="h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -252,7 +252,7 @@ export const OAuthClientForm = ({
|
||||
className="max-w-[400px] whitespace-normal"
|
||||
content="If enabled, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Leave this disabled if you want to create a managed user and then manually create event types for the user.">
|
||||
<div className="ml-1">
|
||||
<Icon name="info" className="h-4 w-4 text-gray-500" aria-hidden="true" />
|
||||
<InfoIcon className="h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { CheckIcon, LoaderIcon } from "@coss/ui/icons";
|
||||
|
||||
type EnterpriseLicenseFormValues = {
|
||||
licenseKey: string;
|
||||
@@ -122,9 +122,9 @@ const EnterpriseLicense = (
|
||||
)}
|
||||
addOnSuffix={
|
||||
checkLicenseLoading ? (
|
||||
<Icon name="loader" className="h-5 w-5 animate-spin" />
|
||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
||||
) : errors.licenseKey === undefined && isDirty ? (
|
||||
<Icon name="check" className="h-5 w-5 text-green-700" />
|
||||
<CheckIcon className="h-5 w-5 text-green-700" />
|
||||
) : undefined
|
||||
}
|
||||
color={errors.licenseKey ? "warn" : ""}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user