diff --git a/e2e/utils/billing.ts b/e2e/utils/billing.ts new file mode 100644 index 00000000..ecadb90f --- /dev/null +++ b/e2e/utils/billing.ts @@ -0,0 +1,14 @@ +import { expect } from '@playwright/test'; +import { PLAYWRIGHT_BASE_URL } from '../../playwright/config'; +import type { TestContext } from './api'; + +/** + * End the trial of the test organization, only works with the Billing extension. Reload the page + * afterwards, the page props still contain the trial otherwise. + */ +export async function cancelTrialViaApi(ctx: TestContext): Promise { + const response = await ctx.request.post( + `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/trial/cancel` + ); + expect(response.status()).toBe(204); +} diff --git a/playwright.config.ts b/playwright.config.ts index cfb92834..945e4c8d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,5 @@ import { defineConfig, devices } from '@playwright/test'; -import { existsSync, readFileSync } from 'node:fs'; +import { enabledExtensions } from './playwright/config'; /** * Read environment variables from file. @@ -7,23 +7,11 @@ import { existsSync, readFileSync } from 'node:fs'; */ // require('dotenv').config(); -/** +/* * Extensions ship their e2e specs in extensions//tests/e2e. Only the specs of extensions * that are enabled in modules_statuses.json (php artisan module:enable ) are collected, * a checked out but disabled extension would fail its specs otherwise. - */ -function enabledExtensions(): string[] { - const statusesFile = './modules_statuses.json'; - if (!existsSync(statusesFile)) { - return []; - } - const statuses: Record = JSON.parse(readFileSync(statusesFile, 'utf-8')); - return Object.entries(statuses) - .filter(([, enabled]) => enabled) - .map(([name]) => name); -} - -/* + * * Every test root gets its own testDir (instead of testDir: '.') so playwright does not walk the * whole repository, including node_modules, vendor and nested worktrees, to find the specs. */ diff --git a/playwright/config.ts b/playwright/config.ts index 69783876..f5633c55 100644 --- a/playwright/config.ts +++ b/playwright/config.ts @@ -1,3 +1,23 @@ +import { existsSync, readFileSync } from 'node:fs'; + export const PLAYWRIGHT_BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://solidtime.test'; export const MAILPIT_BASE_URL = process.env.MAILPIT_BASE_URL ?? 'http://mailpit:8025'; export const TEST_USER_PASSWORD = 'amazingpassword123'; + +/** + * Extensions that are enabled in modules_statuses.json (php artisan module:enable ) + */ +export function enabledExtensions(): string[] { + const statusesFile = './modules_statuses.json'; + if (!existsSync(statusesFile)) { + return []; + } + const statuses: Record = JSON.parse(readFileSync(statusesFile, 'utf-8')); + return Object.entries(statuses) + .filter(([, enabled]) => enabled) + .map(([name]) => name); +} + +export function isExtensionEnabled(name: string): boolean { + return enabledExtensions().includes(name); +} diff --git a/resources/js/Components/Billing/BillingBanner.vue b/resources/js/Components/Billing/BillingBanner.vue index 4f23784f..fe40cc9e 100644 --- a/resources/js/Components/Billing/BillingBanner.vue +++ b/resources/js/Components/Billing/BillingBanner.vue @@ -1,137 +1,21 @@ diff --git a/resources/js/Components/Billing/CancelTrialModal.vue b/resources/js/Components/Billing/CancelTrialModal.vue new file mode 100644 index 00000000..1bd3e1bd --- /dev/null +++ b/resources/js/Components/Billing/CancelTrialModal.vue @@ -0,0 +1,86 @@ + + + diff --git a/resources/js/Components/Billing/SidebarPlanStatus.vue b/resources/js/Components/Billing/SidebarPlanStatus.vue new file mode 100644 index 00000000..4115666d --- /dev/null +++ b/resources/js/Components/Billing/SidebarPlanStatus.vue @@ -0,0 +1,56 @@ + + + diff --git a/resources/js/Components/Billing/TrialUpgradeModal.vue b/resources/js/Components/Billing/TrialUpgradeModal.vue new file mode 100644 index 00000000..39716075 --- /dev/null +++ b/resources/js/Components/Billing/TrialUpgradeModal.vue @@ -0,0 +1,79 @@ + + + diff --git a/resources/js/Components/Common/Project/ProjectTableRow.vue b/resources/js/Components/Common/Project/ProjectTableRow.vue index 5be8d918..2a0164b2 100644 --- a/resources/js/Components/Common/Project/ProjectTableRow.vue +++ b/resources/js/Components/Common/Project/ProjectTableRow.vue @@ -18,7 +18,7 @@ import ProjectEditModal from '@/Components/Common/Project/ProjectEditModal.vue'; import { formatCents } from '@/packages/ui/src/utils/money'; import { getOrganizationCurrencyString } from '@/utils/money'; import EstimatedTimeProgress from '@/packages/ui/src/EstimatedTimeProgress.vue'; -import UpgradeBadge from '@/Components/Common/UpgradeBadge.vue'; +import UpgradeBadge from '@/packages/ui/src/UpgradeBadge.vue'; import { formatHumanReadableDuration } from '../../../packages/ui/src/utils/time'; import { isAllowedToPerformPremiumAction } from '@/utils/billing'; import { canUpdateProjects, canDeleteProjects } from '@/utils/permissions'; @@ -119,7 +119,9 @@ const showEditProjectModal = ref(false);
--
- + - - Sharable Reports is only available in solidtime Professional. - + Save Report diff --git a/resources/js/Components/Common/Reporting/ReportingExportButton.vue b/resources/js/Components/Common/Reporting/ReportingExportButton.vue index 310870e7..63a22964 100644 --- a/resources/js/Components/Common/Reporting/ReportingExportButton.vue +++ b/resources/js/Components/Common/Reporting/ReportingExportButton.vue @@ -10,7 +10,7 @@ import { import type { ExportFormat } from '@/types/reporting'; import { ref } from 'vue'; import { isAllowedToPerformPremiumAction } from '@/utils/billing'; -import UpgradeModal from '@/Components/Common/UpgradeModal.vue'; +import UpgradeModal from '@/packages/ui/src/UpgradeModal.vue'; const props = defineProps<{ download: (format: ExportFormat) => Promise; @@ -48,9 +48,7 @@ function triggerDownload(format: ExportFormat) { Export as ODS - - PDF Reports are only available in solidtime Professional. - + diff --git a/resources/js/Components/Common/Reporting/ReportingOverview.vue b/resources/js/Components/Common/Reporting/ReportingOverview.vue index 4a1b0af1..227f82b8 100644 --- a/resources/js/Components/Common/Reporting/ReportingOverview.vue +++ b/resources/js/Components/Common/Reporting/ReportingOverview.vue @@ -30,7 +30,8 @@ import { DropdownMenuTrigger, } from '@/packages/ui/src'; import ReportCreateModal from '@/Components/Common/Report/ReportCreateModal.vue'; -import UpgradeModal from '@/Components/Common/UpgradeModal.vue'; +import UpgradeModal from '@/packages/ui/src/UpgradeModal.vue'; +import type { UpgradeFeature } from '@/packages/ui/src/UpgradeModal/useUpgradeCta'; import { canCreateReports } from '@/utils/permissions'; import { isAllowedToPerformPremiumAction } from '@/utils/billing'; import { computed, type ComputedRef, inject, ref, watch } from 'vue'; @@ -214,10 +215,13 @@ const showExportModal = ref(false); const exportUrl = ref(null); const showCreateReportModal = ref(false); const showPremiumModal = ref(false); +// Note: The premium modal is shared by the PDF export and saving a report +const premiumFeature = ref('pdf-reports'); const exportLoading = ref(false); function triggerExport(format: ExportFormat) { if (format === 'pdf' && !isAllowedToPerformPremiumAction()) { + premiumFeature.value = 'pdf-reports'; showPremiumModal.value = true; return; } @@ -231,6 +235,7 @@ function onSaveReportClick() { if (isAllowedToPerformPremiumAction()) { showCreateReportModal.value = true; } else { + premiumFeature.value = 'shareable-reports'; showPremiumModal.value = true; } } @@ -299,9 +304,7 @@ const tableData = computed(() => { - - This feature is only available in solidtime Professional. - +
diff --git a/resources/js/Components/Common/Task/TaskTableRow.vue b/resources/js/Components/Common/Task/TaskTableRow.vue index 99c52a81..e8e62501 100644 --- a/resources/js/Components/Common/Task/TaskTableRow.vue +++ b/resources/js/Components/Common/Task/TaskTableRow.vue @@ -9,7 +9,7 @@ import TaskEditModal from '@/Components/Common/Task/TaskEditModal.vue'; import { ref, inject, type ComputedRef } from 'vue'; import { isAllowedToPerformPremiumAction } from '@/utils/billing'; import EstimatedTimeProgress from '@/packages/ui/src/EstimatedTimeProgress.vue'; -import UpgradeBadge from '@/Components/Common/UpgradeBadge.vue'; +import UpgradeBadge from '@/packages/ui/src/UpgradeBadge.vue'; import { formatHumanReadableDuration } from '../../../packages/ui/src/utils/time'; import type { Organization } from '@/packages/api/src'; @@ -55,7 +55,9 @@ const showTaskEditModal = ref(false); --
- + -import DialogModal from '@/packages/ui/src/DialogModal.vue'; -import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue'; -import { Link } from '@inertiajs/vue3'; -import { isBillingActivated } from '@/utils/billing'; -import { CreditCardIcon, UserGroupIcon } from '@heroicons/vue/20/solid'; -import { canManageBilling, canUpdateOrganization } from '@/utils/permissions'; -import { SecondaryButton } from '@/packages/ui/src'; - -const show = defineModel('show', { default: false }); - - - - - diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index 4bad78e2..9e869082 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -1,5 +1,5 @@