From 8fb842fe8b89241f8c72d60efb3eea257c18a9a0 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Tue, 18 Jul 2023 16:53:41 +0530
Subject: [PATCH 01/16] Enable Pro plan - WIP
---
.../Controllers/SubscriptionController.php | 26 +-
app/Http/Middleware/Form/ProForm.php | 2 +-
app/Http/Requests/AnswerFormRequest.php | 22 +-
.../UpdateStripeDetailsRequest.php | 21 +
app/Models/Workspace.php | 8 +-
app/Service/Forms/FormCleaner.php | 90 +-
resources/js/components/Navbar.vue | 12 +-
resources/js/components/common/ProTag.vue | 13 +-
.../js/components/forms/ToggleSwitchInput.vue | 4 +-
.../js/components/open/forms/OpenForm.vue | 6 +-
.../forms/components/FormFieldsEditor.vue | 5 -
.../open/forms/components/FormStats.vue | 4 +-
.../form-components/FormAboutSubmission.vue | 10 +-
.../form-components/FormCustomization.vue | 10 +-
.../form-components/FormSecurityPrivacy.vue | 1 -
.../FormBlockLogicEditor.vue | 1 -
.../forms/fields/FormFieldOptionsModal.vue | 5 -
.../pages/forms/show/UrlFormPrefill.vue | 2 -
.../pages/pricing/CheckoutDetailsModal.vue | 94 +++
.../pages/pricing/MonthlyYearlySelector.vue | 41 +
.../components/pages/pricing/PricingTable.vue | 791 ++++++++++++++++++
resources/js/pages/forms/show/index.vue | 2 +-
resources/js/pages/pricing.vue | 156 ++++
resources/js/pages/settings/index.vue | 2 +-
resources/js/router/routes.js | 1 +
resources/views/spa.blade.php | 3 +-
routes/api.php | 1 +
27 files changed, 1225 insertions(+), 108 deletions(-)
create mode 100644 app/Http/Requests/Subscriptions/UpdateStripeDetailsRequest.php
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/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php
index 5ffcb2720..f34311979 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 @@ public function checkout($pricing, $plan, $trial = null)
->allowPromotionCodes();
if ($trial != null) {
- $checkoutBuilder->trialDays(3);
+ $checkoutBuilder->trialUntil(now()->addDays(3)->addHour());
}
$checkout = $checkoutBuilder
@@ -49,6 +50,11 @@ public function checkout($pricing, $plan, $trial = null)
->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 @@ public function checkout($pricing, $plan, $trial = null)
]);
}
+ 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');
diff --git a/app/Http/Middleware/Form/ProForm.php b/app/Http/Middleware/Form/ProForm.php
index c92417346..2f4732279 100644
--- a/app/Http/Middleware/Form/ProForm.php
+++ b/app/Http/Middleware/Form/ProForm.php
@@ -18,7 +18,7 @@ class ProForm
public function handle(Request $request, Closure $next)
{
if ($request->route('formId') && $form = Form::findOrFail($request->route('formId'))) {
- if ($form->is_pro) {
+ if (true || $form->is_pro) { // For now it's FREE for all
$request->merge([
'form' => $form,
]);
diff --git a/app/Http/Requests/AnswerFormRequest.php b/app/Http/Requests/AnswerFormRequest.php
index b5bd53f9b..8b59b492c 100644
--- a/app/Http/Requests/AnswerFormRequest.php
+++ b/app/Http/Requests/AnswerFormRequest.php
@@ -14,8 +14,8 @@
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 @@ public function __construct(Request $request)
{
$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 @@ public function rules()
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 @@ public function rules()
}
// 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 @@ private function getPropertyRules($property): array
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 @@ private function getPropertyRules($property): array
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 000000000..1dcbbea59
--- /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/Models/Workspace.php b/app/Models/Workspace.php
index a27977fe1..2262b3851 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 @@ public function getIsProAttribute()
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) {
diff --git a/app/Service/Forms/FormCleaner.php b/app/Service/Forms/FormCleaner.php
index b93bc6026..4739f00d6 100644
--- a/app/Service/Forms/FormCleaner.php
+++ b/app/Service/Forms/FormCleaner.php
@@ -11,7 +11,6 @@
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,33 @@ 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,
];
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.',
// 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 +100,8 @@ public function processRequest(UserFormRequest $request): 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 +116,11 @@ private function isPro(Workspace $workspace) {
* 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 +132,9 @@ public function simulateCleaning(Workspace $workspace): 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 +170,7 @@ private function cleanForm(array &$data, $simulation = false): void
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 +180,15 @@ private function cleanProperties(array &$data, $simulation = false): void
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 +197,18 @@ private function cleanProperties(array &$data, $simulation = false): void
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
diff --git a/resources/js/components/Navbar.vue b/resources/js/components/Navbar.vue
index 0d9640281..73df31fd1 100644
--- a/resources/js/components/Navbar.vue
+++ b/resources/js/components/Navbar.vue
@@ -8,9 +8,7 @@
- {{ appName }} 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,9 @@ export default {
}
return null
},
+ workspace () {
+ return this.$store.getters['open/workspaces/getCurrent']()
+ },
showAuth() {
return this.$route.name && !this.$route.name.startsWith('forms.show_public')
},
diff --git a/resources/js/components/common/ProTag.vue b/resources/js/components/common/ProTag.vue
index c8176f03c..180f5cc3f 100644
--- a/resources/js/components/common/ProTag.vue
+++ b/resources/js/components/common/ProTag.vue
@@ -33,7 +33,7 @@
- Feel free to contact us if you have any feature request.
+ Feel free to contact us if you have any feature request.
@@ -66,13 +66,20 @@ export default {
currentWorkSpace: 'open/workspaces/getCurrent',
}),
shouldDisplayProTag() {
- return false; //!this.user.is_subscribed && !(this.currentWorkSpace.is_pro || this.currentWorkSpace.is_enterprise);
+ if(!window.config.paid_plans_enabled) return false
+ if (!this.user) return true
+ return !(this.currentWorkSpace().is_pro || this.currentWorkSpace().is_enterprise)
},
},
mounted () {
},
- methods: {}
+ methods: {
+ openChat () {
+ window.$crisp.push(['do', 'chat:show'])
+ window.$crisp.push(['do', 'chat:open'])
+ },
+ }
}
diff --git a/resources/js/components/forms/ToggleSwitchInput.vue b/resources/js/components/forms/ToggleSwitchInput.vue
index f1d31442c..073e591ba 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/OpenForm.vue b/resources/js/components/open/forms/OpenForm.vue
index b79c84e68..8b6f10b58 100644
--- a/resources/js/components/open/forms/OpenForm.vue
+++ b/resources/js/components/open/forms/OpenForm.vue
@@ -362,7 +362,7 @@ export default {
const formData = clonedeep(this.dataForm ? this.dataForm.data() : {})
let urlPrefill = null
- if (this.isPublicFormPage && this.form.is_pro) {
+ if (this.isPublicFormPage) {
urlPrefill = new URLSearchParams(window.location.search)
}
@@ -494,8 +494,8 @@ export default {
}
} else if (field.type === 'files' || (field.type === 'url' && field.file_upload)) {
inputProperties.multiple = (field.multiple !== undefined && field.multiple)
- inputProperties.mbLimit = 5
- inputProperties.accept = (this.form.is_pro && field.allowed_file_types) ? field.allowed_file_types : ""
+ inputProperties.mbLimit = this.form.workspace.is_pro ? 50 : 5
+ inputProperties.accept = (field.allowed_file_types) ? field.allowed_file_types : ""
} else if (field.type === 'number' && field.is_rating) {
inputProperties.numberOfStars = parseInt(field.rating_max_value)
} else if (['number', 'phone_number'].includes(field.type)) {
diff --git a/resources/js/components/open/forms/components/FormFieldsEditor.vue b/resources/js/components/open/forms/components/FormFieldsEditor.vue
index eff4da2dd..d865d2efd 100644
--- a/resources/js/components/open/forms/components/FormFieldsEditor.vue
+++ b/resources/js/components/open/forms/components/FormFieldsEditor.vue
@@ -52,11 +52,6 @@
-
-
-
-
-
diff --git a/resources/js/components/open/forms/components/FormStats.vue b/resources/js/components/open/forms/components/FormStats.vue
index 642bdedb0..4f069755e 100644
--- a/resources/js/components/open/forms/components/FormStats.vue
+++ b/resources/js/components/open/forms/components/FormStats.vue
@@ -1,6 +1,6 @@
-
+
@@ -102,7 +102,7 @@ export default {
},
methods: {
getChartData () {
- if (!this.form || !this.form.is_pro) { return null }
+ if (!this.form) { return null }
this.isLoading = true
axios.get('/api/open/workspaces/' + this.form.workspace_id + '/form-stats/' + this.form.id).then((response) => {
const statsData = response.data
diff --git a/resources/js/components/open/forms/components/form-components/FormAboutSubmission.vue b/resources/js/components/open/forms/components/form-components/FormAboutSubmission.vue
index 9bcf7a965..54ea93f47 100644
--- a/resources/js/components/open/forms/components/form-components/FormAboutSubmission.vue
+++ b/resources/js/components/open/forms/components/form-components/FormAboutSubmission.vue
@@ -21,8 +21,13 @@
/>
+ help="Gives user a unique url to update their submission"
+ >
+
+ Editable submissions
+
+
+
-
diff --git a/resources/js/components/open/forms/components/form-components/FormCustomization.vue b/resources/js/components/open/forms/components/form-components/FormCustomization.vue
index 1427f15bb..87feaea86 100644
--- a/resources/js/components/open/forms/components/form-components/FormCustomization.vue
+++ b/resources/js/components/open/forms/components/form-components/FormCustomization.vue
@@ -10,7 +10,6 @@
Customization
-
@@ -62,9 +61,12 @@
-
+
+
+ Remove OpnForm Branding
+
+
+
diff --git a/resources/js/components/open/forms/components/form-components/FormSecurityPrivacy.vue b/resources/js/components/open/forms/components/form-components/FormSecurityPrivacy.vue
index a3dcbfb42..718ab848b 100644
--- a/resources/js/components/open/forms/components/form-components/FormSecurityPrivacy.vue
+++ b/resources/js/components/open/forms/components/form-components/FormSecurityPrivacy.vue
@@ -17,7 +17,6 @@
label="Protect your form with a Captcha"
help="If enabled we will make sure respondant is a human"
/>
-
diff --git a/resources/js/components/open/forms/components/form-logic-components/FormBlockLogicEditor.vue b/resources/js/components/open/forms/components/form-logic-components/FormBlockLogicEditor.vue
index b6d78a635..4346bd3af 100644
--- a/resources/js/components/open/forms/components/form-logic-components/FormBlockLogicEditor.vue
+++ b/resources/js/components/open/forms/components/form-logic-components/FormBlockLogicEditor.vue
@@ -2,7 +2,6 @@
Logic
-
Add some logic to this block. Start by adding some conditions, and then add some actions.
diff --git a/resources/js/components/open/forms/fields/FormFieldOptionsModal.vue b/resources/js/components/open/forms/fields/FormFieldOptionsModal.vue
index 4d34944b7..cc2a3c4c9 100644
--- a/resources/js/components/open/forms/fields/FormFieldOptionsModal.vue
+++ b/resources/js/components/open/forms/fields/FormFieldOptionsModal.vue
@@ -99,7 +99,6 @@
Number Options
-
Date Options
-
Select Options
-
Advanced options for your select/multiselect fields.
@@ -218,7 +215,6 @@
Customization
-
@@ -322,7 +318,6 @@
Advanced Options
-
Url form pre-fill
-
@@ -26,7 +25,6 @@
Url Form Prefill
-
diff --git a/resources/js/components/pages/pricing/CheckoutDetailsModal.vue b/resources/js/components/pages/pricing/CheckoutDetailsModal.vue
new file mode 100644
index 000000000..08197ac7f
--- /dev/null
+++ b/resources/js/components/pages/pricing/CheckoutDetailsModal.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+ Go to checkout
+
+
+
+
+
diff --git a/resources/js/components/pages/pricing/MonthlyYearlySelector.vue b/resources/js/components/pages/pricing/MonthlyYearlySelector.vue
new file mode 100644
index 000000000..9bcfc9ef4
--- /dev/null
+++ b/resources/js/components/pages/pricing/MonthlyYearlySelector.vue
@@ -0,0 +1,41 @@
+
+
+
+
+ Monthly
+
+
+
+
+ Yearly
+
+
+
+ โ Save almost 2 months!
+
+
+
+
+
diff --git a/resources/js/components/pages/pricing/PricingTable.vue b/resources/js/components/pages/pricing/PricingTable.vue
new file mode 100644
index 000000000..5068ef3b0
--- /dev/null
+++ b/resources/js/components/pages/pricing/PricingTable.vue
@@ -0,0 +1,791 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Free
+
+ $0
+
+
+ Per month
+
+
+
+
+ Pro
+
+
+ $24
+
+
+ Per month
+
+
+
+
+ $20
+
+
+ Per month
+
+
+
+
+
+ Enterprise
+
+
+ $59
+
+
+ Per month
+
+
+
+
+ $50
+
+
+ Per month
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ {{ planRow }}
+
+
+
+
+
+
+
+
+ Get Started
+
+
+
+
+
+
+
+ Start Trial
+
+
+
+
+
+
+
+ Start Trial
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ Start Trial
+
+
+
+
+
+
+
+ -
+
+
+
+ Start Trial
+
+
+
+
+
+ Upgrade Now
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Free
+
+ $0
+
+
Per month
+
+
+
+
Pro
+
+
+ $24
+
+ Per month
+
+
+
+ $20
+
+ Per month
+
+
+
+
+
Enterprise
+
+
+ $59
+
+ Per month
+
+
+
+ $50
+
+ Per month
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ {{ planRow }}
+
+
+
+
+
+
+
+
Free
+
+ $0
+
+
Per month
+
+ Get Started
+
+
+
+
+
Pro
+
+
+ $24
+
+ Per month
+
+
+
+ $240
+
+ Per year
+
+
+ Start Trial
+
+
+
+
+
Enterprise
+
+
+ $59
+
+ Per month
+
+
+
+ $599
+
+ Per year
+
+
+ Start Trial
+
+
+
+
+
+
Free
+
+ $0
+
+
Per month
+
+
+
+
+
Pro
+
+
+ $24
+
+ Per month
+
+
+
+ $240
+
+ Per year
+
+
+ Start Trial
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getCurrentFeatureInfo.title }}
+
+
+
+ {{ getCurrentFeatureInfo.description }}
+
+
+
+
+ Close
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/forms/show/index.vue b/resources/js/pages/forms/show/index.vue
index 5d970c84c..1e6da5dc8 100644
--- a/resources/js/pages/forms/show/index.vue
+++ b/resources/js/pages/forms/show/index.vue
@@ -71,7 +71,7 @@
{{tab.name}}
diff --git a/resources/js/pages/pricing.vue b/resources/js/pages/pricing.vue
new file mode 100644
index 000000000..81d83b0b7
--- /dev/null
+++ b/resources/js/pages/pricing.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+ All of our core features are free . You don't need to pay to create awesome
+ OpnForms.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Do
+
+ even more with OpnForms Pro Enterprise
+
+
+ We're happy to have you as a Pro Enterprise
+ subscriber. If you're having any issue with OpnForms, or if you have a
+ feature request, please contact us .
+
+
+
+
+
Create your first Form now
+
+ OpnForms is the best and easiest solution to create forms for Opn users. You don't need take our word
+ for it, just try for free!
+
+
+ Create a Form
+
+
+
+
+ Our different plans
+
+
+ With the free plan you can create as many forms as you need and there is no limit on the number of responses
+ you can receive.
+ The Pro plan offers tons of features to make your forms beautiful and even more powerful. Finally, the
+ Enterprise plan increases the limits
+ of the Pro plan (larger file uploads, unlimited workspaces), allows you to collaborate (unlimited number of
+ users), to your own domain, and comes with
+ priority support.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/settings/index.vue b/resources/js/pages/settings/index.vue
index b1b924f08..b371a513d 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 ac52ef387..d31da7565 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 f9dc12d16..719ae000f 100644
--- a/resources/views/spa.blade.php
+++ b/resources/views/spa.blade.php
@@ -13,7 +13,8 @@
'google_analytics_code' => config('services.google_analytics_code'),
'amplitude_code' => config('services.amplitude_code'),
'crisp_website_id' => config('services.crisp_website_id'),
- 'ai_features_enabled' => !is_null(config('services.openai.api_key'))
+ 'ai_features_enabled' => !is_null(config('services.openai.api_key')),
+ 'paid_plans_enabled' => !is_null(config('cashier.key'))
];
@endphp
diff --git a/routes/api.php b/routes/api.php
index e58856825..7028239d7 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -41,6 +41,7 @@
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).')')
From 3a26786f45695854d228e0206f79a46b5506d920 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Tue, 18 Jul 2023 17:08:28 +0530
Subject: [PATCH 02/16] no pricing page if have no paid plans
---
resources/js/components/Navbar.vue | 5 ++++-
resources/js/pages/pricing.vue | 8 ++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/resources/js/components/Navbar.vue b/resources/js/components/Navbar.vue
index 73df31fd1..d781552a9 100644
--- a/resources/js/components/Navbar.vue
+++ b/resources/js/components/Navbar.vue
@@ -21,7 +21,7 @@
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8">
Templates
-
Upgrade
Pricing
@@ -164,6 +164,9 @@ export default {
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/pages/pricing.vue b/resources/js/pages/pricing.vue
index 81d83b0b7..fd447aa54 100644
--- a/resources/js/pages/pricing.vue
+++ b/resources/js/pages/pricing.vue
@@ -97,6 +97,14 @@ export default {
props: {},
+ beforeRouteEnter (to, from, next) {
+ if (!window.config.paid_plans_enabled) { // If no paid plan so no need this page
+ window.location.href = '/'
+ return false
+ }
+ next()
+ },
+
data: () => ({
metaTitle: 'Pricing',
metaDescription: 'All of our core features are free, and there is no quantity limit. You can also created more advanced and customized forms with OpnForms Pro.',
From 7e04f6b8c7c20e0300d0498d8966c8564dd31ed5 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Tue, 18 Jul 2023 17:20:23 +0530
Subject: [PATCH 03/16] Set pricing ids in env
---
.env.example | 11 ++++++
.../Controllers/SubscriptionController.php | 2 +-
config/pricing.php | 36 ++++---------------
3 files changed, 18 insertions(+), 31 deletions(-)
diff --git a/.env.example b/.env.example
index bcd572688..4fa348396 100644
--- a/.env.example
+++ b/.env.example
@@ -56,6 +56,17 @@ 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=
diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php
index f34311979..e3e3710ab 100644
--- a/app/Http/Controllers/SubscriptionController.php
+++ b/app/Http/Controllers/SubscriptionController.php
@@ -91,7 +91,7 @@ public function billingPortal()
]);
}
- 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/config/pricing.php b/config/pricing.php
index b5b17bf35..d96e405a8 100644
--- a/config/pricing.php
+++ b/config/pricing.php
@@ -4,44 +4,20 @@
'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',
- 'pricing' => [
- 'yearly' => 'price_1KquBXKTHIweYlJTHYU6UXA1',
- 'monthly' => 'price_1KquBXKTHIweYlJTOGEWKr0B',
- 'yearly_2022' => 'price_1LKwvpKTHIweYlJT74vdfJcK',
- 'monthly_2022' => 'price_1LKwvLKTHIweYlJTOAyghKkJ',
- ]
- ],
-
- 'enterprise' => [
- 'product_id' => 'prod_LY0CdM6YtwODqn',
+ 'product_id' => env('STRIPE_TEST_DEFAULT_PRODUCT_ID'),
'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'),
]
]
]
From eee23959ecca8e5489958ba07a21a9f520fa0d41 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Tue, 18 Jul 2023 17:26:50 +0530
Subject: [PATCH 04/16] views & submissions FREE for all
---
app/Http/Resources/FormResource.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/Http/Resources/FormResource.php b/app/Http/Resources/FormResource.php
index 253a9920b..a46ff8a60 100644
--- a/app/Http/Resources/FormResource.php
+++ b/app/Http/Resources/FormResource.php
@@ -26,8 +26,8 @@ public function toArray($request)
$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,
From 4061752ff3ccea8f70185c1e51fca91b4523d666 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Tue, 18 Jul 2023 18:11:23 +0530
Subject: [PATCH 05/16] extra param for env
---
.env.example | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.env.example b/.env.example
index 4fa348396..f781ac18d 100644
--- a/.env.example
+++ b/.env.example
@@ -70,4 +70,7 @@ H_CAPTCHA_SECRET=
MUX_WORKSPACE_ID=
MUX_API_TOKEN=
+ADMIN_EMAILS=
+TEMPLATE_EDITOR_EMAILS=
+
OPEN_AI_API_KEY=
From 5ceb9d08041c04432de6c6f09b1b8a6d9a6ddb94 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Tue, 18 Jul 2023 18:11:48 +0530
Subject: [PATCH 06/16] form password FREE for all
---
app/Http/Middleware/Form/PasswordProtectedForm.php | 2 +-
app/Http/Resources/FormResource.php | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/Http/Middleware/Form/PasswordProtectedForm.php b/app/Http/Middleware/Form/PasswordProtectedForm.php
index 322d7a402..b4e2e15bd 100644
--- a/app/Http/Middleware/Form/PasswordProtectedForm.php
+++ b/app/Http/Middleware/Form/PasswordProtectedForm.php
@@ -26,7 +26,7 @@ public function handle(Request $request, Closure $next)
'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/Resources/FormResource.php b/app/Http/Resources/FormResource.php
index a46ff8a60..990958ad8 100644
--- a/app/Http/Resources/FormResource.php
+++ b/app/Http/Resources/FormResource.php
@@ -95,7 +95,7 @@ public function setCleanings(array $cleanings)
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);
}
From 56cfa9e4d619a520551897105e1d97c04efc77c9 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Tue, 18 Jul 2023 19:04:38 +0530
Subject: [PATCH 07/16] Custom Code is PRO feature
---
app/Service/Forms/FormCleaner.php | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/Service/Forms/FormCleaner.php b/app/Service/Forms/FormCleaner.php
index 4739f00d6..c9c39e0ce 100644
--- a/app/Service/Forms/FormCleaner.php
+++ b/app/Service/Forms/FormCleaner.php
@@ -31,6 +31,7 @@ class FormCleaner
'slack_webhook_url' => null,
'discord_webhook_url' => null,
'editable_submissions' => false,
+ 'custom_code' => null,
];
private array $fieldDefaults = [
@@ -47,11 +48,11 @@ class FormCleaner
'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
'file_upload' => "Link field is not a file upload.",
'custom_block' => 'The custom block was removed.',
- 'files' => 'The file upload file was hidden.',
];
/**
From ff945111268bdcd1958455b52d00615c20bf57b5 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Tue, 18 Jul 2023 19:05:33 +0530
Subject: [PATCH 08/16] Replace codeinput prism with codemirror
---
package.json | 4 +-
resources/js/components/forms/CodeInput.vue | 47 ++++++++++---------
.../form-components/FormCustomCode.vue | 5 +-
resources/js/config/form-themes.js | 6 +--
4 files changed, 30 insertions(+), 32 deletions(-)
diff --git a/package.json b/package.json
index f65c784bc..bc19dbc48 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/forms/CodeInput.vue b/resources/js/components/forms/CodeInput.vue
index 8526df70e..169730e70 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/open/forms/components/form-components/FormCustomCode.vue b/resources/js/components/open/forms/components/form-components/FormCustomCode.vue
index b3132f3f2..4c3cf1aa3 100644
--- a/resources/js/components/open/forms/components/form-components/FormCustomCode.vue
+++ b/resources/js/components/open/forms/components/form-components/FormCustomCode.vue
@@ -12,10 +12,7 @@
- The code will be injected in the head section of your form page. Click
- here to get an example CSS code.
+ The code will be injected in the head section of your form page.
-
-
- 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/components/form-components/FormEditorPreview.vue b/resources/js/components/open/forms/components/form-components/FormEditorPreview.vue
index abd59b7f3..6a9baf18d 100644
--- a/resources/js/components/open/forms/components/form-components/FormEditorPreview.vue
+++ b/resources/js/components/open/forms/components/form-components/FormEditorPreview.vue
@@ -53,6 +53,7 @@
diff --git a/resources/js/components/pages/forms/show/FormCleanings.vue b/resources/js/components/pages/forms/show/FormCleanings.vue
new file mode 100644
index 000000000..3066f4c46
--- /dev/null
+++ b/resources/js/components/pages/forms/show/FormCleanings.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+ Some features that are included in our {{ form.is_pro ? 'Enterprise' : 'Pro' }} plan are disabled when
+ publicly sharing this form (only owners of this form can see this) .
+
+
+
+
+
+
+
+ {{ form.is_pro ? 'Upgrade your OpnForms plan today' : 'Start your free OpnForms trial' }}
+
+ to unlock all of our features and build powerful forms.
+
+
+
+
+ {{ form.is_pro ? 'Upgrade plan' : 'Start free trial' }}
+
+
+
+
+
+ Contact us
+
+
+
+ Hide warning
+
+
+
+
+
+
+
+
diff --git a/resources/js/mixins/forms/saveUpdateAlert.js b/resources/js/mixins/forms/saveUpdateAlert.js
index 9909fda1c..17c6c0f5d 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 1e6da5dc8..482fa45f8 100644
--- a/resources/js/pages/forms/show/index.vue
+++ b/resources/js/pages/forms/show/index.vue
@@ -67,7 +67,9 @@
This form will stop accepting submissions after {{ form.max_submissions_count }} submissions.
-
+
+
+
Date: Wed, 19 Jul 2023 13:15:49 +0530
Subject: [PATCH 10/16] Added risky user email spam protection
---
.../Forms/SubmissionConfirmation.php | 25 ++++++++++++++++++-
app/Models/User.php | 9 +++++++
app/Models/Workspace.php | 22 ++++++++++++++++
3 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/app/Listeners/Forms/SubmissionConfirmation.php b/app/Listeners/Forms/SubmissionConfirmation.php
index 6e96a4c5d..a64145538 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 @@ private function getRespondentEmail(FormSubmitted $event)
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 5b5dba130..ec963b0dc 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -179,6 +179,15 @@ public function getJWTCustomClaims()
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 2262b3851..5974a5937 100644
--- a/app/Models/Workspace.php
+++ b/app/Models/Workspace.php
@@ -51,6 +51,28 @@ public function getIsEnterpriseAttribute()
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
*/
From 7efe3eb85f70feba9b51d2e4daed8a7f8cf1ff01 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Thu, 20 Jul 2023 13:26:47 +0530
Subject: [PATCH 11/16] fix form cleaning
---
app/Http/Controllers/Forms/FormController.php | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/app/Http/Controllers/Forms/FormController.php b/app/Http/Controllers/Forms/FormController.php
index 9233e7246..d8fd25a49 100644
--- a/app/Http/Controllers/Forms/FormController.php
+++ b/app/Http/Controllers/Forms/FormController.php
@@ -123,8 +123,7 @@ public function update(UpdateFormRequest $request, string $id)
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()),
]);
}
From 6e68ba77cb72d1d2771fd43641a8bae9f27cabde Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Thu, 20 Jul 2023 13:28:35 +0530
Subject: [PATCH 12/16] Custom SEO
---
app/Http/Requests/UserFormRequest.php | 3 +
app/Http/Resources/FormResource.php | 3 +-
app/Models/Forms/Form.php | 8 ++-
app/Service/Forms/FormCleaner.php | 30 +++++++++
app/Service/SeoMetaResolver.php | 20 ++++--
database/factories/FormFactory.php | 3 +-
...023_07_20_073728_add_seo_meta_to_forms.php | 32 ++++++++++
.../open/forms/components/FormEditor.vue | 5 +-
.../form-components/FormCustomSeo.vue | 63 +++++++++++++++++++
resources/js/mixins/form_editor/initForm.js | 5 +-
resources/js/mixins/seo-meta.js | 3 +-
resources/js/pages/forms/show-public.vue | 16 +++++
12 files changed, 179 insertions(+), 12 deletions(-)
create mode 100644 database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php
create mode 100644 resources/js/components/open/forms/components/form-components/FormCustomSeo.vue
diff --git a/app/Http/Requests/UserFormRequest.php b/app/Http/Requests/UserFormRequest.php
index f822269f5..a0d777c01 100644
--- a/app/Http/Requests/UserFormRequest.php
+++ b/app/Http/Requests/UserFormRequest.php
@@ -121,6 +121,9 @@ public function rules()
// Security & Privacy
'can_be_indexed' => 'boolean',
'password' => 'sometimes|nullable',
+
+ // Custom SEO
+ 'seo_meta' => 'nullable|array'
];
}
diff --git a/app/Http/Resources/FormResource.php b/app/Http/Resources/FormResource.php
index 3ffb362ca..49857ad75 100644
--- a/app/Http/Resources/FormResource.php
+++ b/app/Http/Resources/FormResource.php
@@ -48,7 +48,8 @@ public function toArray($request)
'slack_webhook_url' => $this->slack_webhook_url,
'discord_webhook_url' => $this->discord_webhook_url,
'removed_properties' => $this->removed_properties,
- 'last_edited_human' => $this->updated_at?->diffForHumans()
+ 'last_edited_human' => $this->updated_at?->diffForHumans(),
+ 'seo_meta' => $this->seo_meta
] : [];
$baseData = $this->getFilteredFormData(parent::toArray($request), $this->userIsFormOwner());
diff --git a/app/Models/Forms/Form.php b/app/Models/Forms/Form.php
index a45752f6d..9f061f444 100644
--- a/app/Models/Forms/Form.php
+++ b/app/Models/Forms/Form.php
@@ -83,7 +83,10 @@ class Form extends Model
// Security & Privacy
'can_be_indexed',
- 'password'
+ 'password',
+
+ // Custom SEO
+ 'seo_meta'
];
protected $casts = [
@@ -91,7 +94,8 @@ class Form extends Model
'database_fields_update' => 'array',
'closes_at' => 'datetime',
'tags' => 'array',
- 'removed_properties' => 'array'
+ 'removed_properties' => 'array',
+ 'seo_meta' => 'object'
];
protected $appends = [
diff --git a/app/Service/Forms/FormCleaner.php b/app/Service/Forms/FormCleaner.php
index c9c39e0ce..71fbdcd46 100644
--- a/app/Service/Forms/FormCleaner.php
+++ b/app/Service/Forms/FormCleaner.php
@@ -23,6 +23,9 @@ class FormCleaner
private array $data;
+ // For remove keys those have empty value
+ private array $customKeys = ['seo_meta'];
+
private array $formDefaults = [
'notifies' => false,
'no_branding' => false,
@@ -32,6 +35,7 @@ class FormCleaner
'discord_webhook_url' => null,
'editable_submissions' => false,
'custom_code' => null,
+ 'seo_meta' => []
];
private array $fieldDefaults = [
@@ -49,6 +53,7 @@ class FormCleaner
'discord_webhook_url' => "Discord webhook disabled.",
'editable_submissions' => 'Users will not be able to edit their submissions.',
'custom_code' => 'Custom code was disabled',
+ 'seo_meta' => 'Custom code was disabled',
// For fields
'file_upload' => "Link field is not a file upload.",
@@ -202,6 +207,9 @@ private function clean(array &$data, array $defaults, $simulation = false): void
// Get value from form
$formVal = Arr::get($data, $key);
+ // Transform customkeys values
+ $formVal = $this->cleanCustomKeys($key, $formVal);
+
// Transform boolean values
$formVal = (($formVal === 0 || $formVal === "0") ? false : $formVal);
$formVal = (($formVal === 1 || $formVal === "1") ? true : $formVal);
@@ -242,4 +250,26 @@ private function cleanField(array &$data, array $defaults, $simulation = false):
}
}
+ // Remove keys those have empty value
+ private function cleanCustomKeys($key, $formVal)
+ {
+ if (in_array($key, $this->customKeys) && $formVal !== null) {
+ $newVal = [];
+ foreach ($formVal as $k => $val) {
+ $changed = false;
+ if ($val) {
+ $newVal[$k] = $val;
+ $changed = true;
+ }
+
+ if ($changed) {
+ $this->cleanings['form'][] = $key;
+ }
+ }
+ return $newVal;
+ }
+
+ return $formVal;
+ }
+
}
diff --git a/app/Service/SeoMetaResolver.php b/app/Service/SeoMetaResolver.php
index 5232562ee..c5ec12e58 100644
--- a/app/Service/SeoMetaResolver.php
+++ b/app/Service/SeoMetaResolver.php
@@ -160,15 +160,25 @@ private function getFormShowMeta(): array
{
$form = Form::whereSlug($this->patternData['slug'])->firstOrFail();
- $meta = [
- 'title' => $form->title . $this->titleSuffix(),
- ];
- if($form->description){
+ $meta = [];
+ if ($form->is_pro && $form->seo_meta->page_title) {
+ $meta['title'] = $form->seo_meta->page_title;
+ } else {
+ $meta['title'] = $form->title . $this->titleSuffix();
+ }
+
+ if ($form->is_pro && $form->seo_meta->page_description) {
+ $meta['description'] = $form->seo_meta->page_description;
+ } else if ($form->description) {
$meta['description'] = Str::of($form->description)->limit(160);
}
- if($form->cover_picture){
+
+ if ($form->is_pro && $form->seo_meta->page_thumbnail) {
+ $meta['image'] = $form->seo_meta->page_thumbnail;
+ } else if ($form->cover_picture) {
$meta['image'] = $form->cover_picture;
}
+
return $meta;
}
diff --git a/database/factories/FormFactory.php b/database/factories/FormFactory.php
index 18a900b2b..c7a831e7f 100644
--- a/database/factories/FormFactory.php
+++ b/database/factories/FormFactory.php
@@ -85,7 +85,8 @@ public function definition()
'tags' => [],
'slack_webhook_url' => null,
'editable_submissions_button_text' => 'Edit submission',
- 'confetti_on_submission' => false
+ 'confetti_on_submission' => false,
+ 'seo_meta' => [],
];
}
diff --git a/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php b/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php
new file mode 100644
index 000000000..e0a67d76b
--- /dev/null
+++ b/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php
@@ -0,0 +1,32 @@
+json('seo_meta')->default('{}');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('forms', function (Blueprint $table) {
+ $table->dropColumn('seo_meta');
+ });
+ }
+};
diff --git a/resources/js/components/open/forms/components/FormEditor.vue b/resources/js/components/open/forms/components/FormEditor.vue
index 3a8f5b289..513c09688 100644
--- a/resources/js/components/open/forms/components/FormEditor.vue
+++ b/resources/js/components/open/forms/components/FormEditor.vue
@@ -37,6 +37,7 @@
+
@@ -66,6 +67,7 @@ import FormNotifications from './form-components/FormNotifications.vue'
import FormIntegrations from './form-components/FormIntegrations.vue'
import FormEditorPreview from './form-components/FormEditorPreview.vue'
import FormSecurityPrivacy from './form-components/FormSecurityPrivacy.vue'
+import FormCustomSeo from './form-components/FormCustomSeo.vue'
import saveUpdateAlert from '../../../../mixins/forms/saveUpdateAlert.js'
export default {
@@ -80,7 +82,8 @@ export default {
FormStructure,
FormInformation,
FormErrorModal,
- FormSecurityPrivacy
+ FormSecurityPrivacy,
+ FormCustomSeo
},
mixins: [saveUpdateAlert],
props: {
diff --git a/resources/js/components/open/forms/components/form-components/FormCustomSeo.vue b/resources/js/components/open/forms/components/form-components/FormCustomSeo.vue
new file mode 100644
index 000000000..fcec4eae6
--- /dev/null
+++ b/resources/js/components/open/forms/components/form-components/FormCustomSeo.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+ Link Settings - SEO
+
+
+
+
+ Customize the image and text that appear when you share your form on other sites (Open Graph).
+
+
+
+
+
+
+
+
diff --git a/resources/js/mixins/form_editor/initForm.js b/resources/js/mixins/form_editor/initForm.js
index 47861286f..cc38ba84d 100644
--- a/resources/js/mixins/form_editor/initForm.js
+++ b/resources/js/mixins/form_editor/initForm.js
@@ -45,7 +45,10 @@ export default {
confetti_on_submission: false,
// Security & Privacy
- can_be_indexed: true
+ can_be_indexed: true,
+
+ // Custom SEO
+ seo_meta: {}
})
},
}
diff --git a/resources/js/mixins/seo-meta.js b/resources/js/mixins/seo-meta.js
index 602059613..2d4044722 100644
--- a/resources/js/mixins/seo-meta.js
+++ b/resources/js/mixins/seo-meta.js
@@ -3,10 +3,11 @@ export default {
const title = this.metaTitle ?? 'OpnForm'
const description = this.metaDescription ?? "Create beautiful forms for free. Unlimited fields, unlimited submissions. It's free and it takes less than 1 minute to create your first form."
const image = this.metaImage ?? this.asset('img/social-preview.jpg')
+ const metaTemplate = this.metaTemplate ?? '%s ยท OpnForm'
return {
title: title,
- titleTemplate: '%s ยท OpnForm',
+ titleTemplate: metaTemplate,
meta: [
...(this.metaTags ?? []),
{ vmid: 'og:title', property: 'og:title', content: title },
diff --git a/resources/js/pages/forms/show-public.vue b/resources/js/pages/forms/show-public.vue
index 39b34c3a3..176c5c4f2 100644
--- a/resources/js/pages/forms/show-public.vue
+++ b/resources/js/pages/forms/show-public.vue
@@ -181,12 +181,28 @@ export default {
return window.location !== window.parent.location || window.frameElement
},
metaTitle () {
+ if(this.form && this.form.is_pro && this.form.seo_meta.page_title) {
+ return this.form.seo_meta.page_title
+ }
return this.form ? this.form.title : 'Create beautiful forms'
},
+ metaTemplate () {
+ if (this.form && this.form.is_pro && this.form.seo_meta.page_title) {
+ // Disable template if custom SEO title
+ return '%s'
+ }
+ return null
+ },
metaDescription () {
+ if (this.form && this.form.is_pro && this.form.seo_meta.page_description) {
+ return this.form.seo_meta.page_description
+ }
return (this.form && this.form.description) ? this.form.description.substring(0, 160) : null
},
metaImage () {
+ if (this.form && this.form.is_pro && this.form.seo_meta.page_thumbnail) {
+ return this.form.seo_meta.page_thumbnail
+ }
return (this.form && this.form.cover_picture) ? this.form.cover_picture : null
},
metaTags () {
From 6a63266bf799f267a79b2ab0964fa8d22c39026d Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Thu, 20 Jul 2023 13:56:09 +0530
Subject: [PATCH 13/16] fix custom seo formcleaner
---
app/Service/Forms/FormCleaner.php | 6 ------
1 file changed, 6 deletions(-)
diff --git a/app/Service/Forms/FormCleaner.php b/app/Service/Forms/FormCleaner.php
index 71fbdcd46..8a76d66ee 100644
--- a/app/Service/Forms/FormCleaner.php
+++ b/app/Service/Forms/FormCleaner.php
@@ -256,14 +256,8 @@ private function cleanCustomKeys($key, $formVal)
if (in_array($key, $this->customKeys) && $formVal !== null) {
$newVal = [];
foreach ($formVal as $k => $val) {
- $changed = false;
if ($val) {
$newVal[$k] = $val;
- $changed = true;
- }
-
- if ($changed) {
- $this->cleanings['form'][] = $key;
}
}
return $newVal;
From 86135d1ba7fc59bb6f75b49778dffb3a269c1000 Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Thu, 20 Jul 2023 19:04:35 +0530
Subject: [PATCH 14/16] Better webhooks
---
app/Listeners/FailedWebhookListener.php | 38 ++++
app/Listeners/Forms/NotifyFormSubmission.php | 185 ++++--------------
app/Listeners/Forms/PostFormDataToWebhook.php | 77 --------
app/Models/Forms/Form.php | 5 +
app/Models/Integration/FormZapierWebhook.php | 15 +-
.../Forms/FailedWebhookNotification.php | 57 ++++++
app/Providers/EventServiceProvider.php | 7 +-
.../Forms/Webhooks/AbstractWebhookHandler.php | 66 +++++++
app/Service/Forms/Webhooks/DiscordHandler.php | 84 ++++++++
.../Forms/Webhooks/SimpleWebhookHandler.php | 24 +++
app/Service/Forms/Webhooks/SlackHandler.php | 75 +++++++
.../Forms/Webhooks/WebhookHandlerProvider.php | 32 +++
app/Service/Forms/Webhooks/ZapierHandler.php | 27 +++
.../views/mail/form/webhook-error.blade.php | 15 ++
14 files changed, 470 insertions(+), 237 deletions(-)
create mode 100644 app/Listeners/FailedWebhookListener.php
delete mode 100644 app/Listeners/Forms/PostFormDataToWebhook.php
create mode 100644 app/Notifications/Forms/FailedWebhookNotification.php
create mode 100644 app/Service/Forms/Webhooks/AbstractWebhookHandler.php
create mode 100644 app/Service/Forms/Webhooks/DiscordHandler.php
create mode 100644 app/Service/Forms/Webhooks/SimpleWebhookHandler.php
create mode 100644 app/Service/Forms/Webhooks/SlackHandler.php
create mode 100644 app/Service/Forms/Webhooks/WebhookHandlerProvider.php
create mode 100644 app/Service/Forms/Webhooks/ZapierHandler.php
create mode 100644 resources/views/mail/form/webhook-error.blade.php
diff --git a/app/Listeners/FailedWebhookListener.php b/app/Listeners/FailedWebhookListener.php
new file mode 100644
index 000000000..c5c6f2028
--- /dev/null
+++ b/app/Listeners/FailedWebhookListener.php
@@ -0,0 +1,38 @@
+meta['type'] == 'form_submission') {
+ $event->meta['form']->creator->notify(new FailedWebhookNotification($event));
+ \Log::error('Failed form submission webhook', [
+ 'webhook_url' => $event->webhookUrl,
+ 'exception' => $event->errorType,
+ 'message' => $event->errorMessage,
+ 'form_id' => $event->meta['form']->id
+ ]);
+ return;
+ }
+
+ \Log::error('Failed webhook', [
+ 'webhook_url' => $event->webhookUrl,
+ 'exception' => $event->errorType,
+ 'message' => $event->errorMessage
+ ]);
+ }
+}
diff --git a/app/Listeners/Forms/NotifyFormSubmission.php b/app/Listeners/Forms/NotifyFormSubmission.php
index b8e59d73f..a2da167a0 100644
--- a/app/Listeners/Forms/NotifyFormSubmission.php
+++ b/app/Listeners/Forms/NotifyFormSubmission.php
@@ -10,8 +10,8 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Notification;
use App\Service\Forms\FormSubmissionFormatter;
+use App\Service\Forms\Webhooks\WebhookHandlerProvider;
use App\Notifications\Forms\FormSubmissionNotification;
-use Vinkla\Hashids\Facades\Hashids;
class NotifyFormSubmission implements ShouldQueue
{
@@ -25,165 +25,46 @@ class NotifyFormSubmission implements ShouldQueue
*/
public function handle(FormSubmitted $event)
{
- if (!$event->form->is_pro) return;
+ $this->sendEmailNotifications($event);
- if ($event->form->notifies) {
- // Send Email Notification
- $subscribers = collect(preg_split("/\r\n|\n|\r/", $event->form->notification_emails))->filter(function($email) {
- return filter_var($email, FILTER_VALIDATE_EMAIL);
- });
- \Log::debug('Sending email notification',[
- 'recipients' => $subscribers->toArray(),
- 'form_id' => $event->form->id,
- 'form_slug' => $event->form->slug,
- ]);
- $subscribers->each(function ($subscriber) use ($event) {
- Notification::route('mail', $subscriber)->notify(new FormSubmissionNotification($event));
- });
- }
-
- if ($event->form->notifies_slack) {
- // Send Slack Notification
- $this->sendSlackNotification($event);
- }
-
- if ($event->form->notifies_discord) {
- // Send Discord Notification
- $this->sendDiscordNotification($event);
+ $this->sendWebhookNotification($event, WebhookHandlerProvider::SIMPLE_WEBHOOK_PROVIDER);
+ $this->sendWebhookNotification($event, WebhookHandlerProvider::SLACK_PROVIDER);
+ $this->sendWebhookNotification($event, WebhookHandlerProvider::DISCORD_PROVIDER);
+ foreach ($event->form->zappierHooks as $hook) {
+ $hook->triggerHook($event->data);
}
-
-
}
-
- private function sendSlackNotification(FormSubmitted $event)
+ private function sendWebhookNotification(FormSubmitted $event, string $provider)
{
- if($this->validateSlackWebhookUrl($event->form->slack_webhook_url)){
- $submissionString = "";
- $formatter = (new FormSubmissionFormatter($event->form, $event->data))->outputStringsOnly();
- foreach ($formatter->getFieldsWithValue() as $field) {
- $tmpVal = is_array($field['value']) ? implode(",", $field['value']) : $field['value'];
- $submissionString .= ">*".ucfirst($field['name'])."*: ".$tmpVal." \n";
- }
-
- $formURL = url("forms/".$event->form->slug);
- $editFormURL = url("forms/".$event->form->slug."/show");
- $submissionId = Hashids::encode($event->data['submission_id']);
- $externalLinks = [
- '*<'.$formURL.'|๐ Open Form>*',
- '*<'.$editFormURL.'|โ๏ธ Edit Form>*'
- ];
- if($event->form->editable_submissions){
- $externalLinks[] = '*<'.$event->form->share_url.'?submission_id='.$submissionId.'|โ๏ธ '.$event->form->editable_submissions_button_text.'>*';
- }
-
- $finalSlackPostData = [
- 'blocks' => [
- [
- 'type' => 'section',
- 'text' => [
- 'type' => 'mrkdwn',
- 'text' => 'New submission for your form *<'.$formURL.'|'.$event->form->title.':>*',
- ]
- ],
- [
- 'type' => 'section',
- 'text' => [
- 'type' => 'mrkdwn',
- 'text' => $submissionString
- ]
- ],
- [
- 'type' => 'section',
- 'text' => [
- 'type' => 'mrkdwn',
- 'text' => implode(' ', $externalLinks),
- ]
- ],
- ]
- ];
-
- WebhookCall::create()
- ->url($event->form->slack_webhook_url)
- ->doNotSign()
- ->payload($finalSlackPostData)
- ->dispatch();
- }
- }
-
- private function validateSlackWebhookUrl($url)
- {
- return ($url) ? str_contains($url, 'https://hooks.slack.com/') : false;
+ WebhookHandlerProvider::getProvider(
+ $event->form,
+ $event->data,
+ $provider
+ )->handle();
}
- private function sendDiscordNotification(FormSubmitted $event)
+ /**
+ * Sends an email to each email address in the form's notification_emails field
+ * @param FormSubmitted $event
+ * @return void
+ */
+ private function sendEmailNotifications(FormSubmitted $event)
{
- if($this->validateDiscordWebhookUrl($event->form->discord_webhook_url)){
- $submissionString = "";
- $formatter = (new FormSubmissionFormatter($event->form, $event->data))->outputStringsOnly();
-
- foreach ($formatter->getFieldsWithValue() as $field) {
- $tmpVal = is_array($field['value']) ? implode(",", $field['value']) : $field['value'];
- $submissionString .= "**".ucfirst($field['name'])."**: `".$tmpVal."`\n";
- }
-
- $form_name = $event->form->title;
- $form = Form::find($event->form->id);
- $formURL = url("forms/".$event->form->slug."/show/submissions");
-
- $finalDiscordPostData = [
- "content" => "@here We have received a new submission for **$form_name**",
- "username" => config('app.name'),
- "avatar_url" => asset('img/logo.png'),
- "tts" => false,
- "embeds" => [
- [
- "title" => "๐ Go to $form_name",
-
- "type" => "rich",
-
- "description" => $submissionString,
-
- "url" => $formURL,
-
- "color" => hexdec(str_replace('#', '', $event->form->color)),
+ if (!$event->form->is_pro || !$event->form->notifies) return;
- "footer" => [
- "text" => config('app.name'),
- "icon_url" => asset('img/logo.png'),
- ],
-
- "author" => [
- "name" => config('app.name'),
- "url" => config('app.url'),
- ],
-
- "fields" => [
- [
- "name" => "Views ๐",
- "value" => "$form->views_count",
- "inline" => true
- ],
- [
- "name" => "Submissions ๐๏ธ",
- "value" => "$form->submissions_count",
- "inline" => true
- ]
- ]
- ]
- ]
- ];
-
- WebhookCall::create()
- ->url($event->form->discord_webhook_url)
- ->doNotSign()
- ->payload($finalDiscordPostData)
- ->dispatch();
- }
- }
-
- private function validateDiscordWebhookUrl($url)
- {
- return ($url) ? str_contains($url, 'https://discord.com/api/webhooks') : false;
+ $subscribers = collect(preg_split("/\r\n|\n|\r/", $event->form->notification_emails))->filter(function (
+ $email
+ ) {
+ return filter_var($email, FILTER_VALIDATE_EMAIL);
+ });
+ \Log::debug('Sending email notification', [
+ 'recipients' => $subscribers->toArray(),
+ 'form_id' => $event->form->id,
+ 'form_slug' => $event->form->slug,
+ ]);
+ $subscribers->each(function ($subscriber) use ($event) {
+ Notification::route('mail', $subscriber)->notify(new FormSubmissionNotification($event));
+ });
}
}
diff --git a/app/Listeners/Forms/PostFormDataToWebhook.php b/app/Listeners/Forms/PostFormDataToWebhook.php
deleted file mode 100644
index 0ebd39539..000000000
--- a/app/Listeners/Forms/PostFormDataToWebhook.php
+++ /dev/null
@@ -1,77 +0,0 @@
-form;
- if (!$form->is_pro) return;
- $data = $this->getWebhookData($event);
-
- $this->sendSimpleWebhook($form, $data);
- $this->sendZappierWebhooks($form, $data);
- }
-
- private function sendSimpleWebhook(Form $form, array $data) {
- if ($form->webhook_url) {
- \Log::debug('Sending data to webhook URL',[
- 'webhook_url' => $form->webhook_url,
- 'form_id' => $form->id,
- 'form_slug' => $form->slug,
- ]);
- WebhookCall::create()
- ->url($form->webhook_url)
- ->doNotSign()
- ->payload($data)
- ->dispatch();
- }
- }
-
- private function sendZappierWebhooks(Form $form, array $data) {
- foreach ($form->zappierHooks as $hook) {
- \Log::debug('Sending data to Zapier webhook',[
- 'form_id' => $form->id,
- 'form_slug' => $form->slug,
- ]);
- $hook->triggerHook($data);
- }
- }
-
- private function getWebhookData(FormSubmitted $event): array {
- $formatter = (new FormSubmissionFormatter($event->form, $event->data))->showHiddenFields();
-
- $formattedData = [];
- foreach ($formatter->getFieldsWithValue() as $field) {
- $formattedData[$field['name']] = $field['value'];
- }
-
- $submissionId = Hashids::encode($event->data['submission_id']);
- $data = [
- 'form_title' => $event->form->title,
- 'form_slug' => $event->form->slug,
- 'submission' => $formattedData
- ];
- if($event->form->editable_submissions){
- $data['edit_link'] = $event->form->share_url.'?submission_id='.$submissionId;
- }
-
- return $data;
- }
-}
diff --git a/app/Models/Forms/Form.php b/app/Models/Forms/Form.php
index 9f061f444..c1458283c 100644
--- a/app/Models/Forms/Form.php
+++ b/app/Models/Forms/Form.php
@@ -140,6 +140,11 @@ public function getShareUrlAttribute()
return url('/forms/'.$this->slug);
}
+ public function getEditUrlAttribute()
+ {
+ return url('/forms/'.$this->slug.'/show');
+ }
+
public function getSubmissionsCountAttribute()
{
return $this->submissions()->count();
diff --git a/app/Models/Integration/FormZapierWebhook.php b/app/Models/Integration/FormZapierWebhook.php
index d8333e9fb..eb157c47e 100644
--- a/app/Models/Integration/FormZapierWebhook.php
+++ b/app/Models/Integration/FormZapierWebhook.php
@@ -3,6 +3,7 @@
namespace App\Models\Integration;
use App\Models\Forms\Form;
+use App\Service\Forms\Webhooks\WebhookHandlerProvider;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -27,11 +28,13 @@ public function form()
return $this->belongsTo(Form::class);
}
- public function triggerHook(array $data) {
- WebhookCall::create()
- ->url($this->hook_url)
- ->doNotSign()
- ->payload($data)
- ->dispatch();
+ public function triggerHook(array $data)
+ {
+ WebhookHandlerProvider::getProvider(
+ $this->form,
+ $data,
+ WebhookHandlerProvider::ZAPIER_PROVIDER,
+ $this->hook_url
+ )->handle();
}
}
diff --git a/app/Notifications/Forms/FailedWebhookNotification.php b/app/Notifications/Forms/FailedWebhookNotification.php
new file mode 100644
index 000000000..230985a07
--- /dev/null
+++ b/app/Notifications/Forms/FailedWebhookNotification.php
@@ -0,0 +1,57 @@
+form = $this->event->meta['form'];
+ $this->provider = $this->event->meta['provider'];
+ }
+
+ /**
+ * Get the notification's delivery channels.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function via($notifiable)
+ {
+ return ['mail'];
+ }
+
+ /**
+ * Get the mail representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return \Illuminate\Notifications\Messages\MailMessage
+ */
+ public function toMail($notifiable)
+ {
+ return (new MailMessage)
+ ->subject("Notification issue with your NotionForm: '" . $this->form->title . "'")
+ ->markdown('mail.form.webhook-error', [
+ 'provider' => $this->provider,
+ 'error' => $this->event->errorMessage,
+ 'form' => $this->form,
+ ]);
+ }
+}
diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php
index af12900b0..164266cdb 100644
--- a/app/Providers/EventServiceProvider.php
+++ b/app/Providers/EventServiceProvider.php
@@ -4,16 +4,17 @@
use App\Events\Forms\FormSubmitted;
use App\Events\Models\FormCreated;
+use App\Listeners\FailedWebhookListener;
use App\Listeners\Auth\RegisteredListener;
use App\Listeners\Forms\FormCreationConfirmation;
use App\Listeners\Forms\NotifyFormSubmission;
-use App\Listeners\Forms\PostFormDataToWebhook;
use App\Listeners\Forms\SubmissionConfirmation;
use App\Notifications\Forms\FormCreatedNotification;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
+use Spatie\WebhookServer\Events\WebhookCallFailedEvent;
class EventServiceProvider extends ServiceProvider
{
@@ -31,8 +32,10 @@ class EventServiceProvider extends ServiceProvider
],
FormSubmitted::class => [
NotifyFormSubmission::class,
- PostFormDataToWebhook::class,
SubmissionConfirmation::class,
+ ],
+ WebhookCallFailedEvent::class => [
+ FailedWebhookListener::class
]
];
diff --git a/app/Service/Forms/Webhooks/AbstractWebhookHandler.php b/app/Service/Forms/Webhooks/AbstractWebhookHandler.php
new file mode 100644
index 000000000..a76d84414
--- /dev/null
+++ b/app/Service/Forms/Webhooks/AbstractWebhookHandler.php
@@ -0,0 +1,66 @@
+form, $this->data))->showHiddenFields();
+
+ $formattedData = [];
+ foreach ($formatter->getFieldsWithValue() as $field) {
+ $formattedData[$field['name']] = $field['value'];
+ }
+
+ $data = [
+ 'form_title' => $this->form->title,
+ 'form_slug' => $this->form->slug,
+ 'submission' => $formattedData,
+ ];
+ if ($this->form->is_pro && $this->form->editable_submissions) {
+ $data['edit_link'] = $this->form->share_url . '?submission_id=' . Hashids::encode($this->data['submission_id']);
+ }
+
+ return $data;
+ }
+
+ abstract protected function shouldRun(): bool;
+
+ public function handle()
+ {
+ if (!$this->shouldRun()) return;
+
+ WebhookCall::create()
+ // Add context on error, used to notify form owner
+ ->meta([
+ 'type' => 'form_submission',
+ 'data' => $this->data,
+ 'form' => $this->form,
+ 'provider' => $this->getProviderName(),
+ ])
+ ->url($this->getWebhookUrl())
+ ->doNotSign()
+ ->payload($this->getWebhookData())
+ ->dispatchSync();
+ }
+}
diff --git a/app/Service/Forms/Webhooks/DiscordHandler.php b/app/Service/Forms/Webhooks/DiscordHandler.php
new file mode 100644
index 000000000..446f21794
--- /dev/null
+++ b/app/Service/Forms/Webhooks/DiscordHandler.php
@@ -0,0 +1,84 @@
+form->discord_webhook_url;
+ }
+
+ protected function getWebhookData(): array
+ {
+ $submissionString = "";
+ $formatter = (new FormSubmissionFormatter($this->form, $this->data))->outputStringsOnly();
+
+ foreach ($formatter->getFieldsWithValue() as $field) {
+ $tmpVal = is_array($field['value']) ? implode(",", $field['value']) : $field['value'];
+ $submissionString .= "**" . ucfirst($field['name']) . "**: `" . $tmpVal . "`\n";
+ }
+
+ $form_name = $this->form->title;
+ $formURL = url("forms/" . $this->form->slug . "/show/submissions");
+
+ return [
+ "content" => "@here We have received a new submission for **$form_name**",
+ "username" => config('app.name'),
+ "avatar_url" => asset('img/logo.png'),
+ "tts" => false,
+ "embeds" => [
+ [
+ "title" => "๐ Go to $form_name",
+
+ "type" => "rich",
+
+ "description" => $submissionString,
+
+ "url" => $formURL,
+
+ "color" => hexdec(str_replace('#', '', $this->form->color)),
+
+ "footer" => [
+ "text" => config('app.name'),
+ "icon_url" => asset('img/logo.png'),
+ ],
+
+ "author" => [
+ "name" => config('app.name'),
+ "url" => config('app.url'),
+ ],
+
+ "fields" => [
+ [
+ "name" => "Views ๐",
+ "value" => (string)$this->form->views_count,
+ "inline" => true
+ ],
+ [
+ "name" => "Submissions ๐๏ธ",
+ "value" => (string)$this->form->submissions_count,
+ "inline" => true
+ ]
+ ]
+ ]
+ ]
+ ];
+ }
+
+ protected function shouldRun(): bool
+ {
+ return !is_null($this->getWebhookUrl())
+ && str_contains($this->getWebhookUrl(), 'https://discord.com/api/webhooks')
+ && $this->form->is_pro;
+ }
+}
diff --git a/app/Service/Forms/Webhooks/SimpleWebhookHandler.php b/app/Service/Forms/Webhooks/SimpleWebhookHandler.php
new file mode 100644
index 000000000..4d3add03b
--- /dev/null
+++ b/app/Service/Forms/Webhooks/SimpleWebhookHandler.php
@@ -0,0 +1,24 @@
+form->webhook_url;
+ }
+
+ protected function shouldRun(): bool
+ {
+ return !is_null($this->getWebhookUrl()) && $this->form->is_pro;
+ }
+}
diff --git a/app/Service/Forms/Webhooks/SlackHandler.php b/app/Service/Forms/Webhooks/SlackHandler.php
new file mode 100644
index 000000000..2ed292db2
--- /dev/null
+++ b/app/Service/Forms/Webhooks/SlackHandler.php
@@ -0,0 +1,75 @@
+form->slack_webhook_url;
+ }
+
+ protected function getWebhookData(): array
+ {
+ $submissionString = '';
+ $formatter = (new FormSubmissionFormatter($this->form, $this->data))->outputStringsOnly();
+ foreach ($formatter->getFieldsWithValue() as $field) {
+ $tmpVal = is_array($field['value']) ? implode(',', $field['value']) : $field['value'];
+ $submissionString .= '>*' . ucfirst($field['name']) . '*: ' . $tmpVal . " \n";
+ }
+
+ $formURL = url('forms/' . $this->form->slug);
+ $editFormURL = url('forms/' . $this->form->slug . '/show');
+ $submissionId = Hashids::encode($this->data['submission_id']);
+ $externalLinks = [
+ '*<' . $formURL . '|๐ Open Form>*',
+ '*<' . $editFormURL . '|โ๏ธ Edit Form>*'
+ ];
+ if ($this->form->editable_submissions) {
+ $externalLinks[] = '*<' . $this->form->share_url . '?submission_id=' . $submissionId . '|โ๏ธ ' . $this->form->editable_submissions_button_text . '>*';
+ }
+
+ return [
+ 'blocks' => [
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => 'New submission for your form *<' . $formURL . '|' . $this->form->title . ':>*',
+ ],
+ ],
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => $submissionString,
+ ],
+ ],
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => implode(' ', $externalLinks),
+ ],
+ ],
+ ],
+ ];
+ }
+
+ protected function shouldRun(): bool
+ {
+ return !is_null($this->getWebhookUrl())
+ && str_contains($this->getWebhookUrl(), 'https://hooks.slack.com/')
+ && $this->form->is_pro;
+ }
+}
diff --git a/app/Service/Forms/Webhooks/WebhookHandlerProvider.php b/app/Service/Forms/Webhooks/WebhookHandlerProvider.php
new file mode 100644
index 000000000..a271aa095
--- /dev/null
+++ b/app/Service/Forms/Webhooks/WebhookHandlerProvider.php
@@ -0,0 +1,32 @@
+webhookUrl;
+ }
+
+ protected function shouldRun(): bool
+ {
+ return !is_null($this->getWebhookUrl());
+ }
+}
diff --git a/resources/views/mail/form/webhook-error.blade.php b/resources/views/mail/form/webhook-error.blade.php
new file mode 100644
index 000000000..4df9a3164
--- /dev/null
+++ b/resources/views/mail/form/webhook-error.blade.php
@@ -0,0 +1,15 @@
+@component('mail::message')
+
+Hello,
+
+We tried to trigger a **{{$provider}}** notification for your form "{{$form->title}}", but it failed. Here is the error that we got:
+
+@component('mail::panel')
+{{$error}}
+@endcomponent
+
+Click [here to edit your form]({{$form->edit_url}}).
+
+Contact us via the website live chat if you need any help.
+
+@endcomponent
From 69c2f21c59650e9ae919abf7a42e1919c527cf3e Mon Sep 17 00:00:00 2001
From: Forms Dev
Date: Wed, 23 Aug 2023 16:37:55 +0530
Subject: [PATCH 15/16] Slack-Discord extra feature
---
app/Http/Requests/UserFormRequest.php | 1 +
app/Http/Resources/FormResource.php | 1 +
app/Models/Forms/Form.php | 4 +-
app/Service/Forms/Webhooks/DiscordHandler.php | 99 ++++++++++---------
app/Service/Forms/Webhooks/SlackHandler.php | 96 +++++++++++-------
database/factories/FormFactory.php | 2 +
...710_add_notification_settings_to_forms.php | 32 ++++++
.../components/FormNotificationsDiscord.vue | 24 +++--
.../FormNotificationsMessageActions.vue | 64 ++++++++++++
.../components/FormNotificationsSlack.vue | 24 +++--
resources/js/mixins/form_editor/initForm.js | 1 +
11 files changed, 245 insertions(+), 103 deletions(-)
create mode 100644 database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php
create mode 100644 resources/js/components/open/forms/components/form-components/components/FormNotificationsMessageActions.vue
diff --git a/app/Http/Requests/UserFormRequest.php b/app/Http/Requests/UserFormRequest.php
index a0d777c01..79d863319 100644
--- a/app/Http/Requests/UserFormRequest.php
+++ b/app/Http/Requests/UserFormRequest.php
@@ -43,6 +43,7 @@ public function rules()
'use_captcha' => 'boolean',
'slack_webhook_url' => 'url|nullable',
'discord_webhook_url' => 'url|nullable',
+ 'notification_settings' => 'nullable',
// Customization
'theme' => ['required',Rule::in(Form::THEMES)],
diff --git a/app/Http/Resources/FormResource.php b/app/Http/Resources/FormResource.php
index 49857ad75..9cdc44c0b 100644
--- a/app/Http/Resources/FormResource.php
+++ b/app/Http/Resources/FormResource.php
@@ -47,6 +47,7 @@ public function toArray($request)
'notification_emails' => $this->notification_emails,
'slack_webhook_url' => $this->slack_webhook_url,
'discord_webhook_url' => $this->discord_webhook_url,
+ 'notification_settings' => $this->notification_settings,
'removed_properties' => $this->removed_properties,
'last_edited_human' => $this->updated_at?->diffForHumans(),
'seo_meta' => $this->seo_meta
diff --git a/app/Models/Forms/Form.php b/app/Models/Forms/Form.php
index c1458283c..7ba3fcc9e 100644
--- a/app/Models/Forms/Form.php
+++ b/app/Models/Forms/Form.php
@@ -41,6 +41,7 @@ class Form extends Model
'notifications_include_submission',
'slack_webhook_url',
'discord_webhook_url',
+ 'notification_settings',
// integrations
'webhook_url',
@@ -95,7 +96,8 @@ class Form extends Model
'closes_at' => 'datetime',
'tags' => 'array',
'removed_properties' => 'array',
- 'seo_meta' => 'object'
+ 'seo_meta' => 'object',
+ 'notification_settings' => 'object'
];
protected $appends = [
diff --git a/app/Service/Forms/Webhooks/DiscordHandler.php b/app/Service/Forms/Webhooks/DiscordHandler.php
index 446f21794..55ebf2d8d 100644
--- a/app/Service/Forms/Webhooks/DiscordHandler.php
+++ b/app/Service/Forms/Webhooks/DiscordHandler.php
@@ -3,7 +3,8 @@
namespace App\Service\Forms\Webhooks;
use App\Service\Forms\FormSubmissionFormatter;
-use Illuminate\Support\Str;
+use Vinkla\Hashids\Facades\Hashids;
+use Illuminate\Support\Arr;
class DiscordHandler extends AbstractWebhookHandler
{
@@ -20,58 +21,60 @@ protected function getWebhookUrl(): ?string
protected function getWebhookData(): array
{
- $submissionString = "";
- $formatter = (new FormSubmissionFormatter($this->form, $this->data))->outputStringsOnly();
+ $settings = (array) Arr::get((array)$this->form->notification_settings, 'discord', []);
+ $externalLinks = [];
+ if(Arr::get($settings, 'link_open_form', true)){
+ $externalLinks[] = '[**๐ Open Form**](' . $this->form->share_url . ')';
+ }
+ if(Arr::get($settings, 'link_edit_form', true)){
+ $editFormURL = url('forms/' . $this->form->slug . '/show');
+ $externalLinks[] = '[**โ๏ธ Edit Form**](' . $editFormURL . ')';
+ }
+ if (Arr::get($settings, 'link_edit_submission', true) && $this->form->editable_submissions) {
+ $submissionId = Hashids::encode($this->data['submission_id']);
+ $externalLinks[] = '[**โ๏ธ ' . $this->form->editable_submissions_button_text . '**](' . $this->form->share_url . '?submission_id=' . $submissionId . ')';
+ }
- foreach ($formatter->getFieldsWithValue() as $field) {
- $tmpVal = is_array($field['value']) ? implode(",", $field['value']) : $field['value'];
- $submissionString .= "**" . ucfirst($field['name']) . "**: `" . $tmpVal . "`\n";
+ $color = hexdec(str_replace('#', '', $this->form->color));
+ $blocks = [];
+ if(Arr::get($settings, 'include_submission_data', true)){
+ $submissionString = "";
+ $formatter = (new FormSubmissionFormatter($this->form, $this->data))->outputStringsOnly();
+ foreach ($formatter->getFieldsWithValue() as $field) {
+ $tmpVal = is_array($field['value']) ? implode(",", $field['value']) : $field['value'];
+ $submissionString .= "**" . ucfirst($field['name']) . "**: " . $tmpVal . "\n";
+ }
+ $blocks[] = [
+ "type" => "rich",
+ "color" => $color,
+ "description" => $submissionString
+ ];
}
- $form_name = $this->form->title;
- $formURL = url("forms/" . $this->form->slug . "/show/submissions");
+ if(Arr::get($settings, 'views_submissions_count', true)){
+ $countString = '**๐ Views**: ' . (string)$this->form->views_count . " \n";
+ $countString .= '**๐๏ธ Submissions**: ' . (string)$this->form->submissions_count;
+ $blocks[] = [
+ "type" => "rich",
+ "color" => $color,
+ "description" => $countString
+ ];
+ }
+ if(count($externalLinks) > 0){
+ $blocks[] = [
+ "type" => "rich",
+ "color" => $color,
+ "description" => implode(' - ', $externalLinks)
+ ];
+ }
+
return [
- "content" => "@here We have received a new submission for **$form_name**",
- "username" => config('app.name'),
- "avatar_url" => asset('img/logo.png'),
- "tts" => false,
- "embeds" => [
- [
- "title" => "๐ Go to $form_name",
-
- "type" => "rich",
-
- "description" => $submissionString,
-
- "url" => $formURL,
-
- "color" => hexdec(str_replace('#', '', $this->form->color)),
-
- "footer" => [
- "text" => config('app.name'),
- "icon_url" => asset('img/logo.png'),
- ],
-
- "author" => [
- "name" => config('app.name'),
- "url" => config('app.url'),
- ],
-
- "fields" => [
- [
- "name" => "Views ๐",
- "value" => (string)$this->form->views_count,
- "inline" => true
- ],
- [
- "name" => "Submissions ๐๏ธ",
- "value" => (string)$this->form->submissions_count,
- "inline" => true
- ]
- ]
- ]
- ]
+ 'content' => 'New submission for your form **' . $this->form->title . '**',
+ 'tts' => false,
+ 'username' => config('app.name'),
+ 'avatar_url' => asset('img/logo.png'),
+ 'embeds' => $blocks
];
}
diff --git a/app/Service/Forms/Webhooks/SlackHandler.php b/app/Service/Forms/Webhooks/SlackHandler.php
index 2ed292db2..5b2faf607 100644
--- a/app/Service/Forms/Webhooks/SlackHandler.php
+++ b/app/Service/Forms/Webhooks/SlackHandler.php
@@ -3,8 +3,8 @@
namespace App\Service\Forms\Webhooks;
use App\Service\Forms\FormSubmissionFormatter;
-use Illuminate\Support\Str;
use Vinkla\Hashids\Facades\Hashids;
+use Illuminate\Support\Arr;
class SlackHandler extends AbstractWebhookHandler
{
@@ -21,48 +21,70 @@ protected function getWebhookUrl(): ?string
protected function getWebhookData(): array
{
- $submissionString = '';
- $formatter = (new FormSubmissionFormatter($this->form, $this->data))->outputStringsOnly();
- foreach ($formatter->getFieldsWithValue() as $field) {
- $tmpVal = is_array($field['value']) ? implode(',', $field['value']) : $field['value'];
- $submissionString .= '>*' . ucfirst($field['name']) . '*: ' . $tmpVal . " \n";
+ $settings = (array) Arr::get((array)$this->form->notification_settings, 'slack', []);
+ $externalLinks = [];
+ if(Arr::get($settings, 'link_open_form', true)){
+ $externalLinks[] = '*<' . $this->form->share_url . '|๐ Open Form>*';
+ }
+ if(Arr::get($settings, 'link_edit_form', true)){
+ $editFormURL = url('forms/' . $this->form->slug . '/show');
+ $externalLinks[] = '*<' . $editFormURL . '|โ๏ธ Edit Form>*';
+ }
+ if (Arr::get($settings, 'link_edit_submission', true) && $this->form->editable_submissions) {
+ $submissionId = Hashids::encode($this->data['submission_id']);
+ $externalLinks[] = '*<' . $this->form->share_url . '?submission_id=' . $submissionId . '|โ๏ธ ' . $this->form->editable_submissions_button_text . '>*';
}
- $formURL = url('forms/' . $this->form->slug);
- $editFormURL = url('forms/' . $this->form->slug . '/show');
- $submissionId = Hashids::encode($this->data['submission_id']);
- $externalLinks = [
- '*<' . $formURL . '|๐ Open Form>*',
- '*<' . $editFormURL . '|โ๏ธ Edit Form>*'
+ $blocks = [
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => 'New submission for your form *' . $this->form->title . '*',
+ ]
+ ]
];
- if ($this->form->editable_submissions) {
- $externalLinks[] = '*<' . $this->form->share_url . '?submission_id=' . $submissionId . '|โ๏ธ ' . $this->form->editable_submissions_button_text . '>*';
+
+ if(Arr::get($settings, 'include_submission_data', true)){
+ $submissionString = '';
+ $formatter = (new FormSubmissionFormatter($this->form, $this->data))->outputStringsOnly();
+ foreach ($formatter->getFieldsWithValue() as $field) {
+ $tmpVal = is_array($field['value']) ? implode(',', $field['value']) : $field['value'];
+ $submissionString .= '>*' . ucfirst($field['name']) . '*: ' . $tmpVal . " \n";
+ }
+ $blocks[] = [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => $submissionString,
+ ]
+ ];
+ }
+
+ if(Arr::get($settings, 'views_submissions_count', true)){
+ $countString = '*๐ Views*: ' . (string)$this->form->views_count . " \n";
+ $countString .= '*๐๏ธ Submissions*: ' . (string)$this->form->submissions_count;
+ $blocks[] = [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => $countString,
+ ]
+ ];
+ }
+
+ if(count($externalLinks) > 0){
+ $blocks[] = [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => implode(' ', $externalLinks),
+ ]
+ ];
}
return [
- 'blocks' => [
- [
- 'type' => 'section',
- 'text' => [
- 'type' => 'mrkdwn',
- 'text' => 'New submission for your form *<' . $formURL . '|' . $this->form->title . ':>*',
- ],
- ],
- [
- 'type' => 'section',
- 'text' => [
- 'type' => 'mrkdwn',
- 'text' => $submissionString,
- ],
- ],
- [
- 'type' => 'section',
- 'text' => [
- 'type' => 'mrkdwn',
- 'text' => implode(' ', $externalLinks),
- ],
- ],
- ],
+ 'blocks' => $blocks
];
}
diff --git a/database/factories/FormFactory.php b/database/factories/FormFactory.php
index c7a831e7f..dab01d3e7 100644
--- a/database/factories/FormFactory.php
+++ b/database/factories/FormFactory.php
@@ -84,6 +84,8 @@ public function definition()
'password' => false,
'tags' => [],
'slack_webhook_url' => null,
+ 'discord_webhook_url' => null,
+ 'notification_settings' => [],
'editable_submissions_button_text' => 'Edit submission',
'confetti_on_submission' => false,
'seo_meta' => [],
diff --git a/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php b/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php
new file mode 100644
index 000000000..b091f6b7f
--- /dev/null
+++ b/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php
@@ -0,0 +1,32 @@
+json('notification_settings')->default('{}')->nullable(true);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('forms', function (Blueprint $table) {
+ $table->dropColumn('notification_settings');
+ });
+ }
+};
diff --git a/resources/js/components/open/forms/components/form-components/components/FormNotificationsDiscord.vue b/resources/js/components/open/forms/components/form-components/components/FormNotificationsDiscord.vue
index f8dc0de62..3ebca14b7 100644
--- a/resources/js/components/open/forms/components/form-components/components/FormNotificationsDiscord.vue
+++ b/resources/js/components/open/forms/components/form-components/components/FormNotificationsDiscord.vue
@@ -28,23 +28,29 @@
-
-
- Receive a discord message on each form submission.
- Click here to learn how to get a discord webhook url.
-
-
+
+
+
+ Receive a discord message on each form submission.
+ Click
+ here to learn how to get a discord webhook url.
+
+
+ Discord message actions
+
+
diff --git a/resources/js/components/open/forms/components/form-components/components/FormNotificationsSlack.vue b/resources/js/components/open/forms/components/form-components/components/FormNotificationsSlack.vue
index 2e7cb7edc..0de50b8be 100644
--- a/resources/js/components/open/forms/components/form-components/components/FormNotificationsSlack.vue
+++ b/resources/js/components/open/forms/components/form-components/components/FormNotificationsSlack.vue
@@ -28,22 +28,30 @@
-
-
- Receive slack message on each form submission. Click here to learn how to get a slack webhook url
-
-
+
+
+
+ Receive slack message on each form submission. Click here to learn how to get a slack
+ webhook url
+
+
+ Slack message actions
+
+