diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index 57ff42b0d..91736f51a 100644 --- a/airtime_mvc/application/Bootstrap.php +++ b/airtime_mvc/application/Bootstrap.php @@ -24,12 +24,14 @@ require_once "FileIO.php"; require_once "OsPath.php"; require_once "Database.php"; require_once "ProvisioningHelper.php"; +require_once "GoogleAnalytics.php"; require_once "Timezone.php"; require_once "Auth.php"; require_once __DIR__.'/forms/helpers/ValidationTypes.php'; require_once __DIR__.'/forms/helpers/CustomDecorators.php'; require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php'; require_once __DIR__.'/controllers/plugins/Maintenance.php'; +require_once __DIR__.'/controllers/plugins/ConversionTracking.php'; require_once __DIR__.'/modules/rest/controllers/ShowImageController.php'; require_once __DIR__.'/modules/rest/controllers/MediaController.php'; @@ -52,6 +54,7 @@ Application_Model_Auth::pinSessionToClient(Zend_Auth::getInstance()); $front = Zend_Controller_Front::getInstance(); $front->registerPlugin(new RabbitMqPlugin()); +$front->registerPlugin(new Zend_Controller_Plugin_ConversionTracking()); $front->throwExceptions(false); //localization configuration diff --git a/airtime_mvc/application/common/GoogleAnalytics.php b/airtime_mvc/application/common/GoogleAnalytics.php new file mode 100644 index 000000000..44d951cc7 --- /dev/null +++ b/airtime_mvc/application/common/GoogleAnalytics.php @@ -0,0 +1,90 @@ +sub($trialDuration); + $interval = $today->diff($accountCreationDate); + $accountDuration = $interval->days; + } + + $code = "$( document ).ready(function() { + dataLayer.push({ + 'UserID': '" . $clientId . "', + 'Customer': 'Customer', + 'PlanType': '" . $plan . "', + 'Trial': '" . $isTrial . "', + 'AccountDuration': '" . strval($accountDuration) . "' + }); + });"; + //No longer sending these variables because we used to make a query to WHMCS + //to fetch them, which was slow. + // 'ZipCode': '" . $postcode . "', + // 'Country': '" . $country . "', + + } catch (Exception $e) { + Logging::error($e); + return ""; + } + return $code; + } + + /** Generate the JavaScript snippet that logs a trial to paid conversion */ + public static function generateConversionTrackingJavaScript() + { + $newPlan = Application_Model_Preference::GetPlanLevel(); + $oldPlan = Application_Model_Preference::GetOldPlanLevel(); + + $code = "dataLayer.push({'event': 'Conversion', + 'Conversion': 'Trial to Paid', + 'Old Plan' : '$oldPlan', + 'New Plan' : '$newPlan'});"; + return $code; + } + + /** Return true if the user used to be on a trial plan and was just converted to a paid plan. */ + public static function didPaidConversionOccur($request) + { + $userInfo = Zend_Auth::getInstance()->getStorage()->read(); + if ($userInfo) { + $user = new Application_Model_User($userInfo->id); + } else { + return; + } + + $oldPlan = Application_Model_Preference::GetOldPlanLevel(); + + if ($user->isSuperAdmin() && $request->getControllerKey() !== "thank-you") + { + //Only tracking trial->paid conversions for now. + if ($oldPlan == "trial") + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/airtime_mvc/application/common/HTTPHelper.php b/airtime_mvc/application/common/HTTPHelper.php index 177892e1f..d6f1d6820 100644 --- a/airtime_mvc/application/common/HTTPHelper.php +++ b/airtime_mvc/application/common/HTTPHelper.php @@ -27,7 +27,7 @@ class Application_Common_HTTPHelper if (empty($baseDir)) { $baseDir = "/"; } - if ($baseDir[0] != "") { + if ($baseDir[0] != "/") { $baseDir = "/" . $baseDir; } diff --git a/airtime_mvc/application/configs/ACL.php b/airtime_mvc/application/configs/ACL.php index 8043a7e76..883656fd7 100644 --- a/airtime_mvc/application/configs/ACL.php +++ b/airtime_mvc/application/configs/ACL.php @@ -36,6 +36,7 @@ $ccAcl->add(new Zend_Acl_Resource('library')) ->add(new Zend_Acl_Resource('rest:media')) ->add(new Zend_Acl_Resource('rest:show-image')) ->add(new Zend_Acl_Resource('billing')) + ->add(new Zend_Acl_Resource('thank-you')) ->add(new Zend_Acl_Resource('provisioning')); /** Creating permissions */ @@ -69,6 +70,7 @@ $ccAcl->allow('G', 'index') ->allow('A', 'user') ->allow('A', 'systemstatus') ->allow('A', 'preference') + ->allow('S', 'thank-you') ->allow('S', 'billing'); diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index eae99d778..f89a5472e 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -44,6 +44,13 @@ class Config { $CC_CONFIG['dev_env'] = 'production'; } + //Backported static_base_dir default value into saas for now. + if (array_key_exists('static_base_dir', $values['general'])) { + $CC_CONFIG['staticBaseDir'] = $values['general']['static_base_dir']; + } else { + $CC_CONFIG['staticBaseDir'] = '/'; + } + // Parse separate conf file for cloud storage values $cloudStorageConfig = "/etc/airtime-saas/".$CC_CONFIG['dev_env']."/cloud_storage.conf"; if (!file_exists($cloudStorageConfig)) { diff --git a/airtime_mvc/application/controllers/ShowbuilderController.php b/airtime_mvc/application/controllers/ShowbuilderController.php index 5638cf719..3bb30abf4 100644 --- a/airtime_mvc/application/controllers/ShowbuilderController.php +++ b/airtime_mvc/application/controllers/ShowbuilderController.php @@ -35,7 +35,7 @@ class ShowbuilderController extends Zend_Controller_Action $user = Application_Model_User::GetCurrentUser(); $userType = $user->getType(); $this->view->headScript()->appendScript("localStorage.setItem( 'user-type', '$userType' );"); - $this->view->headScript()->appendScript($this->generateGoogleTagManagerDataLayerJavaScript()); + $this->view->headScript()->appendScript(Application_Common_GoogleAnalytics::generateGoogleTagManagerDataLayerJavaScript()); $this->view->headScript()->appendFile($baseUrl.'js/contextmenu/jquery.contextMenu.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/datatables/js/jquery.dataTables.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); @@ -367,104 +367,5 @@ class ShowbuilderController extends Zend_Controller_Action throw new Exception("this controller is/was a no-op please fix your code"); } - - /** Returns a string containing the JavaScript code to pass some billing account info - * into Google Tag Manager / Google Analytics, so we can track things like the plan type. - */ - private static function generateGoogleTagManagerDataLayerJavaScript() - { - $code = ""; - - try - { - $accessKey = $_SERVER["WHMCS_ACCESS_KEY"]; - $username = $_SERVER["WHMCS_USERNAME"]; - $password = $_SERVER["WHMCS_PASSWORD"]; - $url = "https://account.sourcefabric.com/includes/api.php?accesskey=" . $accessKey; # URL to WHMCS API file goes here - - $postfields = array(); - $postfields["username"] = $username; - $postfields["password"] = md5($password); - $postfields["action"] = "getclientsdetails"; - $postfields["stats"] = true; - $postfields["clientid"] = Application_Model_Preference::GetClientId(); - $postfields["responsetype"] = "json"; - - $query_string = ""; - foreach ($postfields AS $k=>$v) $query_string .= "$k=".urlencode($v)."&"; - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 ); // WHMCS IP whitelist doesn't support IPv6 - curl_setopt($ch, CURLOPT_TIMEOUT, 5); //Aggressive 5 second timeout - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $query_string); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - $jsondata = curl_exec($ch); - if (curl_error($ch)) { - //die("Connection Error: ".curl_errno($ch).' - '.curl_error($ch)); - throw new Exception("WHMCS server down or invalid request."); - } - curl_close($ch); - - $arr = json_decode($jsondata); # Decode JSON String - - if ($arr->result !== "success") { - Logging::warn("WHMCS API call failed in " . __FUNCTION__); - return; - } - - $client = $arr->client; - $stats = $arr->stats; - $currencyCode = $client->currency_code; - //$incomeCents = NumberFormatter::parseCurrency($stats->income, $currencyCode); - - $isTrial = true; - if (strpos($stats->income, "0.00") === FALSE) { - $isTrial = false; - } - /* - if ($incomeCents > 0) { - $isTrial = false; - }*/ - $plan = Application_Model_Preference::GetPlanLevel(); - $country = $client->country; - $postcode = $client->postcode; - - //Figure out how long the customer has been around using a mega hack. - //(I'm avoiding another round trip to WHMCS for now...) - //We calculate it based on the trial end date... - $trialEndDateStr = Application_Model_Preference::GetTrialEndingDate(); - if ($trialEndDateStr == '') { - $accountDuration = 0; - } else { - $today = new DateTime(); - $trialEndDate = new DateTime($trialEndDateStr); - $trialDuration = new DateInterval("P30D"); //30 day trial duration - $accountCreationDate = $trialEndDate->sub($trialDuration); - $interval = $today->diff($accountCreationDate); - $accountDuration = $interval->days; - } - - $code = "$( document ).ready(function() { - dataLayer.push({ - 'ZipCode': '" . $postcode . "', - 'UserID': '" . $client->id . "', - 'Customer': 'Customer', - 'PlanType': '" . $plan . "', - 'Trial': '" . $isTrial . "', - 'Country': '" . $country . "', - 'AccountDuration': '" . strval($accountDuration) . "' - }); - });"; - - } - catch (Exception $e) - { - return ""; - } - return $code; - } + } diff --git a/airtime_mvc/application/controllers/ThankYouController.php b/airtime_mvc/application/controllers/ThankYouController.php new file mode 100644 index 000000000..86a57a54c --- /dev/null +++ b/airtime_mvc/application/controllers/ThankYouController.php @@ -0,0 +1,48 @@ +view->stationUrl = Application_Common_HTTPHelper::getStationUrl(); + $this->view->conversionUrl = Application_Common_HTTPHelper::getStationUrl() . 'thank-you/confirm-conversion'; + $this->view->gaEventTrackingJsCode = ""; //Google Analytics event tracking code that logs an event. + + // Embed the Google Analytics conversion tracking code if the + // user is a super admin and old plan level is set to trial. + if (Application_Common_GoogleAnalytics::didPaidConversionOccur($this->getRequest())) { + $this->view->gaEventTrackingJsCode = Application_Common_GoogleAnalytics::generateConversionTrackingJavaScript(); + } + + $csrf_namespace = new Zend_Session_Namespace('csrf_namespace'); + $csrf_element = new Zend_Form_Element_Hidden('csrf'); + $csrf_element->setValue($csrf_namespace->authtoken)->setRequired('true')->removeDecorator('HtmlTag')->removeDecorator('Label'); + $csrf_form = new Zend_Form(); + $csrf_form->addElement($csrf_element); + $this->view->form = $csrf_form; + } + + /** Confirm that a conversion was tracked. */ + public function confirmConversionAction() + { + $this->view->layout()->disableLayout(); + $this->_helper->viewRenderer->setNoRender(true); + + $current_namespace = new Zend_Session_Namespace('csrf_namespace'); + $observed_csrf_token = $this->_getParam('csrf_token'); + $expected_csrf_token = $current_namespace->authtoken; + + if($observed_csrf_token != $expected_csrf_token) { + Logging::info("Invalid CSRF token"); + return; + } + + if ($this->getRequest()->isPost()) { + Logging::info("Goal conversion from trial to paid."); + // Clear old plan level so we prevent duplicate events. + // This should only be called from AJAX. See thank-you/index.phtml + Application_Model_Preference::ClearOldPlanLevel(); + } + } +} \ No newline at end of file diff --git a/airtime_mvc/application/controllers/plugins/ConversionTracking.php b/airtime_mvc/application/controllers/plugins/ConversionTracking.php new file mode 100644 index 000000000..09904828d --- /dev/null +++ b/airtime_mvc/application/controllers/plugins/ConversionTracking.php @@ -0,0 +1,21 @@ +getControllerName() != 'thank-you') + { + $request->setModuleName('default') + ->setControllerName('thank-you') + ->setActionName('index') + ->setDispatched(true); + } + } + } + +} \ No newline at end of file diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php index a25679826..b7ea8a70e 100644 --- a/airtime_mvc/application/models/Preference.php +++ b/airtime_mvc/application/models/Preference.php @@ -824,7 +824,10 @@ class Application_Model_Preference public static function SetPlanLevel($plan) { + $oldPlanLevel = GetPlanLevel(); self::setValue("plan_level", $plan); + //We save the old plan level temporarily to facilitate conversion tracking + self::setValue("old_plan_level", $oldPlanLevel); } public static function GetPlanLevel() @@ -837,6 +840,19 @@ class Application_Model_Preference return $plan; } + public static function GetOldPlanLevel() + { + $oldPlan = self::getValue("old_plan_level"); + return $oldPlan; + } + + /** Clearing the old plan level indicates a change in your plan has been tracked (Google Analytics) */ + public static function ClearOldPlanLevel() + { + self::setValue("old_plan_level", ''); + } + + public static function SetTrialEndingDate($date) { self::setValue("trial_end_date", $date); diff --git a/airtime_mvc/application/views/scripts/thank-you/index.phtml b/airtime_mvc/application/views/scripts/thank-you/index.phtml new file mode 100644 index 000000000..436b9b378 --- /dev/null +++ b/airtime_mvc/application/views/scripts/thank-you/index.phtml @@ -0,0 +1,20 @@ + +form->getElement('csrf') ?> + +
diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css index d709bf038..67eb50aaf 100644 --- a/airtime_mvc/public/css/styles.css +++ b/airtime_mvc/public/css/styles.css @@ -3141,3 +3141,15 @@ dd .stream-status { .quota-reached { font-size: 14px !important; } + +.thankyou-panel +{ + width: 400px; + margin: 0 auto; + margin-bottom: 30px; +} + +.thankyou-panel h3 +{ + color: #222; +}