<?php
namespace Give\DonationForms\Actions;
use Exception;
use Give\DonationForms\Repositories\DonationFormRepository;
use Give\DonationForms\Rules\AuthenticationRule;
use Give\DonationForms\Rules\BillingAddressCityRule;
use Give\DonationForms\Rules\BillingAddressStateRule;
use Give\DonationForms\Rules\BillingAddressZipRule;
use Give\DonationForms\Rules\GatewayRule;
use Give\DonationForms\Rules\PhoneIntlInputRule;
use Give\FormBuilder\BlockModels\DonationAmountBlockModel;
use Give\FormBuilder\BlockTypes\TextBlockType;
use Give\Framework\Blocks\BlockCollection;
use Give\Framework\Blocks\BlockModel;
use Give\Framework\FieldsAPI\Authentication;
use Give\Framework\FieldsAPI\BillingAddress;
use Give\Framework\FieldsAPI\Checkbox;
use Give\Framework\FieldsAPI\Contracts\Node;
use Give\Framework\FieldsAPI\DonationForm;
use Give\Framework\FieldsAPI\DonationSummary;
use Give\Framework\FieldsAPI\Email;
use Give\Framework\FieldsAPI\Exceptions\EmptyNameException;
use Give\Framework\FieldsAPI\Exceptions\NameCollisionException;
use Give\Framework\FieldsAPI\Exceptions\TypeNotSupported;
use Give\Framework\FieldsAPI\Field;
use Give\Framework\FieldsAPI\Name;
use Give\Framework\FieldsAPI\Paragraph;
use Give\Framework\FieldsAPI\PaymentGateways;
use Give\Framework\FieldsAPI\Phone;
use Give\Framework\FieldsAPI\Section;
use Give\Framework\FieldsAPI\Text;
use Give\Framework\FieldsAPI\Textarea;
use Give\Helpers\IntlTelInput;
use WP_User;
/**
* @since 3.0.0
*/
class ConvertDonationFormBlocksToFieldsApi
{
/**
* @var int
*/
protected $formId;
/**
* @var string
*/
protected $currency;
/**
* @var array{blockClientId: {node: Node, block: BlockModel}}
*/
protected $blockNodeRelationships = [];
/**
* @since 3.0.0
*
* @return array{form: DonationForm, array{clientId: string{node: Node, block: BlockModel}}}
* @throws TypeNotSupported|NameCollisionException
*/
public function __invoke(BlockCollection $blocks, int $formId): array
{
$this->formId = $formId;
$this->currency = give_get_currency($formId);
$form = new DonationForm('donation-form');
$form->defaultCurrency($this->currency);
$blockIndex = 0;
foreach ($blocks->getBlocks() as $block) {
$blockIndex++;
$section = $this->convertTopLevelBlockToSection($block, $blockIndex);
if ($block->innerBlocks) {
$innerBlocks = $block->innerBlocks->getBlocks();
$nodes = array_filter(
array_map([$this, 'convertInnerBlockToNode'], $innerBlocks, array_keys($innerBlocks)),
static function ($node) {
return $node instanceof Node;
}
);
$section->append(
...$nodes
);
}
$form->append($section);
}
return [$form, $this->blockNodeRelationships];
}
/**
* @since 3.0.0
*/
protected function convertTopLevelBlockToSection(BlockModel $block, int $blockIndex): Section
{
$node = Section::make($block->getShortName() . '-' . $blockIndex)
->label($block->getAttribute('title'))
->description($block->getAttribute('description'));
$this->mapBlockToNodeRelationships($block, $node);
return $node;
}
/**
* @since 3.0.0
*
* @return Node|null
* @throws EmptyNameException|NameCollisionException
*
*/
protected function convertInnerBlockToNode(BlockModel $block, int $blockIndex)
{
$node = $this->createNodeFromBlockWithUniqueAttributes($block, $blockIndex);
if (!$node instanceof Node) {
return null;
}
if ($node instanceof Field) {
$node = $this->mapGenericBlockAttributesToField($block, $node);
}
$this->mapBlockToNodeRelationships($block, $node);
return $node;
}
/**
* @since 3.21.0 updated to use TextBlockType
* @since 3.19.4 add max rule to company field
* @since 3.9.0 Add "givewp/donor-phone" block
* @since 3.0.0
*
* @return Node|null
* @throws NameCollisionException
*
* @throws EmptyNameException
* @throws Exception
*/
protected function createNodeFromBlockWithUniqueAttributes(BlockModel $block, int $blockIndex)
{
$blockName = $block->name;
switch ($blockName) {
case "givewp/donation-amount":
return $this->createNodeFromAmountBlock($block);
case "givewp/donor-name":
return $this->createNodeFromDonorNameBlock($block);
case "givewp/donor-comments":
return Textarea::make('comment')
->label($block->getAttribute('label'))
->helpText($block->getAttribute('description'))
->rules('max:5000');
case "givewp/billing-address":
return $this->createNodeFromBillingAddressBlock($block);
case "givewp/paragraph":
return Paragraph::make($block->getShortName() . '-' . $blockIndex)
->content($block->getAttribute('content'));
case "givewp/email":
return Email::make('email')
->emailTag('email')
->rules('required', 'email')
->tap(function ($email) use ($block) {
if (is_user_logged_in()) {
$email->defaultValue(wp_get_current_user()->user_email);
}
return $email;
});
case 'givewp/donor-phone':
return Phone::make('phone')
->setIntlTelInputSettings(IntlTelInput::getSettings())
->rules('max:50', (bool)$block->getAttribute('required') ? 'required' : 'optional',
new PhoneIntlInputRule());
case "givewp/payment-gateways":
$defaultGatewayId = give(DonationFormRepository::class)->getDefaultEnabledGatewayId($this->formId);
return PaymentGateways::make('gatewayId')
->testMode(give_is_test_mode())
->rules(new GatewayRule())
->required()
->defaultValue(!empty($defaultGatewayId) ? $defaultGatewayId : null);
case "givewp/donation-summary":
return DonationSummary::make('donation-summary');
case "givewp/company":
return Text::make('company')->rules('max:255');
case TextBlockType::name():
$textBlockType = new TextBlockType($block);
return Text::make($textBlockType->getFieldName($blockIndex))
->storeAsDonorMeta($textBlockType->storeAsDonorMeta)
->description($textBlockType->description)
->defaultValue($textBlockType->defaultValue);
case "givewp/terms-and-conditions":
return $this->createNodeFromConsentBlock($block, $blockIndex)
->required();
case "givewp/login":
return Authentication::make('login')
->required($block->getAttribute('required'))
->isAuthenticated(is_user_logged_in())
->lostPasswordUrl(wp_lostpassword_url())
->loginRedirect($block->getAttribute('loginRedirect'))
->loginRedirectUrl(wp_login_url())
->loginNotice($block->getAttribute('loginNotice'))
->loginConfirmation($block->getAttribute('loginConfirmation'))
->tapNode('login', function ($field) use ($block) {
if ($block->getAttribute('required')) {
if (!is_user_logged_in()) {
$field->required();
}
$field->rules(new AuthenticationRule());
}
});
case "givewp/anonymous":
return Checkbox::make('anonymous')
->label($block->getAttribute('label'))
->helpText($block->getAttribute('description'))
->showInAdmin()
->showInReceipt()
->rules('boolean');
default:
$customField = apply_filters(
'givewp_donation_form_block_render',
null,
$block,
$blockIndex,
$this->formId
);
$customField = apply_filters(
"givewp_donation_form_block_render_{$blockName}",
$customField,
$block,
$blockIndex,
$this->formId
);
if ($customField instanceof Node) {
return $customField;
}
return null;
}
}
/**
* @since 3.17.0 updated honorific field with validation, global options, and user defaults
*
* @since 3.0.0
*/
protected function createNodeFromDonorNameBlock(BlockModel $block): Node
{
return Name::make('name')->tap(function ($group) use ($block) {
$group->getNodeByName('firstName')
->label($block->getAttribute('firstNameLabel'))
->placeholder($block->getAttribute('firstNamePlaceholder'))
->required($block->getAttribute('requireFirstName'))
->rules('required', 'max:255');
$group->getNodeByName('lastName')
->label($block->getAttribute('lastNameLabel'))
->placeholder($block->getAttribute('lastNamePlaceholder'))
->required($block->getAttribute('requireLastName'))
->rules('max:255');
if (is_user_logged_in()) {
/** @var WP_User $user */
$user = wp_get_current_user();
if ($user->first_name) {
$group->getNodeByName('firstName')->defaultValue($user->first_name);
}
if ($user->last_name) {
$group->getNodeByName('lastName')->defaultValue($user->last_name);
}
}
if ($block->hasAttribute('showHonorific') && $block->getAttribute('showHonorific') === true) {
$options = array_filter(array_values((array)$block->getAttribute('honorifics')));
if ($block->hasAttribute('useGlobalSettings') && $block->getAttribute('useGlobalSettings') === true) {
$options = give_get_option('title_prefixes', give_get_default_title_prefixes());
}
if (!empty($options)){
$group->getNodeByName('honorific')
->label(__('Title', 'give'))
->options(...$options)
->rules('max:255', 'in:' . implode(',', $options));
}
} else {
$group->remove('honorific');
}
});
}
/**
* @since 4.14.0 Add support for default country value in billing address field
* @since 3.4.0 updated fields to add optional rules last so they can be dynamically validated.
* @since 3.0.0
*/
protected function createNodeFromBillingAddressBlock(BlockModel $block): Node
{
$countryList = [];
foreach (give_get_country_list() as $value => $label) {
$countryList[] = [$value, $label];
}
return BillingAddress::make('billingAddress')
->setApiUrl(
give_get_ajax_url([
'action' => 'give_get_states',
'field_name' => 'state_selector',
])
)
->setGroupLabel(
$block->getAttribute('groupLabel')
)
->tap(function ($group) use ($block, $countryList) {
$group->getNodeByName('country')
->label($block->getAttribute('countryLabel'))
->options(...$countryList)
->defaultValue($block->getAttribute('defaultCountry'))
->rules('required');
$group->getNodeByName('address1')
->label($block->getAttribute('address1Label'))
->placeholder($block->getAttribute('address1Placeholder'))
->rules('required', 'max:255');
$group->getNodeByName('address2')
->label($block->getAttribute('address2Label'))
->placeholder($block->getAttribute('address2Placeholder'))
->required($block->getAttribute('requireAddress2'))
->rules('max:255');
$group->getNodeByName('city')
->label($block->getAttribute('cityLabel'))
->placeholder($block->getAttribute('cityPlaceholder'))
->rules('max:255', new BillingAddressCityRule(), 'optional');
$group->getNodeByName('state')
->label($block->getAttribute('stateLabel'))
->rules('max:255', new BillingAddressStateRule(), 'optional');
$group->getNodeByName('zip')
->label($block->getAttribute('zipLabel'))
->placeholder($block->getAttribute('zipPlaceholder'))
->rules('max:255', new BillingAddressZipRule(), 'optional');
});
}
/**
* @since 3.0.0
*
* @throws NameCollisionException
* @throws EmptyNameException
*/
protected function createNodeFromAmountBlock(BlockModel $block): Node
{
$donationAmountBlockModel = new DonationAmountBlockModel($block);
return (new ConvertDonationAmountBlockToFieldsApi())($donationAmountBlockModel, $this->currency);
}
/**
* @since 3.0.0
*/
protected function createNodeFromConsentBlock(BlockModel $block, int $blockIndex): Node
{
return (new ConvertConsentBlockToFieldsApi())($block, $blockIndex);
}
/**
* @since 4.14.2 add emailTag to field
* @since 3.4.1 updated to be field specific and prevent overwriting of existing values
* @since 3.0.0
*/
protected function mapGenericBlockAttributesToField(BlockModel $block, Field $field): Node
{
// Label
if ($block->hasAttribute('label') && method_exists($field, 'label')) {
$field->label($block->getAttribute('label'));
}
// Placeholder
if ($block->hasAttribute('placeholder') && method_exists($field, 'placeholder')) {
$field->placeholder($block->getAttribute('placeholder'));
}
// Required
if ($block->hasAttribute('isRequired') && method_exists($field, 'required') && !$field->isRequired()) {
$field->required($block->getAttribute('isRequired'));
}
if ($block->hasAttribute('displayInAdmin') && $block->getAttribute('displayInAdmin')) {
$field->showInAdmin($block->getAttribute('displayInAdmin'));
}
if ($block->hasAttribute('displayInReceipt') && $block->getAttribute('displayInReceipt')) {
$field->showInReceipt($block->getAttribute('displayInReceipt'));
}
if ($block->hasAttribute('emailTag') && method_exists($field, 'emailTag')) {
$field->emailTag($block->getAttribute('emailTag'));
}
return $field;
}
/**
* @since 3.0.0
*
* @return void
*/
private function mapBlockToNodeRelationships(BlockModel $block, Node $node)
{
$this->blockNodeRelationships[$block->clientId] = [
'block' => $block,
'node' => $node,
];
}
}