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:
@@ -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()),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user