PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.17.0
GiveWP – Donation Plugin and Fundraising Platform v4.17.0
4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 All 256 releases
give / src / DonationForms / ViewModels / DonationFormViewModel.php

DonationFormViewModel.php in GiveWP – Donation Plugin and Fundraising Platform 4.17.0, at src/DonationForms/ViewModels/DonationFormViewModel.php

575 lines 18.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\DonationForms\ViewModels;
4
5 use Give\Campaigns\Models\Campaign;
6 use Give\Campaigns\ValueObjects\CampaignGoalType;
7 use Give\DonationForms\Actions\GenerateAuthUrl;
8 use Give\DonationForms\Actions\GenerateDonateRouteUrl;
9 use Give\DonationForms\Actions\GenerateDonationFormValidationRouteUrl;
10 use Give\DonationForms\Actions\GetFormSkeletonData;
11 use Give\DonationForms\Actions\IsolateEnqueuedFormViewAssets;
12 use Give\DonationForms\Actions\RenderFormSkeleton;
13 use Give\DonationForms\DataTransferObjects\DonationFormGoalData;
14 use Give\DonationForms\Models\DonationForm;
15 use Give\DonationForms\Properties\FormSettings;
16 use Give\DonationForms\Repositories\DonationFormRepository;
17 use Give\DonationForms\ValueObjects\GoalType;
18 use Give\Framework\Blocks\BlockCollection;
19 use Give\Framework\DesignSystem\Actions\RegisterDesignSystemStyles;
20 use Give\Framework\FormDesigns\FormDesign;
21 use Give\Framework\FormDesigns\Registrars\FormDesignRegistrar;
22 use Give\Framework\Support\Scripts\Concerns\HasScriptAssetFile;
23 use Give\Helpers\Hooks;
24 use Give\Helpers\Language;
25
26 /**
27 * @since 3.0.0
28 */
29 class DonationFormViewModel
30 {
31 use HasScriptAssetFile;
32
33 /**
34 * @var int
35 */
36 private $donationFormId;
37 /**
38 * @var BlockCollection
39 */
40 private $formBlocks;
41 /**
42 *
43 * @var FormSettings
44 */
45 private $formSettings;
46 /**
47 * @var DonationFormRepository
48 */
49 private $donationFormRepository;
50 /**
51 * @var bool
52 */
53 private $previewMode;
54
55 /**
56 * The skeleton markup for this render, or empty when there is none to show.
57 *
58 * @since 4.17.0
59 *
60 * @var string
61 */
62 private $skeleton = '';
63 /**
64 * @since 4.1.0
65 */
66 private DonationFormGoalData $donationFormGoalData;
67
68 /**
69 * @since 3.0.0
70 */
71 public function __construct(
72 int $donationFormId,
73 BlockCollection $formBlocks,
74 FormSettings $formSettings,
75 bool $previewMode = false
76 ) {
77 $this->donationFormId = $donationFormId;
78 $this->formBlocks = $formBlocks;
79 $this->formSettings = $formSettings;
80 $this->donationFormRepository = give(DonationFormRepository::class);
81 $this->previewMode = $previewMode;
82 $this->donationFormGoalData = new DonationFormGoalData($donationFormId, $formSettings);
83 }
84
85 /**
86 * @since 3.0.0
87 */
88 public function designId(): string
89 {
90 return $this->formSettings->designId ?? '';
91 }
92
93 /**
94 * @since 4.14.6 Sanitize output; campaign colors bypass FormSettings::fromArray.
95 * @since 4.1.0 Add support for campaign colors
96 * @since 3.0.0
97 */
98 public function primaryColor(): string
99 {
100 $color = $this->formSettings->primaryColor ?? '';
101
102 if ($this->formSettings->inheritCampaignColors) {
103 $campaignColors = $this->getCampaignColors($this->donationFormId);
104
105 if ($campaignColors['primaryColor']) {
106 $color = $campaignColors['primaryColor'];
107 }
108 }
109
110 return sanitize_hex_color($color) ?? '';
111 }
112
113 /**
114 * @since 4.14.6 Sanitize output; campaign colors bypass FormSettings::fromArray.
115 * @since 4.1.0 Add support for campaign colors
116 * @since 3.0.0
117 */
118 public function secondaryColor(): string
119 {
120 $color = $this->formSettings->secondaryColor ?? '';
121
122 if ($this->formSettings->inheritCampaignColors) {
123 $campaignColors = $this->getCampaignColors($this->donationFormId);
124
125 if ($campaignColors['secondaryColor']) {
126 $color = $campaignColors['secondaryColor'];
127 }
128 }
129
130 return sanitize_hex_color($color) ?? '';
131 }
132
133 /**
134 * @since 4.1.0 Added custom form styles
135 * @since 3.0.0
136 */
137 public function enqueueGlobalStyles()
138 {
139 (new RegisterDesignSystemStyles())();
140 wp_enqueue_style('givewp-design-system-foundation');
141
142 wp_register_style(
143 'givewp-base-form-styles',
144 GIVE_PLUGIN_URL . 'build/baseFormDesignCss.css'
145 );
146
147 wp_add_inline_style(
148 'givewp-base-form-styles',
149 ":root {
150 --givewp-primary-color:{$this->primaryColor()};
151 --givewp-secondary-color:{$this->secondaryColor()};
152 }"
153 );
154
155 wp_add_inline_style(
156 'givewp-base-form-styles',
157 wp_strip_all_tags(give_get_option('custom_form_styles', ''))
158 );
159
160 wp_enqueue_style('givewp-base-form-styles');
161
162 // The skeleton prints in the body before the app bundles load, so its styles go in the head with the rest.
163 if ($this->skeleton) {
164 wp_register_style('givewp-form-skeleton-styles', false);
165 wp_add_inline_style('givewp-form-skeleton-styles', (new RenderFormSkeleton())->css());
166 wp_enqueue_style('givewp-form-skeleton-styles');
167 }
168 }
169
170 /**
171 * @since 3.0.0
172 *
173 * @return GoalType|CampaignGoalType
174 */
175 private function goalType()
176 {
177 if ($this->formSettings->goalSource->isCampaign()) {
178 $campaign = Campaign::findByFormId($this->donationFormId);
179
180 if ($campaign) {
181 return $campaign->goalType;
182 }
183 }
184
185 return $this->formSettings->goalType;
186 }
187
188 /**
189 * @since 4.2.0 always return count value
190 * @since 4.1.0 use DonationFormGoalData
191 * @since 3.0.0
192 */
193 private function getTotalCountValue(): ?int
194 {
195 switch ($this->goalType()->getValue()):
196 case 'donors':
197 case 'donorsFromSubscriptions':
198 return $this->donationFormGoalData->getQuery()->countDonors();
199 case 'donations':
200 return $this->formSettings->goalSource->isCampaign()
201 ? $this->donationFormGoalData->getQuery()->countDonations()
202 : $this->donationFormGoalData->getQuery()->count();
203 default:
204 return $this->donationFormGoalData->getQuery()->count();
205 endswitch;
206 }
207
208 /**
209 * @since 3.0.0
210 */
211 public function getCountLabel(): ?string
212 {
213 if ($this->goalType()->isDonors() || $this->goalType()->isDonorsFromSubscriptions()) {
214 return __('Donors', 'give');
215 }
216
217 if ($this->goalType()->isDonations() || $this->goalType()->isAmount()) {
218 return __('Donations', 'give');
219 }
220
221 if ($this->goalType()->isSubscriptions() || $this->goalType()->isAmountFromSubscriptions()) {
222 return __('Recurring Donations', 'give');
223 }
224
225 return __('Counted', 'give');
226 }
227
228 /**
229 * @since 4.3.0 ensure totalRevenue always reflects revenue
230 * @since 4.1.0 use DonationFormGoalData instead of repository
231 * @since 3.0.0
232 */
233 private function formStatsData(): array
234 {
235 $query = $this->donationFormGoalData->getQuery();
236
237 // Only form goal has range
238 if ($this->formSettings->goalSource->isForm() && $this->formSettings->goalProgressType->isCustom()) {
239 $query->between($this->formSettings->goalStartDate, $this->formSettings->goalEndDate);
240 }
241
242 return [
243 'totalRevenue' => $this->donationFormGoalData->getTotalDonationRevenue(),
244 'totalCountValue' => $this->goalType()->isDonations() || $this->goalType()->isAmount()
245 ? $query->count()
246 : $this->getTotalCountValue(),
247 'totalCountLabel' => $this->getCountLabel(),
248 ];
249 }
250
251 /**
252 * @since 3.6.0 added includeHeaderInMultiStep to form design export
253 * @since 3.0.0
254 */
255 public function exports(): array
256 {
257 $donateUrl = (new GenerateDonateRouteUrl())();
258 $validateUrl = (new GenerateDonationFormValidationRouteUrl())();
259 $authUrl = (new GenerateAuthUrl())();
260
261 $formDataGateways = $this->donationFormRepository->getFormDataGateways($this->donationFormId);
262 $formApi = $this->donationFormRepository->getFormSchemaFromBlocks(
263 $this->donationFormId,
264 $this->formBlocks
265 );
266
267 $formDesign = $this->getFormDesign($this->designId());
268
269 return [
270 'donateUrl' => $donateUrl,
271 'validateUrl' => $validateUrl,
272 'authUrl' => $authUrl,
273 'inlineRedirectRoutes' => [
274 'donation-confirmation-receipt-view',
275 ],
276 'registeredGateways' => $formDataGateways,
277 'form' => array_merge($formApi->jsonSerialize(), [
278 'settings' => $this->formSettings,
279 'currency' => $formApi->getDefaultCurrency(),
280 'goal' => $this->previewMode || ($this->formSettings->showHeader && $this->formSettings->enableDonationGoal)
281 ? $this->donationFormGoalData->toArray()
282 : [],
283 'stats' => $this->previewMode || ($this->formSettings->showHeader && $this->formSettings->enableDonationGoal)
284 ? $this->formStatsData()
285 : [],
286 'design' => $formDesign ? [
287 'id' => $formDesign::id(),
288 'name' => $formDesign::name(),
289 'isMultiStep' => $formDesign->isMultiStep(),
290 'includeHeaderInMultiStep' => $formDesign->shouldIncludeHeaderInMultiStep(),
291 ] : null,
292 ]),
293 'previewMode' => $this->previewMode,
294 ];
295 }
296
297 /**
298 * @since 4.17.0 print the form's skeleton inside the root and announce the shell to the embedding page.
299 * This is the order of loading:
300 * 1. Enqueue global styles from WP.
301 * - This ensures template compatability with global WP css variables as needed. Loads before our templates, so they can use things like global font-family, etc.
302 * 2. Enqueue our donation form specific scripts & styles.
303 * - We will let WP handle the actual printing depending on how they were enqueued.
304 * 3. Pin the script and style queues to what we enqueued.
305 * - The print functions below fire actions that other plugins enqueue from, so the queues
306 * have to be constrained before printing rather than trusted.
307 * 4. Call the specific WP functions wp_print_styles() and wp_print_head_scripts()
308 * - This will only print the styles and scripts that are enqueued within our route - so we don't have to dequeue a bunch of stuff.
309 * 5. Manually echo our window data and root div for our React app to consume
310 * 6. Finally, call the specific WP function wp_print_footer_scripts()
311 * - This will only print the footer scripts that are enqueued within our route.
312 *
313 * @since 4.16.7 Isolate the printed assets from anything enqueued after the form has been prepared
314 * @since 4.14.3 Escape HTML attributes for classNames property
315 * @since 3.20.0 Adds class for form design
316 * @since 3.11.0 Sanitize customCSS property
317 * @since 3.0.0
318 */
319 public function render(): string
320 {
321 $this->skeleton = $this->previewMode ? '' : $this->renderSkeleton();
322
323 $this->enqueueGlobalStyles();
324
325 $this->enqueueFormScripts(
326 $this->donationFormId,
327 $this->designId()
328 );
329
330 (new IsolateEnqueuedFormViewAssets())();
331
332 ob_start();
333 wp_print_styles();
334 wp_print_head_scripts();
335 ?>
336
337 <?php
338 if ($this->previewMode || $this->formSettings->customCss): ?>
339 <style id="root-givewp-donation-form-style"><?php
340 echo wp_strip_all_tags($this->formSettings->customCss); ?></style>
341 <?php
342 endif; ?>
343
344 <?php
345 $classNames = ['givewp-donation-form', "givewp-donation-form-design--{$this->designId()}"];
346
347 if ($this->previewMode) {
348 $classNames[] = 'givewp-donation-form--preview';
349 }
350 ?>
351
352 <div data-theme="light" id="root-givewp-donation-form"
353 data-iframe-height
354 class="<?= esc_attr(implode(' ', $classNames)) ?>"><?= $this->skeleton ?></div>
355
356 <?php if ($this->skeleton): ?>
357 <script>parent.postMessage({type: 'givewp-embed-shell', height: document.documentElement.scrollHeight}, '*');</script>
358 <?php endif; ?>
359
360 <?php
361 wp_print_footer_scripts();
362
363 echo ob_get_clean();
364
365 exit();
366 }
367
368 /**
369 * The skeleton RenderFormSkeleton draws, printed inside the root so the iframe shows the form's
370 * shape as soon as its HTML and head styles arrive, before the app bundles load. createRoot()
371 * replaces it with the form. The inline script that follows the root tells the embedding page
372 * the shell has painted and how tall it is; the payload is that height and nothing else, so it
373 * is addressed to any origin and the embed authenticates the message by its origin and source
374 * instead. Both are skipped for an unknown design, where there is nothing to show, and in the
375 * builder preview.
376 *
377 * @since 4.17.0
378 */
379 private function renderSkeleton(): string
380 {
381 $form = DonationForm::find($this->donationFormId);
382
383 if (!$form) {
384 return '';
385 }
386
387 return (new RenderFormSkeleton())((new GetFormSkeletonData())($form));
388 }
389
390 /**
391 * Loads scripts in order: [Registrars, Designs, Gateways, Block]
392 *
393 * @since 3.0.0
394 *
395 * @return void
396 */
397 private function enqueueFormScripts(int $formId, string $formDesignId)
398 {
399 $this->enqueueRegistrars();
400 $this->enqueueDesign($formDesignId);
401 $this->enqueueGateways($formId);
402 $this->enqueueFormApp();
403 }
404
405 /**
406 * @since 3.0.0
407 *
408 * @return FormDesign|null
409 */
410 protected function getFormDesign(string $designId)
411 {
412 /** @var FormDesignRegistrar $formDesignRegistrar */
413 $formDesignRegistrar = give(FormDesignRegistrar::class);
414
415 return $formDesignRegistrar->hasDesign($this->designId()) ? $formDesignRegistrar->getDesign($designId) : null;
416 }
417
418 /**
419 * @since 3.0.0
420 */
421 private function enqueueRegistrars()
422 {
423 wp_enqueue_style(
424 'givewp-donation-form-registrars',
425 GIVE_PLUGIN_URL . 'build/donationFormRegistrars.css',
426 [],
427 GIVE_VERSION
428 );
429
430 $handle = 'givewp-donation-form-registrars';
431 wp_enqueue_script(
432 $handle,
433 GIVE_PLUGIN_URL . 'build/donationFormRegistrars.js',
434 $this->getScriptAssetDependencies(GIVE_PLUGIN_DIR . 'build/donationFormRegistrars.asset.php'),
435 GIVE_VERSION,
436 true
437 );
438
439 Language::setScriptTranslations($handle);
440
441 wp_add_inline_script(
442 'givewp-donation-form-registrars',
443 'window.givewpDonationFormExports = ' . wp_json_encode($this->exports()) . ';',
444 'before'
445 );
446
447 Hooks::doAction('givewp_donation_form_enqueue_scripts');
448 }
449
450 /**
451 * @since 3.0.0
452 */
453 private function enqueueGateways(int $formId)
454 {
455 /** @var DonationFormRepository $donationFormRepository */
456 $donationFormRepository = give(DonationFormRepository::class);
457
458 // load gateway scripts
459 foreach ($donationFormRepository->getEnabledPaymentGateways($formId) as $gateway) {
460 if (method_exists($gateway, 'enqueueScript')) {
461 $gateway->enqueueScript($formId);
462 }
463 }
464 }
465
466 /**
467 * @since 3.0.0 Set script translations
468 * @since 3.0.0
469 */
470 private function enqueueDesign(string $formDesignId)
471 {
472 $design = $this->getFormDesign($formDesignId);
473
474 // silently fail if design is missing for some reason
475 if ($design) {
476 if ($design->css()) {
477 wp_enqueue_style('givewp-form-design-' . $design::id(), $design->css());
478 }
479
480 if ($design->js()) {
481 $handle = 'givewp-form-design-' . $design::id();
482 wp_enqueue_script(
483 $handle,
484 $design->js(),
485 array_merge(
486 $design->dependencies(),
487 ['givewp-donation-form-registrars']
488 ),
489 true
490 );
491
492 Language::setScriptTranslations($handle);
493 }
494 }
495 }
496
497 /**
498 * @since 3.0.0
499 */
500 private function enqueueFormApp()
501 {
502 // load block - since this is using render_callback viewScript in blocks.json will not work.
503 $handle = 'givewp-donation-form-app';
504 wp_enqueue_script(
505 $handle,
506 GIVE_PLUGIN_URL . 'build/donationFormApp.js',
507 array_merge(
508 $this->getScriptAssetDependencies(GIVE_PLUGIN_DIR . 'build/donationFormApp.asset.php'),
509 ['givewp-donation-form-registrars']
510 ),
511 GIVE_VERSION,
512 true
513 );
514
515 Language::setScriptTranslations($handle);
516
517 /**
518 * Load iframeResizer.contentWindow.min.js inside iframe
519 *
520 * @see https://github.com/davidjbradshaw/iframe-resizer
521 */
522 $handle = 'givewp-donation-form-embed-inside';
523 wp_enqueue_script(
524 $handle,
525 GIVE_PLUGIN_URL . 'build/donationFormEmbedInside.js',
526 [],
527 GIVE_VERSION,
528 true
529 );
530
531 Language::setScriptTranslations($handle);
532 }
533
534 /**
535 * @since 3.4.0
536 */
537 private function updateDesignSettingsClassNames(array &$classNames)
538 {
539 if ($this->formSettings->designSettingsImageUrl) {
540 $classNames[] = 'givewp-design-settings--image';
541 $classNames[] = 'givewp-design-settings--image-style__' . $this->formSettings->designSettingsImageStyle;
542 }
543
544 if ($this->formSettings->designSettingsLogoUrl) {
545 $classNames[] = 'givewp-design-settings--logo';
546 $classNames[] = 'givewp-design-settings--logo-position__' . $this->formSettings->designSettingsLogoPosition;
547 }
548
549 $classNames[] = 'givewp-design-settings--section-style__' . $this->formSettings->designSettingsSectionStyle;
550
551 $classNames[] = 'givewp-design-settings--textField-style__' . $this->formSettings->designSettingsTextFieldStyle;
552 }
553
554 /**
555 * @since 4.1.0
556 */
557 private function getCampaignColors(int $formId): array
558 {
559 /** @var Campaign $campaign */
560 $campaign = give()->campaigns->getByFormId($formId);
561
562 if ($campaign) {
563 return [
564 'primaryColor' => $campaign->primaryColor,
565 'secondaryColor' => $campaign->secondaryColor,
566 ];
567 }
568
569 return [
570 'primaryColor' => '',
571 'secondaryColor' => '',
572 ];
573 }
574 }
575