From fb79a5bf3ec062c5dfb20b911c4085b095664716 Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Wed, 30 Aug 2023 13:28:29 +0530 Subject: [PATCH] Enable pricing (#151) * Enable Pro plan - WIP * no pricing page if have no paid plans * Set pricing ids in env * views & submissions FREE for all * extra param for env * form password FREE for all * Custom Code is PRO feature * Replace codeinput prism with codemirror * Better form Cleaning message * Added risky user email spam protection * fix form cleaning * Pricing page new UI * form cleaner * Polish changes * Fixed tests --------- Co-authored-by: Julien Nahum --- .env.example | 14 + app/Http/Controllers/Forms/FormController.php | 16 +- .../Controllers/Forms/FormStatsController.php | 7 +- .../Forms/PublicFormController.php | 5 +- .../Controllers/SubscriptionController.php | 28 +- .../Middleware/Form/PasswordProtectedForm.php | 2 +- app/Http/Requests/AnswerFormRequest.php | 22 +- .../UpdateStripeDetailsRequest.php | 21 ++ app/Http/Resources/FormResource.php | 13 +- .../Forms/SubmissionConfirmation.php | 25 +- app/Models/User.php | 9 + app/Models/Workspace.php | 30 +- app/Service/Forms/FormCleaner.php | 97 +++--- config/pricing.php | 36 +-- package-lock.json | 63 ++-- package.json | 4 +- resources/js/components/Navbar.vue | 15 +- resources/js/components/common/Collapse.vue | 17 +- resources/js/components/common/ProTag.vue | 13 +- resources/js/components/forms/CodeInput.vue | 45 +-- .../js/components/forms/ToggleSwitchInput.vue | 4 +- .../open/forms/OpenCompleteForm.vue | 37 +-- .../js/components/open/forms/OpenForm.vue | 4 +- .../forms/components/FormFieldsEditor.vue | 5 - .../open/forms/components/FormStats.vue | 4 +- .../form-components/FormAboutSubmission.vue | 10 +- .../form-components/FormCustomCode.vue | 5 +- .../form-components/FormCustomization.vue | 10 +- .../form-components/FormSecurityPrivacy.vue | 1 - .../FormBlockLogicEditor.vue | 1 - .../forms/fields/FormFieldOptionsModal.vue | 5 - .../pages/forms/show/FormCleanings.vue | 107 +++++++ .../pages/forms/show/UrlFormPrefill.vue | 2 - .../pages/pricing/CheckoutDetailsModal.vue | 94 ++++++ .../pages/pricing/MonthlyYearlySelector.vue | 41 +++ .../components/pages/pricing/PricingTable.vue | 132 ++++++++ resources/js/config/form-themes.js | 6 +- resources/js/mixins/forms/saveUpdateAlert.js | 15 +- resources/js/pages/forms/show/index.vue | 10 +- resources/js/pages/pricing.vue | 281 ++++++++++++++++++ resources/js/pages/settings/index.vue | 2 +- resources/js/router/routes.js | 1 + resources/views/spa.blade.php | 3 +- routes/api.php | 7 +- tests/Feature/Forms/ConfirmationEmailTest.php | 2 +- tests/Feature/Forms/FormStatTest.php | 4 +- tests/Feature/Forms/FormUpdateTest.php | 2 +- vite.config.js | 3 +- 48 files changed, 1011 insertions(+), 269 deletions(-) create mode 100644 app/Http/Requests/Subscriptions/UpdateStripeDetailsRequest.php create mode 100644 resources/js/components/pages/forms/show/FormCleanings.vue create mode 100644 resources/js/components/pages/pricing/CheckoutDetailsModal.vue create mode 100644 resources/js/components/pages/pricing/MonthlyYearlySelector.vue create mode 100644 resources/js/components/pages/pricing/PricingTable.vue create mode 100644 resources/js/pages/pricing.vue diff --git a/.env.example b/.env.example index bcd5726..f781ac1 100644 --- a/.env.example +++ b/.env.example @@ -56,7 +56,21 @@ JWT_SECRET= STRIPE_KEY= STRIPE_SECRET= +STRIPE_PROD_DEFAULT_PRODUCT_ID= +STRIPE_PROD_DEFAULT_PRICING_MONTHLY= +STRIPE_PROD_DEFAULT_PRICING_YEARLY= + +STRIPE_TEST_DEFAULT_PRODUCT_ID= +STRIPE_TEST_DEFAULT_PRICING_MONTHLY= +STRIPE_TEST_DEFAULT_PRICING_YEARLY= + +H_CAPTCHA_SITE_KEY= +H_CAPTCHA_SECRET= + MUX_WORKSPACE_ID= MUX_API_TOKEN= +ADMIN_EMAILS= +TEMPLATE_EDITOR_EMAILS= + OPEN_AI_API_KEY= diff --git a/app/Http/Controllers/Forms/FormController.php b/app/Http/Controllers/Forms/FormController.php index 5865f1d..80f2561 100644 --- a/app/Http/Controllers/Forms/FormController.php +++ b/app/Http/Controllers/Forms/FormController.php @@ -34,13 +34,21 @@ class FormController extends Controller $this->authorize('viewAny', Form::class); $workspaceIsPro = $workspace->is_pro; - $forms = $workspace->forms()->with(['creator','views','submissions'])->paginate(10)->through(function (Form $form) use ($workspace, $workspaceIsPro){ + $forms = $workspace->forms()->with(['creator','views','submissions']) + ->orderByDesc('updated_at') + ->paginate(10)->through(function (Form $form) use ($workspace, $workspaceIsPro){ + // Add attributes for faster loading $form->extra = (object) [ 'loadedWorkspace' => $workspace, 'workspaceIsPro' => $workspaceIsPro, 'userIsOwner' => true, + 'cleanings' => $this->formCleaner + ->processForm(request(), $form) + ->simulateCleaning($workspace) + ->getPerformedCleanings() ]; + return $form; }); return FormResource::collection($forms); @@ -91,8 +99,7 @@ class FormController extends Controller return $this->success([ 'message' => $this->formCleaner->hasCleaned() ? 'Form successfully created, but the Pro features you used will be disabled when sharing your form:' : 'Form created.', - 'form_cleaning' => $this->formCleaner->getPerformedCleanings(), - 'form' => new FormResource($form), + 'form' => (new FormResource($form))->setCleanings($this->formCleaner->getPerformedCleanings()), 'users_first_form' => $request->user()->forms()->count() == 1 ]); } @@ -116,8 +123,7 @@ class FormController extends Controller return $this->success([ 'message' => $this->formCleaner->hasCleaned() ? 'Form successfully updated, but the Pro features you used will be disabled when sharing your form:' : 'Form updated.', - 'form_cleaning' => $this->formCleaner->getPerformedCleanings(), - 'form' => new FormResource($form) + 'form' => (new FormResource($form))->setCleanings($this->formCleaner->getPerformedCleanings()), ]); } diff --git a/app/Http/Controllers/Forms/FormStatsController.php b/app/Http/Controllers/Forms/FormStatsController.php index c422376..211f52e 100644 --- a/app/Http/Controllers/Forms/FormStatsController.php +++ b/app/Http/Controllers/Forms/FormStatsController.php @@ -5,8 +5,6 @@ namespace App\Http\Controllers\Forms; use App\Http\Controllers\Controller; use App\Models\Forms\Form; use Carbon\CarbonPeriod; -use App\Models\Forms\FormStatistic; -use Illuminate\Http\Request; class FormStatsController extends Controller { @@ -15,9 +13,10 @@ class FormStatsController extends Controller $this->middleware('auth'); } - public function getFormStats(Request $request) + public function getFormStats(string $formId) { - $form = $request->form; // Added by ProForm middleware + $form = Form::findOrFail($formId); + $this->authorize('view', $form); $formStats = $form->statistics()->where('date','>',now()->subDays(29)->startOfDay())->get(); diff --git a/app/Http/Controllers/Forms/PublicFormController.php b/app/Http/Controllers/Forms/PublicFormController.php index 8856ba7..8520b60 100644 --- a/app/Http/Controllers/Forms/PublicFormController.php +++ b/app/Http/Controllers/Forms/PublicFormController.php @@ -45,9 +45,8 @@ class PublicFormController extends Controller $form->views()->create(); } - $formResource = new FormResource($form); - $formResource->setCleanings($formCleaner->getPerformedCleanings()); - return $formResource; + return (new FormResource($form)) + ->setCleanings($formCleaner->getPerformedCleanings()); } public function listUsers(Request $request) diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php index 5ffcb27..e3e3710 100644 --- a/app/Http/Controllers/SubscriptionController.php +++ b/app/Http/Controllers/SubscriptionController.php @@ -2,13 +2,14 @@ namespace App\Http\Controllers; +use App\Http\Requests\Subscriptions\UpdateStripeDetailsRequest; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; use Laravel\Cashier\Subscription; class SubscriptionController extends Controller { - const SUBSCRIPTION_PLANS = ['monthly_2022', 'yearly_2022']; + const SUBSCRIPTION_PLANS = ['monthly', 'yearly']; const PRO_SUBSCRIPTION_NAME = 'default'; const ENTERPRISE_SUBSCRIPTION_NAME = 'enterprise'; @@ -41,7 +42,7 @@ class SubscriptionController extends Controller ->allowPromotionCodes(); if ($trial != null) { - $checkoutBuilder->trialDays(3); + $checkoutBuilder->trialUntil(now()->addDays(3)->addHour()); } $checkout = $checkoutBuilder @@ -49,6 +50,11 @@ class SubscriptionController extends Controller ->checkout([ 'success_url' => url('/subscriptions/success'), 'cancel_url' => url('/subscriptions/error'), + 'billing_address_collection' => 'required', + 'customer_update' => [ + 'address' => 'auto', + 'name' => 'never', + ] ]); return $this->success([ @@ -56,6 +62,22 @@ class SubscriptionController extends Controller ]); } + public function updateStripeDetails(UpdateStripeDetailsRequest $request) + { + $user = Auth::user(); + if (!$user->hasStripeId()) { + $user->createAsStripeCustomer(); + } + $user->updateStripeCustomer([ + 'email' => $request->email, + 'name' => $request->name, + ]); + + return $this->success([ + 'message' => 'Details saved.', + ]); + } + public function billingPortal() { $this->middleware('auth'); @@ -69,7 +91,7 @@ class SubscriptionController extends Controller ]); } - private function getPricing($product = 'pro') + private function getPricing($product = 'default') { return App::environment() == 'production' ? config('pricing.production.'.$product.'.pricing') : config('pricing.test.'.$product.'.pricing'); } diff --git a/app/Http/Middleware/Form/PasswordProtectedForm.php b/app/Http/Middleware/Form/PasswordProtectedForm.php index 322d7a4..b4e2e15 100644 --- a/app/Http/Middleware/Form/PasswordProtectedForm.php +++ b/app/Http/Middleware/Form/PasswordProtectedForm.php @@ -26,7 +26,7 @@ class PasswordProtectedForm 'form' => $form, ]); $userIsFormOwner = Auth::check() && Auth::user()->workspaces()->find($form->workspace_id) !== null; - if (!$userIsFormOwner && $form->is_pro && $form->has_password) { + if (!$userIsFormOwner && $form->has_password) { if($this->hasCorrectPassword($request, $form)){ return $next($request); } diff --git a/app/Http/Requests/AnswerFormRequest.php b/app/Http/Requests/AnswerFormRequest.php index b5bd53f..8b59b49 100644 --- a/app/Http/Requests/AnswerFormRequest.php +++ b/app/Http/Requests/AnswerFormRequest.php @@ -14,8 +14,8 @@ use App\Rules\ValidHCaptcha; class AnswerFormRequest extends FormRequest { - const MAX_FILE_SIZE_PRO = 5000000; - const MAX_FILE_SIZE_ENTERPRISE = 20000000; + const MAX_FILE_SIZE_FREE = 5000000; // 5 MB + const MAX_FILE_SIZE_PRO = 50000000; // 50 MB public Form $form; @@ -26,10 +26,10 @@ class AnswerFormRequest extends FormRequest { $this->form = $request->form; - $this->maxFileSize = self::MAX_FILE_SIZE_PRO; + $this->maxFileSize = self::MAX_FILE_SIZE_FREE; $workspace = $this->form->workspace; - if ($workspace && $workspace->is_enterprise) { - $this->maxFileSize = self::MAX_FILE_SIZE_ENTERPRISE; + if ($workspace && $workspace->is_pro) { + $this->maxFileSize = self::MAX_FILE_SIZE_PRO; } } @@ -53,9 +53,9 @@ class AnswerFormRequest extends FormRequest foreach ($this->form->properties as $property) { $rules = []; - if (!$this->form->is_pro) { // If not pro then not check logic + /*if (!$this->form->is_pro) { // If not pro then not check logic $property['logic'] = false; - } + }*/ // For get values instead of Id for select/multi select options $data = $this->toArray(); @@ -96,12 +96,12 @@ class AnswerFormRequest extends FormRequest } // Validate hCaptcha - if ($this->form->is_pro && $this->form->use_captcha) { + if ($this->form->use_captcha) { $this->requestRules['h-captcha-response'] = [new ValidHCaptcha()]; } // Validate submission_id for edit mode - if ($this->form->editable_submissions) { + if ($this->form->is_pro && $this->form->editable_submissions) { $this->requestRules['submission_id'] = 'string'; } @@ -160,7 +160,7 @@ class AnswerFormRequest extends FormRequest return ['numeric']; case 'select': case 'multi_select': - if ($this->form->is_pro && ($property['allow_creation'] ?? false)) { + if (($property['allow_creation'] ?? false)) { return ['string']; } return [Rule::in($this->getSelectPropertyOptions($property))]; @@ -174,7 +174,7 @@ class AnswerFormRequest extends FormRequest return ['url']; case 'files': $allowedFileTypes = []; - if($this->form->is_pro && !empty($property['allowed_file_types'])){ + if(!empty($property['allowed_file_types'])){ $allowedFileTypes = explode(",", $property['allowed_file_types']); } $this->requestRules[$property['id'].'.*'] = [new StorageFile($this->maxFileSize, $allowedFileTypes, $this->form)]; diff --git a/app/Http/Requests/Subscriptions/UpdateStripeDetailsRequest.php b/app/Http/Requests/Subscriptions/UpdateStripeDetailsRequest.php new file mode 100644 index 0000000..1dcbbea --- /dev/null +++ b/app/Http/Requests/Subscriptions/UpdateStripeDetailsRequest.php @@ -0,0 +1,21 @@ + + */ + public function rules() + { + return [ + 'name' => 'required|string', + 'email' => 'required|email', + ]; + } +} diff --git a/app/Http/Resources/FormResource.php b/app/Http/Resources/FormResource.php index 253a992..3ffb362 100644 --- a/app/Http/Resources/FormResource.php +++ b/app/Http/Resources/FormResource.php @@ -26,8 +26,8 @@ class FormResource extends JsonResource $ownerData = $this->userIsFormOwner() ? [ 'creator' => new UserResource($this->creator), - 'views_count' => $this->when($this->workspaceIsPro(), $this->views_count), - 'submissions_count' => $this->when($this->workspaceIsPro(), $this->submissions_count), + 'views_count' => $this->views_count, + 'submissions_count' => $this->submissions_count, 'notifies' => $this->notifies, 'notifies_slack' => $this->notifies_slack, 'notifies_discord' => $this->notifies_discord, @@ -35,7 +35,7 @@ class FormResource extends JsonResource 'webhook_url' => $this->webhook_url, 'redirect_url' => $this->redirect_url, 'database_fields_update' => $this->database_fields_update, - 'cleanings' => $this->cleanings, + 'cleanings' => $this->getCleanigns(), 'notification_sender' => $this->notification_sender, 'notification_subject' => $this->notification_subject, 'notification_body' => $this->notification_body, @@ -95,7 +95,7 @@ class FormResource extends JsonResource private function doesMissPassword(Request $request) { - if (!$this->workspaceIsPro() || !$this->has_password) return false; + if (!$this->has_password) return false; return !PasswordProtectedForm::hasCorrectPassword($request, $this->resource); } @@ -132,4 +132,9 @@ class FormResource extends JsonResource && Auth::user()->workspaces()->find($this->workspace_id) !== null ); } + + private function getCleanigns() + { + return $this->extra?->cleanings ?? $this->cleanings; + } } diff --git a/app/Listeners/Forms/SubmissionConfirmation.php b/app/Listeners/Forms/SubmissionConfirmation.php index 6e96a4c..a641455 100644 --- a/app/Listeners/Forms/SubmissionConfirmation.php +++ b/app/Listeners/Forms/SubmissionConfirmation.php @@ -18,6 +18,8 @@ class SubmissionConfirmation implements ShouldQueue { use InteractsWithQueue; + const RISKY_USERS_LIMIT = 120; + /** * Handle the event. * @@ -26,7 +28,13 @@ class SubmissionConfirmation implements ShouldQueue */ public function handle(FormSubmitted $event) { - if (!$event->form->send_submission_confirmation) return; + if ( + !$event->form->is_pro || + !$event->form->send_submission_confirmation || + $this->riskLimitReached($event) // To avoid phishing abuse we limit this feature for risky users + ) { + return; + } $email = $this->getRespondentEmail($event); if (!$email) return; @@ -56,6 +64,21 @@ class SubmissionConfirmation implements ShouldQueue return null; } + private function riskLimitReached(FormSubmitted $event): bool + { + // This is a per-workspace limit for risky workspaces + if ($event->form->workspace->is_risky) { + if ($event->form->workspace->submissions_count >= self::RISKY_USERS_LIMIT) { + \Log::error('!!!DANGER!!! Dangerous user detected! Attempting many email sending.', [ + 'form_id' => $event->form->id, + 'workspace_id' => $event->form->workspace->id, + ]); + return true; + } + } + return false; + } + public static function validateEmail($email): bool { return (boolean) filter_var($email, FILTER_VALIDATE_EMAIL); } diff --git a/app/Models/User.php b/app/Models/User.php index 5b5dba1..ec963b0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -179,6 +179,15 @@ class User extends Authenticatable implements JWTSubject //, MustVerifyEmail return []; } + public function getIsRiskyAttribute() + { + return $this->created_at->isAfter(now()->subDays(3)) || // created in last 3 days + $this->subscriptions()->where(function ($q) { + $q->where('stripe_status', 'trialing') + ->orWhere('stripe_status', 'active'); + })->first()?->onTrial(); + } + public static function boot () { parent::boot(); diff --git a/app/Models/Workspace.php b/app/Models/Workspace.php index a27977f..5974a59 100644 --- a/app/Models/Workspace.php +++ b/app/Models/Workspace.php @@ -24,7 +24,9 @@ class Workspace extends Model public function getIsProAttribute() { - return true; // Temporary true for ALL + if(is_null(config('cashier.key'))){ + return true; // If no paid plan so TRUE for ALL + } // Make sure at least one owner is pro foreach ($this->owners as $owner) { @@ -37,7 +39,9 @@ class Workspace extends Model public function getIsEnterpriseAttribute() { - return true; // Temporary true for ALL + if(is_null(config('cashier.key'))){ + return true; // If no paid plan so TRUE for ALL + } foreach ($this->owners as $owner) { if ($owner->has_enterprise_subscription) { @@ -47,6 +51,28 @@ class Workspace extends Model return false; } + public function getIsRiskyAttribute() + { + // A workspace is risky if all of his users are risky + foreach ($this->owners as $owner) { + if (!$owner->is_risky) { + return false; + } + } + + return true; + } + + public function getSubmissionsCountAttribute() + { + $total = 0; + foreach ($this->forms as $form) { + $total += $form->submissions_count; + } + + return $total; + } + /** * Relationships */ diff --git a/app/Service/Forms/FormCleaner.php b/app/Service/Forms/FormCleaner.php index b93bc60..03e1417 100644 --- a/app/Service/Forms/FormCleaner.php +++ b/app/Service/Forms/FormCleaner.php @@ -11,7 +11,6 @@ use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Stevebauman\Purify\Facades\Purify; -use function App\Service\str_starts_with; use function collect; class FormCleaner @@ -26,76 +25,34 @@ class FormCleaner private array $formDefaults = [ 'notifies' => false, - 'color' => '#3B82F6', - 'hide_title' => false, 'no_branding' => false, - 'transparent_background' => false, - 'uppercase_labels' => true, 'webhook_url' => null, - 'cover_picture' => null, - 'logo_picture' => null, 'database_fields_update' => null, - 'theme' => 'default', - 'use_captcha' => false, - 'password' => null, 'slack_webhook_url' => null, 'discord_webhook_url' => null, + 'editable_submissions' => false, + 'custom_code' => null, ]; private array $fieldDefaults = [ // 'name' => '' TODO: prevent name changing, use alias for column and keep original name as it is - 'hide_field_name' => false, - 'prefill' => null, - 'placeholder' => null, - 'help' => null, 'file_upload' => false, - 'with_time' => null, - 'width' => 'full', - 'generates_uuid' => false, - 'generates_auto_increment_id' => false, - 'logic' => null, - 'allow_creation' => false ]; private array $cleaningMessages = [ // For form 'notifies' => "Email notification were disabled.", - 'color' => "Form color set to default blue.", - 'hide_title' => "Title is not hidden.", 'no_branding' => "OpenForm branding is not hidden.", - 'transparent_background' => "Transparent background was disabled.", - 'uppercase_labels' => "Labels use uppercase letters", 'webhook_url' => "Webhook disabled.", - 'cover_picture' => 'The cover picture was removed.', - 'logo_picture' => 'The logo was removed.', 'database_fields_update' => 'Form submission will only create new records (no updates).', - 'theme' => 'Default theme was applied.', 'slack_webhook_url' => "Slack webhook disabled.", 'discord_webhook_url' => "Discord webhook disabled.", + 'editable_submissions' => 'Users will not be able to edit their submissions.', + 'custom_code' => 'Custom code was disabled', // For fields - 'hide_field_name' => 'Hide field name removed.', - 'prefill' => "Field prefill removed.", - 'placeholder' => "Empty text (placeholder) removed", - 'help' => "Help text removed.", 'file_upload' => "Link field is not a file upload.", - 'with_time' => "Time was removed from date input.", 'custom_block' => 'The custom block was removed.', - 'files' => 'The file upload file was hidden.', - 'relation' => 'The relation file was hidden.', - 'width' => 'The field width was set to full width', - 'allow_creation' => 'Select option creation was disabled.', - - // Advanced fields - 'generates_uuid' => 'ID generation disabled.', - 'generates_auto_increment_id' => 'ID generation disabled.', - - 'use_captcha' => 'Captcha form protection was disabled.', - - // Security & Privacy - 'password' => 'Password protection was disabled', - - 'logic' => 'Logic disabled for this property' ]; /** @@ -144,7 +101,8 @@ class FormCleaner /** * Create form cleaner instance from existing form */ - public function processForm(Request $request, Form $form) : FormCleaner { + public function processForm(Request $request, Form $form) : FormCleaner + { $data = (new FormResource($form))->toArray($request); $this->data = $this->commonCleaning($data); @@ -159,10 +117,11 @@ class FormCleaner * Dry run celanings * @param User|null $user */ - public function simulateCleaning(Workspace $workspace): FormCleaner { - if($this->isPro($workspace)) return $this; - - $this->data = $this->removeProFeatures($this->data, true); + public function simulateCleaning(Workspace $workspace): FormCleaner + { + if (!$this->isPro($workspace)) { + $this->data = $this->removeProFeatures($this->data, true); + } return $this; } @@ -174,9 +133,9 @@ class FormCleaner */ public function performCleaning(Workspace $workspace): FormCleaner { - if($this->isPro($workspace)) return $this; - - $this->data = $this->removeProFeatures($this->data); + if (!$this->isPro($workspace)) { + $this->data = $this->removeProFeatures($this->data); + } return $this; } @@ -212,6 +171,7 @@ class FormCleaner private function cleanProperties(array &$data, $simulation = false): void { foreach ($data['properties'] as $key => &$property) { + /* // Remove pro custom blocks if (\Str::of($property['type'])->startsWith('nf-')) { $this->cleanings[$property['name']][] = 'custom_block'; @@ -221,6 +181,15 @@ class FormCleaner continue; } + // Remove logic + if (($property['logic']['conditions'] ?? null) != null || ($property['logic']['actions'] ?? []) != []) { + $this->cleanings[$property['name']][] = 'logic'; + if (!$simulation) { + unset($data['properties'][$key]['logic']); + } + } + */ + // Clean pro field options $this->cleanField($property, $this->fieldDefaults, $simulation); } @@ -229,8 +198,18 @@ class FormCleaner private function clean(array &$data, array $defaults, $simulation = false): void { foreach ($defaults as $key => $value) { - if (Arr::get($data, $key) !== $value) { - if (!isset($this->cleanings['form'])) $this->cleanings['form'] = []; + + // Get value from form + $formVal = Arr::get($data, $key); + + // Transform boolean values + $formVal = (($formVal === 0 || $formVal === "0") ? false : $formVal); + $formVal = (($formVal === 1 || $formVal === "1") ? true : $formVal); + + if (!is_null($formVal) && $formVal !== $value) { + if (!isset($this->cleanings['form'])) { + $this->cleanings['form'] = []; + } $this->cleanings['form'][] = $key; // If not a simulation, do the cleaning @@ -253,14 +232,14 @@ class FormCleaner } // Remove pro types columns - foreach (['files'] as $proType) { + /*foreach (['files'] as $proType) { if ($data['type'] == $proType && (!isset($data['hidden']) || !$data['hidden'])) { $this->cleanings[$data['name']][] = $proType; if (!$simulation) { $data['hidden'] = true; } } - } + }*/ } } diff --git a/config/pricing.php b/config/pricing.php index b5b17bf..d96e405 100644 --- a/config/pricing.php +++ b/config/pricing.php @@ -4,44 +4,20 @@ return [ 'production' => [ 'default' => [ - 'product_id' => 'prod_JpQMgFHw0PSuzM', + 'product_id' => env('STRIPE_PROD_DEFAULT_PRODUCT_ID'), 'pricing' => [ - 'yearly' => 'price_1JBlWXLQM1kjk4NvEWonKifC', - 'monthly' => 'price_1JBlWELQM1kjk4NvmtrstOpi', - 'yearly_2022' => 'price_1LLmZsLQM1kjk4Nv0oLa6MeZ', - 'monthly_2022' => 'price_1LLmaYLQM1kjk4NvQER36XPA', - ] - ], - - 'enterprise' => [ - 'product_id' => 'prod_KXUeOAd1H42xMM', - 'pricing' => [ - 'yearly' => 'price_1JsPfeLQM1kjk4NvV8MJ53yV', - 'monthly' => 'price_1JsPfeLQM1kjk4NvtSszj1jE', - 'yearly_2022' => 'price_1LLmXALQM1kjk4NvXXz5Rxxv', - 'monthly_2022' => 'price_1LLmXtLQM1kjk4Nv1DShm9zs', + 'monthly' => env('STRIPE_PROD_DEFAULT_PRICING_MONTHLY'), + 'yearly' => env('STRIPE_PROD_DEFAULT_PRICING_YEARLY'), ] ] ], 'test' => [ 'default' => [ - 'product_id' => 'prod_LY0BWzSv0Cl5Db', + 'product_id' => env('STRIPE_TEST_DEFAULT_PRODUCT_ID'), 'pricing' => [ - 'yearly' => 'price_1KquBXKTHIweYlJTHYU6UXA1', - 'monthly' => 'price_1KquBXKTHIweYlJTOGEWKr0B', - 'yearly_2022' => 'price_1LKwvpKTHIweYlJT74vdfJcK', - 'monthly_2022' => 'price_1LKwvLKTHIweYlJTOAyghKkJ', - ] - ], - - 'enterprise' => [ - 'product_id' => 'prod_LY0CdM6YtwODqn', - 'pricing' => [ - 'yearly' => 'price_1KquCYKTHIweYlJTiR3TBMTV', - 'monthly' => 'price_1KquCYKTHIweYlJT4vQVLcQ7', - 'yearly_2022' => 'price_1LKwxCKTHIweYlJTJb71yk4V', - 'monthly_2022' => 'price_1LKwwtKTHIweYlJTqO2JrQv4', + 'monthly' => env('STRIPE_TEST_DEFAULT_PRICING_MONTHLY'), + 'yearly' => env('STRIPE_TEST_DEFAULT_PRICING_YEARLY'), ] ] ] diff --git a/package-lock.json b/package-lock.json index c037f93..09b2fec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,15 +23,15 @@ "tinymotion": "^0.2.0", "vform": "^2.1.1", "vt-notifications": "^0.4.1", - "vue": "^2.6.14", + "vue": "^2.7.14", "vue-chartjs": "^4.1.0", "vue-clickaway": "^2.2.2", + "vue-codemirror": "^4.0.6", "vue-confetti": "^2.3.0", "vue-i18n": "^8.25.0", "vue-meta": "^2.4.0", "vue-notion": "^0.4.0", "vue-plugin-load-script": "^1.3.2", - "vue-prism-editor": "^1.2.2", "vue-router": "^3.5.2", "vue-signature-pad": "^2.0.5", "vue-tailwind": "^2.5.0", @@ -4144,6 +4144,11 @@ "node": ">= 4.0" } }, + "node_modules/codemirror": { + "version": "5.65.15", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.15.tgz", + "integrity": "sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==" + }, "node_modules/collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -4863,6 +4868,11 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, "node_modules/dijkstrajs": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", @@ -12434,6 +12444,19 @@ "vue": "^2.0.0" } }, + "node_modules/vue-codemirror": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/vue-codemirror/-/vue-codemirror-4.0.6.tgz", + "integrity": "sha512-ilU7Uf0mqBNSSV3KT7FNEeRIxH4s1fmpG4TfHlzvXn0QiQAbkXS9lLfwuZpaBVEnpP5CSE62iGJjoliTuA8poQ==", + "dependencies": { + "codemirror": "^5.41.0", + "diff-match-patch": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/vue-confetti": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/vue-confetti/-/vue-confetti-2.3.0.tgz", @@ -12592,17 +12615,6 @@ "resolved": "https://registry.npmjs.org/vue-prism-component/-/vue-prism-component-1.2.0.tgz", "integrity": "sha512-0N9CNuQu+36CJpdsZHrhdq7d18oBvjVMjawyKdIr8xuzFWLfdxECZQYbFaYoopPBg3SvkEEMtkhYqdgTQl5Y+A==" }, - "node_modules/vue-prism-editor": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/vue-prism-editor/-/vue-prism-editor-1.3.0.tgz", - "integrity": "sha512-54RfgtMGRMNr9484zKMOZs1wyLDR6EfFylzE2QrMCD9alCvXyYYcS0vX8oUHh+6pMUu6ts59uSN9cHglpU2NRQ==", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "vue": "^2.6.11" - } - }, "node_modules/vue-property-decorator": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz", @@ -16031,6 +16043,11 @@ "q": "^1.1.2" } }, + "codemirror": { + "version": "5.65.15", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.15.tgz", + "integrity": "sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==" + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -16584,6 +16601,11 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, "dijkstrajs": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", @@ -22312,6 +22334,15 @@ "loose-envify": "^1.2.0" } }, + "vue-codemirror": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/vue-codemirror/-/vue-codemirror-4.0.6.tgz", + "integrity": "sha512-ilU7Uf0mqBNSSV3KT7FNEeRIxH4s1fmpG4TfHlzvXn0QiQAbkXS9lLfwuZpaBVEnpP5CSE62iGJjoliTuA8poQ==", + "requires": { + "codemirror": "^5.41.0", + "diff-match-patch": "^1.0.0" + } + }, "vue-confetti": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/vue-confetti/-/vue-confetti-2.3.0.tgz", @@ -22432,12 +22463,6 @@ "resolved": "https://registry.npmjs.org/vue-prism-component/-/vue-prism-component-1.2.0.tgz", "integrity": "sha512-0N9CNuQu+36CJpdsZHrhdq7d18oBvjVMjawyKdIr8xuzFWLfdxECZQYbFaYoopPBg3SvkEEMtkhYqdgTQl5Y+A==" }, - "vue-prism-editor": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/vue-prism-editor/-/vue-prism-editor-1.3.0.tgz", - "integrity": "sha512-54RfgtMGRMNr9484zKMOZs1wyLDR6EfFylzE2QrMCD9alCvXyYYcS0vX8oUHh+6pMUu6ts59uSN9cHglpU2NRQ==", - "requires": {} - }, "vue-property-decorator": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz", diff --git a/package.json b/package.json index f65c784..bc19dbc 100644 --- a/package.json +++ b/package.json @@ -23,15 +23,15 @@ "tinymotion": "^0.2.0", "vform": "^2.1.1", "vt-notifications": "^0.4.1", - "vue": "^2.6.14", + "vue": "^2.7.14", "vue-chartjs": "^4.1.0", "vue-clickaway": "^2.2.2", + "vue-codemirror": "^4.0.6", "vue-confetti": "^2.3.0", "vue-i18n": "^8.25.0", "vue-meta": "^2.4.0", "vue-notion": "^0.4.0", "vue-plugin-load-script": "^1.3.2", - "vue-prism-editor": "^1.2.2", "vue-router": "^3.5.2", "vue-signature-pad": "^2.0.5", "vue-tailwind": "^2.5.0", diff --git a/resources/js/components/Navbar.vue b/resources/js/components/Navbar.vue index 0d96402..d781552 100644 --- a/resources/js/components/Navbar.vue +++ b/resources/js/components/Navbar.vue @@ -8,9 +8,7 @@ BETA + {{ appName }} @@ -23,6 +21,11 @@ class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"> Templates + + Upgrade + Pricing + @@ -158,6 +161,12 @@ export default { } return null }, + workspace () { + return this.$store.getters['open/workspaces/getCurrent']() + }, + paidPlansEnabled() { + return window.config.paid_plans_enabled + }, showAuth() { return this.$route.name && !this.$route.name.startsWith('forms.show_public') }, diff --git a/resources/js/components/common/Collapse.vue b/resources/js/components/common/Collapse.vue index 488aaea..969c914 100644 --- a/resources/js/components/common/Collapse.vue +++ b/resources/js/components/common/Collapse.vue @@ -5,8 +5,13 @@
- - + +
@@ -20,21 +25,23 @@ diff --git a/resources/js/components/forms/CodeInput.vue b/resources/js/components/forms/CodeInput.vue index 8526df7..169730e 100644 --- a/resources/js/components/forms/CodeInput.vue +++ b/resources/js/components/forms/CodeInput.vue @@ -7,13 +7,13 @@ * - + + /> + @@ -23,31 +23,32 @@ diff --git a/resources/js/components/forms/ToggleSwitchInput.vue b/resources/js/components/forms/ToggleSwitchInput.vue index f1d3144..073e591 100644 --- a/resources/js/components/forms/ToggleSwitchInput.vue +++ b/resources/js/components/forms/ToggleSwitchInput.vue @@ -5,7 +5,9 @@
- {{ label }} * + + {{ label }} * +
diff --git a/resources/js/components/open/forms/OpenCompleteForm.vue b/resources/js/components/open/forms/OpenCompleteForm.vue index 38e4352..d649e94 100644 --- a/resources/js/components/open/forms/OpenCompleteForm.vue +++ b/resources/js/components/open/forms/OpenCompleteForm.vue @@ -49,23 +49,7 @@ -
-
-

- You're seeing this because you are an owner of this form.
- All your Pro features are de-activated when sharing this form:
- - -

-
-
- - Close - -
-
+ 0) { - let message = '' - Object.keys(this.form.cleanings).forEach((key) => { - const fieldName = key.charAt(0).toUpperCase() + key.slice(1) - let fieldInfo = '
' + fieldName + "
    " - this.form.cleanings[key].forEach((msg) => { - fieldInfo = fieldInfo + '
  • ' + msg + '
  • ' - }) - message = message + fieldInfo + '
      ' - }) - - return message - } - return false - }, isPublicFormPage () { return this.$route.name === 'forms.show_public' }, diff --git a/resources/js/components/open/forms/OpenForm.vue b/resources/js/components/open/forms/OpenForm.vue index 03505cc..11b13d5 100644 --- a/resources/js/components/open/forms/OpenForm.vue +++ b/resources/js/components/open/forms/OpenForm.vue @@ -7,8 +7,8 @@
      - - - - - + diff --git a/resources/js/components/pages/forms/show/UrlFormPrefill.vue b/resources/js/components/pages/forms/show/UrlFormPrefill.vue index 230de87..33cf1b3 100644 --- a/resources/js/components/pages/forms/show/UrlFormPrefill.vue +++ b/resources/js/components/pages/forms/show/UrlFormPrefill.vue @@ -13,7 +13,6 @@ /> Url form pre-fill - @@ -26,7 +25,6 @@
      diff --git a/resources/js/components/pages/pricing/CheckoutDetailsModal.vue b/resources/js/components/pages/pricing/CheckoutDetailsModal.vue new file mode 100644 index 0000000..f2645be --- /dev/null +++ b/resources/js/components/pages/pricing/CheckoutDetailsModal.vue @@ -0,0 +1,94 @@ + + + diff --git a/resources/js/components/pages/pricing/MonthlyYearlySelector.vue b/resources/js/components/pages/pricing/MonthlyYearlySelector.vue new file mode 100644 index 0000000..11a7f2c --- /dev/null +++ b/resources/js/components/pages/pricing/MonthlyYearlySelector.vue @@ -0,0 +1,41 @@ + + + diff --git a/resources/js/components/pages/pricing/PricingTable.vue b/resources/js/components/pages/pricing/PricingTable.vue new file mode 100644 index 0000000..eddcf13 --- /dev/null +++ b/resources/js/components/pages/pricing/PricingTable.vue @@ -0,0 +1,132 @@ + + + diff --git a/resources/js/config/form-themes.js b/resources/js/config/form-themes.js index f32b095..720e05e 100644 --- a/resources/js/config/form-themes.js +++ b/resources/js/config/form-themes.js @@ -13,7 +13,7 @@ export const themes = { }, CodeInput: { label: 'text-gray-700 dark:text-gray-300 font-semibold', - input: 'rounded-lg border-gray-300 flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent p-2', + input: 'rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden', help: 'text-gray-400 dark:text-gray-500' }, RichTextAreaInput: { @@ -43,7 +43,7 @@ export const themes = { }, CodeInput: { label: 'text-gray-700 dark:text-gray-300 font-semibold', - input: 'border-transparent flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-2 focus:border-transparent p-2', + input: 'border border-gray-300 dark:border-gray-600 overflow-hidden', help: 'text-gray-400 dark:text-gray-500' }, RichTextAreaInput: { @@ -68,7 +68,7 @@ export const themes = { }, CodeInput: { label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4', - input: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion border border-gray-300 dark:border-gray-600 w-full text-gray-900 bg-notion-input-background dark:bg-notion-dark-light shadow-inner dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-0 focus:border-transparent focus:shadow-focus-notion p-2', + input: 'rounded shadow-inner-notion border border-gray-300 dark:border-gray-600 overflow-hidden', help: 'text-notion-input-help dark:text-gray-500' }, RichTextAreaInput: { diff --git a/resources/js/mixins/forms/saveUpdateAlert.js b/resources/js/mixins/forms/saveUpdateAlert.js index 9909fda..17c6c0f 100644 --- a/resources/js/mixins/forms/saveUpdateAlert.js +++ b/resources/js/mixins/forms/saveUpdateAlert.js @@ -1,17 +1,8 @@ export default { methods: { - displayFormModificationAlert(responseData) { - if (responseData.form_cleaning && Object.keys(responseData.form_cleaning).length > 0) { - let message = responseData.message + '
      ' - Object.keys(responseData.form_cleaning).forEach((key) => { - const fieldName = key.charAt(0).toUpperCase() + key.slice(1) - let fieldInfo = "
      " + fieldName + "
        " - responseData.form_cleaning[key].forEach((msg) => { - fieldInfo = fieldInfo + "
      • " + msg +"
      • " - }) - message = message + fieldInfo + "
          " - }) - this.alertWarning(message) + displayFormModificationAlert (responseData) { + if (responseData.form.cleanings && Object.keys(responseData.form.cleanings).length > 0) { + this.alertWarning(responseData.message) } else { this.alertSuccess(responseData.message) } diff --git a/resources/js/pages/forms/show/index.vue b/resources/js/pages/forms/show/index.vue index 5d970c8..482fa45 100644 --- a/resources/js/pages/forms/show/index.vue +++ b/resources/js/pages/forms/show/index.vue @@ -67,11 +67,13 @@ This form will stop accepting submissions after {{ form.max_submissions_count }} submissions.

          -
          + + +
          • {{tab.name}}
          • @@ -109,6 +111,7 @@ import ProTag from '../../../components/common/ProTag.vue' import VButton from "../../../components/common/Button.vue"; import ExtraMenu from '../../../components/pages/forms/show/ExtraMenu.vue' import SeoMeta from '../../../mixins/seo-meta.js' +import FormCleanings from '../../../components/pages/forms/show/FormCleanings.vue' const loadForms = function () { store.commit('open/forms/startLoading') @@ -122,7 +125,8 @@ export default { components: { VButton, ProTag, - ExtraMenu + ExtraMenu, + FormCleanings }, mixins: [SeoMeta], diff --git a/resources/js/pages/pricing.vue b/resources/js/pages/pricing.vue new file mode 100644 index 0000000..a83b22c --- /dev/null +++ b/resources/js/pages/pricing.vue @@ -0,0 +1,281 @@ + + + diff --git a/resources/js/pages/settings/index.vue b/resources/js/pages/settings/index.vue index b1b924f..b371a51 100644 --- a/resources/js/pages/settings/index.vue +++ b/resources/js/pages/settings/index.vue @@ -16,7 +16,7 @@
            • {{tab.name}}
            • diff --git a/resources/js/router/routes.js b/resources/js/router/routes.js index ac52ef3..d31da75 100644 --- a/resources/js/router/routes.js +++ b/resources/js/router/routes.js @@ -61,6 +61,7 @@ export default [ // Guest Routes { path: '/', name: 'welcome', component: page('welcome.vue') }, + { path: '/pricing', name: 'pricing', component: page('pricing.vue') }, { path: '/ai-form-builder', name: 'aiformbuilder', component: page('ai-form-builder.vue') }, { path: '/integrations', name: 'integrations', component: page('integrations.vue') }, { path: '/forms/:slug', name: 'forms.show_public', component: page('forms/show-public.vue') }, diff --git a/resources/views/spa.blade.php b/resources/views/spa.blade.php index 0be87c3..6d909c5 100644 --- a/resources/views/spa.blade.php +++ b/resources/views/spa.blade.php @@ -14,7 +14,8 @@ 'amplitude_code' => config('services.amplitude_code'), 'crisp_website_id' => config('services.crisp_website_id'), 'ai_features_enabled' => !is_null(config('services.openai.api_key')), - 's3_enabled' => config('filesystems.default') === 's3' + 's3_enabled' => config('filesystems.default') === 's3', + 'paid_plans_enabled' => !is_null(config('cashier.key')) ]; @endphp diff --git a/routes/api.php b/routes/api.php index e588568..005fe67 100644 --- a/routes/api.php +++ b/routes/api.php @@ -41,6 +41,7 @@ Route::group(['middleware' => 'auth:api'], function () { Route::patch('settings/password', [PasswordController::class, 'update']); Route::prefix('subscription')->name('subscription.')->group(function () { + Route::put('/update-customer-details', [SubscriptionController::class, 'updateStripeDetails'])->name('update-stripe-details'); Route::get('/new/{subscription}/{plan}/checkout/{trial?}', [SubscriptionController::class, 'checkout']) ->name('checkout') ->where('subscription', '('.implode('|', SubscriptionController::SUBSCRIPTION_NAMES).')') @@ -70,9 +71,7 @@ Route::group(['middleware' => 'auth:api'], function () { [FormController::class, 'index'])->name('forms.index'); Route::delete('/', [WorkspaceController::class, 'delete'])->name('delete'); - Route::middleware('pro-form')->group(function () { - Route::get('form-stats/{formId}', [FormStatsController::class, 'getFormStats'])->name('form.stats'); - }); + Route::get('form-stats/{formId}', [FormStatsController::class, 'getFormStats'])->name('form.stats'); }); }); @@ -84,7 +83,7 @@ Route::group(['middleware' => 'auth:api'], function () { Route::get('/{id}/submissions', [FormSubmissionController::class, 'submissions'])->name('submissions'); Route::get('/{id}/submissions/export', [FormSubmissionController::class, 'export'])->name('submissions.export'); Route::get('/{id}/submissions/file/{filename}', [FormSubmissionController::class, 'submissionFile'])->name('submissions.file'); - + Route::delete('/{id}/records/{recordid}/delete', [RecordController::class, 'delete'])->name('records.delete'); // Form Admin tool diff --git a/tests/Feature/Forms/ConfirmationEmailTest.php b/tests/Feature/Forms/ConfirmationEmailTest.php index 3e7df13..2f7683f 100644 --- a/tests/Feature/Forms/ConfirmationEmailTest.php +++ b/tests/Feature/Forms/ConfirmationEmailTest.php @@ -50,7 +50,7 @@ it('creates confirmation emails without the submitted data', function () { }); it('sends a confirmation email if needed', function () { - $user = $this->actingAsUser(); + $user = $this->actingAsProUser(); $workspace = $this->createUserWorkspace($user); $form = $this->createForm($user, $workspace, [ 'send_submission_confirmation' => true, diff --git a/tests/Feature/Forms/FormStatTest.php b/tests/Feature/Forms/FormStatTest.php index de1a7f6..19222cc 100644 --- a/tests/Feature/Forms/FormStatTest.php +++ b/tests/Feature/Forms/FormStatTest.php @@ -38,6 +38,8 @@ it('check formstat chart data', function () { } // Now check chart data + $response = $this->getJson(route('open.workspaces.form.stats', [$workspace->id, $form->id])); + $this->getJson(route('open.workspaces.form.stats', [$workspace->id, $form->id])) ->assertSuccessful() ->assertJson(function (\Illuminate\Testing\Fluent\AssertableJson $json) use ($views, $submissions) { @@ -62,4 +64,4 @@ it('check formstat chart data', function () { ->etc(); }); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Forms/FormUpdateTest.php b/tests/Feature/Forms/FormUpdateTest.php index 7de4c21..1a0f446 100644 --- a/tests/Feature/Forms/FormUpdateTest.php +++ b/tests/Feature/Forms/FormUpdateTest.php @@ -3,7 +3,7 @@ use Tests\Helpers\FormSubmissionDataFactory; it('can update form with existing record', function () { - $user = $this->actingAsUser(); + $user = $this->actingAsProUser(); $workspace = $this->createUserWorkspace($user); $form = $this->createForm($user, $workspace, [ 'editable_submissions' => true, diff --git a/vite.config.js b/vite.config.js index 09d936c..daf4732 100644 --- a/vite.config.js +++ b/vite.config.js @@ -11,7 +11,8 @@ export default defineConfig({ laravel({ input: [ 'resources/js/app.js' - ] + ], + valetTls: 'opnform.test' }), vue({ template: {