diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f7cc96..43bbbad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Solspace Freeform Changelog +## 2.5.18 - 2019-04-18 +### Added +- Added Active Campaign mailing list API integration (Pro edition). +- Added `EVENT_AFTER_GENERATE_RETURN_URL` developer event, allowing modifying of the return URL of forms. + +### Changed +- Updated the HubSpot integration to not create blank Deals if no Freeform data is mapped to Deal fields. +- Updated the HubSpot integration to include an IP Address mapping setting, allowing you to map IP addresses to a custom field in Contacts. + ## 2.5.17 - 2019-04-08 ### Added - Added a `getTagAttributes()` function to the Form component. diff --git a/README.md b/README.md index 5f1e25b..00b0668 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,10 @@ # Solspace Freeform plugin for Craft CMS 3.x - -Freeform is the most powerful form building plugin for Craft CMS. It gives you full control to create simple or complex multi-page forms, as well as connect your forms to many popular API integrations. +Freeform is the most reliable, intuitive and powerful form builder for Craft. Effortlessly build beautiful forms in minutes! ![Screenshot](src/icon.svg) -## Requirements - -* **Craft 3.0.0 or later** -* PHP 7.0+ -* MySQL 5.5+ (with InnoDB) or PostgreSQL 9.5+ -* [BC Math](http://php.net/manual/en/book.bc.php) or [GMP](http://php.net/manual/en/book.gmp.php) PHP extensions (usually included in most server environments) -* Windows and OS X browsers: - * Chrome 29 or later - * Firefox 28 or later - * Safari 9.0 or later - * Internet Explorer 11 or later - * Microsoft Edge - -## Installation - -To install Freeform, simply: - -1. Go to the **Plugin Store** area inside your Craft control panel and search for *Freeform*. -2. Choose to install *Freeform Lite* and/or *Freeform Pro* (*Pro* requires *Lite* to be installed) by clicking on them. -3. Click on the **Try** button to install a trial copy of Freeform. -4. Try things out and if Freeform is right for your site, and then purchase a copy of it through the Plugin Store when you're ready! - -Freeform can also be installed manually through Composer: - -1. Open your terminal and go to your Craft project: `cd /path/to/project` -2. Then tell Composer to require the plugin: `composer require solspace/craft3-freeform` -3. If you'd like Freeform Pro, also run: `composer require solspace/craft3-freeform-pro` -4. In the Craft control panel, go to *Settings → Plugins* and click the **Install** button for Freeform Lite (and Freeform Pro if you're using Pro edition). - -## Freeform Overview - -Freeform centers itself around the idea of letting admins and/or clients enjoy the experience of building and managing simple or complex forms in an intuitive interface that lets you see a live preview of the forms you're building. We call this Composer, where almost everything is at your fingertips as it aims to stay out of your way and let you do as much as possible without having to move around to other areas in the control panel. At the same time, Freeform is very powerful and flexible, so there is also a wide variety of other features, settings and options. - -Freeform uses its own set of fields and field types. Fields are global and available to all forms, but they can also be overwritten per form. This allows you to save time reusing existing fields when making other forms, but also gives you flexibility to make adjustments to them when needed. So to clarify, you can create fields with labels and options that are common to all forms, but also override those on each form. An advanced set of fields are available with purchase of Freeform Pro. - -Email notifications are global and can be reused for multiple forms, saving you time when you are managing many forms. Freeform allows you to send email notifications upon submittal of a form 5 different ways, each with their own content/template. Email templates can be managed within Craft control panel (saved to database), or as HTML template files. - -Freeform attempts to do all the heavy lifting when it comes to templating. Our looping templating approach allows you to automate all or almost all of your form formatting. - -Freeform also allows for true multi-page forms, has its own built in spam protection service, and Freeform Pro supports several popular Mailing List and CRM (Customer Relationship Management) API integrations, including MailChimp, dotmailer, Constant Contact, Campaign Monitor, Salesforce and HubSpot. -Freeform also allows for true multi-page forms, has its own built in spam protection service (including **reCAPTCHA** for *Pro* edition), allows you to map/connect your submission data to Craft Elements, and *Freeform Pro* supports several popular Mailing List and CRM (Customer Relationship Management) API integrations, including MailChimp, Constant Contact, Campaign Monitor, Salesforce, HubSpot and Pipedrive. - -Freeform Pro edition also includes Conditional Rules logic that can be added to forms. This feature allows you to effortlessly set fields to show or hide based on the contents/selection of other fields, and even skip pages based on the contents/selection of fields on a previous page. - -Also available for Freeform is the [Freeform Payments](http://docs.solspace.com/craft/freeform/v2/api-integrations/payments/) add-on plugin, which adds support for Stripe payments on forms. Works with both Lite and Pro editions. - -Last but not least, included with Freeform is a set of Demo Templates that can be installed on your site, instantaneously giving you a real-world set of styled, working templates. - - -## Using Freeform +## Overview +Freeform is the most reliable, intuitive and powerful form building plugin for Craft. Everything is at your fingertips in our elegant form builder. It gives you full control to create simple or complex multi-page forms, as well as connect your forms to many popular API integrations. Templating is easy and highly customizable. Our ready-to-go templates and features will have you ready in minutes! No other form plugin even comes close to comparing! Stop wasting valuable development hours wrestling with and tuning the Craft Contact Form or other alternatives. You can trust Freeform (and the team behind it) to deliver the quality and support you expect and deserve. +## Documentation Full documentation for Freeform can be found on the [Solspace documentation website](http://docs.solspace.com/craft/freeform/v2/). diff --git a/composer.json b/composer.json index 788333f..970cd64 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "solspace/craft3-freeform-pro", "description": "Adds API integrations, reCAPTCHA, conditional rules logic, premium field types, advanced exporting and widgets to Freeform.", - "version": "2.5.17", + "version": "2.5.18", "type": "craft-plugin", "authors": [ { diff --git a/src/Integrations/CRM/HubSpot.php b/src/Integrations/CRM/HubSpot.php index c207615..3c827c5 100644 --- a/src/Integrations/CRM/HubSpot.php +++ b/src/Integrations/CRM/HubSpot.php @@ -18,13 +18,14 @@ use Solspace\Freeform\Library\Integrations\DataObjects\FieldObject; use Solspace\Freeform\Library\Integrations\IntegrationStorageInterface; use Solspace\Freeform\Library\Integrations\SettingBlueprint; -use Solspace\Freeform\Library\Logging\LoggerInterface; class HubSpot extends AbstractCRMIntegration { - const SETTING_API_KEY = 'api_key'; - const TITLE = 'HubSpot'; - const LOG_CATEGORY = 'HubSpot'; + const SETTING_API_KEY = 'api_key'; + const SETTING_IP_FIELD = 'ip_field'; + + const TITLE = 'HubSpot'; + const LOG_CATEGORY = 'HubSpot'; /** * Returns a list of additional settings for this integration @@ -42,6 +43,12 @@ public static function getSettingBlueprints(): array 'Enter your HubSpot API key here.', true ), + new SettingBlueprint( + SettingBlueprint::TYPE_TEXT, + self::SETTING_IP_FIELD, + 'IP Address Field', + 'Enter a custom HubSpot Contact field handle where you wish to store the client\'s IP address from the submission (optional).' + ), ]; } @@ -83,6 +90,13 @@ public function pushObject(array $keyValueList): bool $contactId = null; if ($contactProps) { + if ($this->getSetting(self::SETTING_IP_FIELD) && isset($_SERVER['REMOTE_ADDR'])) { + $contactProps[] = [ + 'value' => $_SERVER['REMOTE_ADDR'], + 'property' => $this->getSetting(self::SETTING_IP_FIELD), + ]; + } + try { $response = $client->post( $this->getEndpoint('/contacts/v1/contact'), @@ -142,33 +156,37 @@ public function pushObject(array $keyValueList): bool } } - $deal = [ - 'properties' => $dealProps, - ]; + if (!empty($dealProps)) { + $deal = [ + 'properties' => $dealProps, + ]; - if ($companyId || $contactId) { - $deal['associations'] = []; + if ($companyId || $contactId) { + $deal['associations'] = []; - if ($companyId) { - $deal['associations']['associatedCompanyIds'] = [$companyId]; - } + if ($companyId) { + $deal['associations']['associatedCompanyIds'] = [$companyId]; + } - if ($contactId) { - $deal['associations']['associatedVids'] = [$contactId]; + if ($contactId) { + $deal['associations']['associatedVids'] = [$contactId]; + } } - } - $response = $client->post( - $endpoint, - [ - 'json' => $deal, - 'query' => ['hapikey' => $this->getAccessToken()], - ] - ); + $response = $client->post( + $endpoint, + [ + 'json' => $deal, + 'query' => ['hapikey' => $this->getAccessToken()], + ] + ); - $this->getHandler()->onAfterResponse($this, $response); + $this->getHandler()->onAfterResponse($this, $response); + + return $response->getStatusCode() === 200; + } - return $response->getStatusCode() === 200; + return true; } /** diff --git a/src/Integrations/MailingLists/ActiveCampaign.php b/src/Integrations/MailingLists/ActiveCampaign.php new file mode 100644 index 0000000..df3b16a --- /dev/null +++ b/src/Integrations/MailingLists/ActiveCampaign.php @@ -0,0 +1,285 @@ +generateAuthorizedClient(); + $endpoint = $this->getEndpoint('/contact/sync'); + + $contactId = null; + + /** + * Create contact with standard fields + */ + try { + $email = reset($emails); + $contactData = ['contact' => array_merge(['email' => $email], $mappedValues)]; + + $response = $client->post($endpoint, ['json' => $contactData]); + $this->getHandler()->onAfterResponse($this, $response); + + $json = \GuzzleHttp\json_decode($response->getBody()); + $contactId = $json->contact->id; + } catch (RequestException $exception) { + throw new IntegrationException($exception->getMessage(), $exception->getCode(), $exception->getPrevious()); + } + + // Remove generic Contact Fields + unset($mappedValues['firstName'], $mappedValues['lastName'], $mappedValues['phone']); + + $endpoint = $this->getEndpoint('/contactLists'); + $payload = [ + 'contactList' => [ + 'list' => $mailingList->getId(), + 'contact' => $contactId, + 'status' => 1, + ], + ]; + + try { + $response = $client->post($endpoint, ['json' => $payload]); + $this->getHandler()->onAfterResponse($this, $response); + } catch (RequestException $exception) { + throw new IntegrationException($exception->getMessage(), $exception->getCode(), $exception->getPrevious()); + } + + $endpoint = $this->getEndpoint('/fieldValues'); + foreach ($mappedValues as $key => $value) { + $fieldId = (string) $key; + + if (is_array($value)) { + $value = '||' . implode('||', $value) . '||'; + } + + $customField = [ + 'fieldValue' => [ + 'contact' => $contactId, + 'field' => $fieldId, + 'value' => $value, + ], + ]; + + try { + $response = $client->post($endpoint, ['json' => $customField]); + $this->getHandler()->onAfterResponse($this, $response); + } catch (RequestException $exception) { + throw new IntegrationException($exception->getMessage(), $exception->getCode(), $exception->getPrevious()); + } + } + + return (bool) $contactId; + } + + /** + * Check if it's possible to connect to the API + * + * @return bool + */ + + public function checkConnection(): bool + { + $client = $this->generateAuthorizedClient(); + $endpoint = $this->getEndpoint('/lists'); + + try { + $response = $client->get($endpoint); + $json = json_decode((string) $response->getBody(), true); + + return isset($json['lists']); + } catch (RequestException $exception) { + throw new IntegrationException($exception->getMessage(), $exception->getCode(), $exception->getPrevious()); + } + } + + /** + * @inheritDoc + */ + protected function fetchLists(): array + { + $client = $this->generateAuthorizedClient(); + $endpoint = $this->getEndpoint('/lists'); + + try { + $response = $client->get($endpoint); + } catch (RequestException $exception) { + $responseBody = (string) $exception->getResponse()->getBody(); + $this->getLogger()->error($responseBody, ['exception' => $exception->getMessage()]); + + throw new IntegrationException( + $this->getTranslator()->translate('Could not connect to API endpoint') + ); + } + + $json = \GuzzleHttp\json_decode((string) $response->getBody()); + + $lists = []; + foreach ($json->lists as $list) { + $lists[] = new ListObject( + $this, + $list->id, + $list->name, + $this->fetchFields($list->id) + ); + } + + return $lists; + } + + /** + * Fetch the custom fields from the integration + * + * @param $listId + * + * @return FieldObject[] + */ + public function fetchFields($listId): array + { + $fieldList = [ + new FieldObject('firstName', 'First Name', FieldObject::TYPE_STRING, false), + new FieldObject('lastName', 'Last Name', FieldObject::TYPE_STRING, false), + new FieldObject('phone', 'Phone', FieldObject::TYPE_STRING, false), + ]; + + $client = $this->generateAuthorizedClient(); + $response = $client->get($this->getEndpoint('/fields/')); + + $data = json_decode((string) $response->getBody()); + $data = $data->fields; + + foreach ($data as $field) { + $type = null; + switch ($field->type) { + case 'text': + case 'textarea': + case 'hidden': + case 'dropdown': + case 'radio': + $type = FieldObject::TYPE_STRING; + break; + + case 'date': + $type = FieldObject::TYPE_DATE; + break; + + case 'checkbox': + case 'listbox': + $type = FieldObject::TYPE_ARRAY; + break; + } + + if (null === $type) { + continue; + } + + $fieldObject = new FieldObject( + $field->id, + $field->title, + $type, + false + ); + + $fieldList[] = $fieldObject; + } + + return $fieldList; + } + + /** + * Authorizes the application + * Returns the access_token + * + * @return string + * @throws IntegrationException + */ + public function fetchAccessToken(): string + { + return $this->getSetting(self::SETTING_API_KEY); + } + + /** + * A method that initiates the authentication + */ + public function initiateAuthentication() + { + } + + /** + * Perform anything necessary before this integration is saved + * + * @param IntegrationStorageInterface $model + */ + public function onBeforeSave(IntegrationStorageInterface $model) + { + $model->updateAccessToken($this->getSetting(self::SETTING_API_KEY)); + } + + /** + * @return string + */ + protected function getApiRootUrl(): string + { + return $this->getSetting(self::SETTING_API_URL) . '/api/3/'; + } + + /** + * @return Client + */ + private function generateAuthorizedClient(): Client + { + return new Client(['headers' => ['Api-Token' => $this->getSetting(self::SETTING_API_KEY)]]); + } +} +