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 <julien@nahum.net>
This commit is contained in:
formsdev
2023-08-30 13:28:29 +05:30
committed by GitHub
parent 29b153bd76
commit fb79a5bf3e
48 changed files with 1011 additions and 269 deletions

View File

@@ -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()),
]);
}

View File

@@ -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();

View File

@@ -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)

View File

@@ -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');
}

View File

@@ -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);
}

View File

@@ -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)];

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Subscriptions;
use Illuminate\Foundation\Http\FormRequest;
class UpdateStripeDetailsRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'name' => 'required|string',
'email' => 'required|email',
];
}
}

View File

@@ -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;
}
}