{"id":128,"date":"2019-06-30T21:44:30","date_gmt":"2019-06-30T21:44:30","guid":{"rendered":"https:\/\/www.antpace.com\/blog\/?p=128"},"modified":"2025-08-25T13:56:42","modified_gmt":"2025-08-25T13:56:42","slug":"building-a-saas-for-a-b-testing","status":"publish","type":"post","link":"https:\/\/www.antpace.com\/blog\/building-a-saas-for-a-b-testing\/","title":{"rendered":"Building a SAAS for A\/B testing"},"content":{"rendered":"<h2>Product development and SAAS<\/h2>\n<p><a href=\"https:\/\/www.splitwit.com\">SplitWit<\/a> is a digital product. It is a \u201csoftware as a service\u201d platform that helps <em>split test<\/em> websites and apps. That means it allows us to make changes to a website, that only half of visitors will see, and then determine which version has better results (sales, sign-ups, etc.).<\/p>\n<h2>Foundational code and design<\/h2>\n<p>I used a <a href=\"https:\/\/www.antpace.com\/blog\/a-framework-for-web-apps-and-startups\/\">template to quickly get things prototyped and working<\/a>. It came with a user account engine to handle registration, login, and more.<\/p>\n<p>The front-end design utilizes basic principles that focus on user experience. I iterated through various color pallets, and ended with a blue-shaded scheme. Subtle textured patterns applied to background sections help add a finished look. And of course, <a href=\"https:\/\/fontawesome.com\/icons?d=gallery\" target=\"_blank\" rel=\"noopener\">FontAwesome<\/a> is my go-to icon set.<\/p>\n<figure id=\"attachment_558\" aria-describedby=\"caption-attachment-558\" style=\"width: 1439px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/www.SplitWit.com\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-558 size-full\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2019\/06\/SplitWit.png\" alt=\"SplitWit.com\" width=\"1439\" height=\"900\" \/><\/a><figcaption id=\"caption-attachment-558\" class=\"wp-caption-text\">https:\/\/www.SplitWit.com<\/figcaption><\/figure>\n<p>I used a CSS rule on the main container of each page to have a minimum height of 100% of the viewport. This ensures that the page footer doesn&#8217;t end up in the middle of the screen if there is not enough content.<\/p>\n<pre>.main-content.container{\n  min-height: 100vh;\n}\n<\/pre>\n<p><a href=\"https:\/\/www.antpace.com\/blog\/sending-email-from-your-app-using-aws-ses\/\">The contact form at the bottom of the homepage is powered by AWS SES.<\/a><\/p>\n<h2>Visual optimizer and editor<\/h2>\n<p>After setting up an account, users can create experiments that target certain pages of a website. The visual optimizer lets changes be made easily between the control and variation versions.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-132\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2019\/06\/visual-editor.png\" alt=\"visual editor\" width=\"992\" height=\"465\" \/><\/p>\n<p>The editor loads up a website as an iFrame on the right side of the page. Once a page is loaded, SplitWit adds an overlay to the iFrame. This way, instead of interacting with the page, clicks can be intercepted. Any elements that get clicked are loaded up as HTML into the &#8220;make a change&#8221; section of the editor. Any changes made are saved to that variation, and will be displayed to half of visitors.<\/p>\n<p>Here is an example of the code that powers the overlay and connects it to the editor:<\/p>\n<pre>pageIframe.contents().find(\"body\").prepend(overlay);\n\npageIframe.contents().find(\"body *\").css(\"z-index\", 1).mouseenter(function(){\n  $(this).addClass('highlighted');\n  testSelectorEl = $(this);\n\n}).mouseout(function(){\n\n  $(this).removeClass('highlighted');\n\n}).click(function(e){\n\n  e.preventDefault();\n  var value = testSelectorEl.getPath()\n  selectNewElement(value);\n  \/\/scroll user to selector input\n  $([document.documentElement, document.body]).animate({\n    scrollTop: $(\".page-editor-info\").offset().top\n  }, 1000);\n\n});\n\nfunction selectNewElement(value){\n\n    testSelectorElPath = value;\n    testSelectorEl = pageIframe.contents().find(value);\n    $(\".change-indicator\").hide()\n    $(\".el-input\").removeAttr(\"disabled\");\n    $(\".element-change-save-btn\").attr(\"disabled\", \"disabled\");\n    $(\".find-selector\").hide();\n    $(\".element-change-wrap .selector-input\").val(testSelectorElPath);\n\n    $(\".toggable-section\").hide();\n    $(\".element-change-wrap\").show();\n    $(\".multiple-elements\").hide();\n\n    if(testSelectorEl.attr(\"src\") &amp;&amp; testSelectorEl.attr(\"src\").length &gt; 0){\n      $(\".img-url\").val(testSelectorEl.attr(\"src\"));\n      $(\".img-url-wrap\").show();\n      testSelectorElImage = testSelectorEl.attr(\"src\");\n    }else{\n      testSelectorElImage = \"\";\n      $(\".img-url\").val(\"\");\n      $(\".img-url-wrap\").hide();\n    }\n    if(testSelectorEl.attr(\"href\") &amp;&amp; testSelectorEl.attr(\"href\").length &gt; 0){\n      $(\".link-url\").val(testSelectorEl.attr(\"href\"));\n      $(\".link-url-wrap\").show();\n      testSelectorElLink = testSelectorEl.attr(\"href\");\n    }else{\n      testSelectorElLink = \"\";\n      $(\".link-url\").val(\"\");\n      $(\".link-url-wrap\").hide();\n    }\n\n    if(testSelectorEl.html() &amp;&amp; testSelectorEl.html().length &gt; 0){\n      $(\".html-input\").val(testSelectorEl.html());\n      $(\".html-input-wrap\").show();\n      testSelectorElHtml = testSelectorEl.html();\n    }else{\n      testSelectorElHtml = \"\";\n      $(\".html-input\").val(\"\");\n      $(\".html-input-wrap\").hide();\n    }\n\n    $(\".elem-css-group\").show();\n    if(testSelectorEl.is(\":visible\")){\n      originalVisibilityState = \"visible\";\n      $(\"#visible-radio\").attr(\"checked\", \"checked\");\n      $(\"#hidden-radio\").removeAttr(\"checked\");\n    }else{\n      originalVisibilityState = \"hidden\";\n      $(\"#hidden-radio\").attr(\"checked\", \"checked\");\n      $(\"#visible-radio\").removeAttr(\"checked\");\n\n    }\n    originalValues['height'] = testSelectorEl.css(\"height\");\n    $(\".height\").val(originalValues['height']);\n    originalValues['width'] = testSelectorEl.css(\"width\");\n    $(\".width\").val(originalValues['width']);\n    originalValues['border'] = testSelectorEl.css(\"border\");\n    $(\".border\").val(originalValues['border']);\n    originalValues['font-family'] = testSelectorEl.css(\"font-family\");\n    $(\".font-family\").val(originalValues['font-family']);\n    originalValues['font-size'] = testSelectorEl.css(\"font-size\");\n    $(\".font-size\").val(originalValues['font-size']);\n    originalValues['font-weight'] = testSelectorEl.css(\"font-weight\");\n    $(\".font-weight\").val(originalValues['font-weight']);\n    originalValues['font-style']= testSelectorEl.css(\"font-style\");\n    $(\".font-style\").val(originalValues['font-style'])\n    originalValues['text-decoration'] = testSelectorEl.css(\"text-decoration\")\n    $(\".text-decoration\").val(originalValues['text-decoration'])\n    originalValues['background'] = \"\";\n    $(\".background\").val(originalValues['background'])\n\n} \/\/end selectNewElement()\n<\/pre>\n<p>The editor has lots of built in options, so users can change the style and behavior of a page without needing to know how to code. A marketer can use this tool without the help of a developer.<\/p>\n<h2>Metrics and statistical significance<\/h2>\n<p>A key feature of SplitWit is to measure conversion metrics and performance indicators. The platform determines which variation is a winner based on the metrics set. Three types of metrics are offered: page views, click events, and custom API calls.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-157\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2019\/06\/Screen-Shot-2019-06-30-at-4.42.55-PM.png\" alt=\"bounce rate metric\" width=\"1523\" height=\"699\" \/><\/p>\n<p><a href=\"https:\/\/www.antpace.com\/blog\/statistics-for-hypothesis-tests\/\" target=\"_blank\" rel=\"noopener\">Algorithms calculate statistical significance<\/a> based on the number of visitors an experiment receives and the conversion metrics configured. This makes sure that the result is very unlikely to have occurred coincidently.<\/p>\n<h2>The code snippet<\/h2>\n<p>Each project setup in SplitWit generates a code snippet. Once this snippet is added to a website, SplitWit is able to do its magic. Using JavaScript, it applies variation changes, splits user traffic between versions, and measures key metrics about the experiments running.<\/p>\n<p>The platform uses a relational database structure.\u00a0As changes are made to experiments, the details are saved and written to a unique snippet file. When the snippet file loads, the first thing is does is check to see if there are any experiments that should be running on the current page. Each experiment can be configured to run on various URLs. The configuration rules contain three parts: a URL pattern, a type (target or exclude), and a match type (exact, basic, or substring). You can read SplitWit documentation to find an explanation of these match types.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-138\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2019\/06\/Screen-Shot-2019-06-30-at-2.44.21-PM.png\" alt=\"experiment settings\" width=\"1610\" height=\"858\" \/><\/p>\n<p>Here is the code used to test a URL against an experiment&#8217;s configuration rules:<\/p>\n<pre>function testUrl(testurl, conditions){\n\n\tif(testurl.search(\/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]\/) &lt; 0){\n\t\treturn window.inputError($(\".test-url-input\"), \"Please test a valid URL.\");\n\t}\n\tvar valid = false;\n\tvar arr  = [],\n\tkeys = Object.keys(conditions);\n\n\tfor(var i=0,n=keys.length;i&lt;n;i++){\n\t\tvar key  = keys[i];\n\t\tarr[i] = conditions[key];\n\t}\n\n\tconditions = arr;\n\tfor (i = 0; i &lt; arr.length; i++) {\n\t\tvar url = conditions[i].url;\n\t\tvar matchtype = conditions[i].matchtype;\n\t\tvar conditiontype = conditions[i].conditiontype;\n\n\t\tif(matchtype == \"exact\" &amp;&amp; conditiontype == \"target\" &amp;&amp; url == testurl){\n\t\t\tvalid = true;\n\t\t}\n\t\tif(matchtype == \"exact\" &amp;&amp; conditiontype == \"exclude\" &amp;&amp; url == testurl){\n\t\t\tvalid = false;\n\t\t}\n\n\t\tif(matchtype == \"basic\"){\n\t\t\tvar cleanTestUrl = testurl.toLowerCase();\n\t\t\tvar cleanUrl = url.toLowerCase();\n\n\t\t\tif(cleanTestUrl.indexOf(\"?\") &gt; 0) {\n\t\t\t\tcleanTestUrl = cleanTestUrl.substring(0, cleanTestUrl.indexOf(\"?\"));\n\t\t\t}\n\t\t\tif(cleanUrl.indexOf(\"?\") &gt; 0) {\n\t\t\t\tcleanUrl = cleanUrl.substring(0, cleanUrl.indexOf(\"?\"));\n\t\t\t}\n\t\t\tif(cleanTestUrl.indexOf(\"&amp;\") &gt; 0) {\n\t\t\t\tcleanTestUrl = cleanTestUrl.substring(0, cleanTestUrl.indexOf(\"&amp;\"));\n\t\t\t}\n\t\t\tif(cleanUrl.indexOf(\"&amp;\") &gt; 0) {\n\t\t\t\tcleanUrl = cleanUrl.substring(0, cleanUrl.indexOf(\"&amp;\"));\n\t\t\t}\n\t\t\tif(cleanTestUrl.indexOf(\"#\") &gt; 0) {\n\t\t\t\tcleanTestUrl = cleanTestUrl.substring(0, cleanTestUrl.indexOf(\"#\"));\n\t\t\t}\n\t\t\tif(cleanUrl.indexOf(\"#\") &gt; 0) {\n\t\t\t\tcleanUrl = cleanUrl.substring(0, cleanUrl.indexOf(\"#\"));\n\t\t\t}\n\t\t\tcleanTestUrl = cleanTestUrl.replace(\/^(?:https?:\\\/\\\/)?(?:www\\.)?\/i, \"\");\n\t\t\tcleanUrl = cleanUrl.replace(\/^(?:https?:\\\/\\\/)?(?:www\\.)?\/i, \"\");\n\t\t\tcleanTestUrl = cleanTestUrl.replace(\/\\\/$\/, \"\");\n\t\t\tcleanUrl = cleanUrl.replace(\/\\\/$\/, \"\");\n\n\t\t\tif(conditiontype == \"target\" &amp;&amp; cleanUrl == cleanTestUrl){\n\t\t\t\tvalid = true;\n\t\t\t}\n\t\t\tif(conditiontype == \"exclude\" &amp;&amp; cleanUrl == cleanTestUrl){\n\t\t\t\tvalid = false;\n\t\t\t}\n\n\t\t}\n\t\tif(matchtype == \"substring\"){\n\t\t\tif(testurl.includes(url) &amp;&amp; conditiontype == \"target\"){\n\t\t\t\tvalid = true;\n\t\t\t}\n\t\t\tif(testurl.includes(url) &amp;&amp; conditiontype == \"exclude\"){\n\t\t\t\tvalid = false;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn valid;\n\n}\n\n<\/pre>\n<h2>Subscription billing workflow<\/h2>\n<p>Stripe is used to bill customers. In the billing dashboard we can create a product, and assign it a monthly pricing plan.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-567\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2019\/06\/stripe.png\" alt=\"Subscription products\" width=\"1391\" height=\"658\" \/><\/p>\n<p>The payment processor handles re-billing customers each month. Our software is responsible for keeping track of each account&#8217;s payment status. In the database we record the date of when an account will be considered delinquent. Upon registration each account has this field set to 15 days in the future, affording a two week trial. At this point, users have not entered any credit card information.<\/p>\n<h3>Initial payment<\/h3>\n<p>Stripe&#8217;s JavaScript SDK is used during initial payment to tokenize credit card information before passing it along to the server.<\/p>\n<figure id=\"attachment_569\" aria-describedby=\"caption-attachment-569\" style=\"width: 731px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-569\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2019\/06\/activate-account.png\" alt=\"activate your subscription\" width=\"731\" height=\"379\" \/><figcaption id=\"caption-attachment-569\" class=\"wp-caption-text\">Stripe&#8217;s JS library handles card validation and tokenization.<\/figcaption><\/figure>\n<p>Below is the <strong>HTML<\/strong> used for a Stripe payment element:<\/p>\n<pre>&lt;div id=\"stripe-payment-modal\" class=\"modal stripe-payment-modal\" style=\"display: none;\"&gt;\n\n\t&lt;!-- Modal content --&gt;\n\t&lt;div class=\"modal-content\"&gt;\n\t\t&lt;p&gt;\n\t\t  &lt;button type=\"button\" class=\"dismiss-modal close\" &gt;&amp;times;&lt;\/button&gt;\n\t\t&lt;\/p&gt;\n\t\t&lt;p&gt;Activate your account subscription.&lt;\/p&gt;\n\t\t&lt;form id=\"payment-form\"&gt;\n\t\t  &lt;div class=\"form-row\"&gt;\n\t\t    &lt;!-- &lt;label for=\"card-element\"&gt;\n\t\t      Credit or debit card\n\t\t    &lt;\/label&gt; --&gt;\n\t\t    &lt;div id=\"card-element\"&gt;\n\t\t      &lt;!-- A Stripe Element will be inserted here. --&gt;\n\t\t    &lt;\/div&gt;\n\n\t\t    &lt;!-- Used to display Element errors. --&gt;\n\t\t    &lt;div id=\"card-errors\" role=\"alert\"&gt;&lt;\/div&gt;\n\t\t  &lt;\/div&gt;\n\n\t\t  &lt;button type=\"button\" class=\"btn submit-payment\"&gt;Submit Payment&lt;\/button&gt;\n\t\t&lt;\/form&gt;\n\n  \t&lt;\/div&gt;\n\n&lt;\/div&gt;\n<\/pre>\n<p>And the <strong>JavaScript<\/strong>:<\/p>\n<pre>&lt;script src=\"https:\/\/js.stripe.com\/v3\/\"&gt;&lt;\/script&gt;\n&lt;script type=\"text\/javascript\"&gt;\nvar stripe = Stripe('your-public-key-goes-here');\n\nvar elements = stripe.elements();\n\n\/\/ Custom styling can be passed to options when creating an Element.\nvar style = {\n  base: {\n    color: '#32325d',\n    fontFamily: '\"Helvetica Neue\", Helvetica, sans-serif',\n    fontSmoothing: 'antialiased',\n    fontSize: '16px',\n    '::placeholder': {\n      color: '#aab7c4'\n    }\n  },\n  invalid: {\n    color: '#fa755a',\n    iconColor: '#fa755a'\n  }\n};\n\n\/\/ Create an instance of the card Element.\nvar card = elements.create('card', {style: style});\n\n\/\/ Add an instance of the card Element into the `card-element` div.\ncard.mount('#card-element');\n\n\/\/ Handle real-time validation errors from the card Element.\ncard.addEventListener('change', function(event) {\n  var displayError = document.getElementById('card-errors');\n  if (event.error) {\n    displayError.textContent = event.error.message;\n  } else {\n    displayError.textContent = '';\n  }\n});\n\n\/\/ Handle form submission.\nvar form = document.getElementById('payment-form');\nform.addEventListener('submit', function(event) {\n  event.preventDefault();\n\n  stripe.createToken(card).then(function(result) {\n    if (result.error) {\n      \/\/ Inform the user if there was an error.\n      var errorElement = document.getElementById('card-errors');\n      errorElement.textContent = result.error.message;\n    } else {\n      \/\/ Send the token to your server.\n      stripeTokenHandler(result.token);\n    }\n  });\n});\n\n\/\/ Submit the form with the token ID.\nfunction stripeTokenHandler(token) {\n  \/\/ Insert the token ID into the form so it gets submitted to the server\n  var form = document.getElementById('payment-form');\n  var hiddenInput = document.createElement('input');\n  hiddenInput.setAttribute('type', 'hidden');\n  hiddenInput.setAttribute('name', 'stripeToken');\n  hiddenInput.setAttribute('value', token.id);\n  form.appendChild(hiddenInput);\n\n  var data = $(\"#payment-form\").serialize();\n  $.ajax({\n  \turl:\"stripe-payment-service.php\",\n  \tmethod: \"POST\",\n  \tdata: data,\n  \tcomplete: function(response){\n  \t\tconsole.log(response);\n  \t\twindow.location.reload();\n  \t}\n  })\n}\n\n$(\".submit-payment\").click(function(){\n\tstripe.createToken(card).then(function(result) {\n    if (result.error) {\n    \t\/\/ Inform the customer that there was an error.\n    \tvar errorElement = document.getElementById('card-errors');\n    \terrorElement.textContent = result.error.message;\n    } else {\n\t$(\".submit-payment\").attr(\"disabled\", \"disabled\").html('Working...');\n      \t\/\/ Send the token to your server.\n      \tstripeTokenHandler(result.token);\n    }\n  });\n});\n&lt;\/script&gt;\n<\/pre>\n<p>The above code creates a new Stripe object using a public API key. That object injects a credit card form\u00a0into our &#8216;#card-element&#8217; div, with custom styles attached. It listens for any changes, and displays validation errors. When the form is submitted, the Stripe object creates a token from the payment information. That token is passed to our back-end. Stripe&#8217;s PHP library is used to finish the transaction:<\/p>\n<pre>&lt;?php\nfunction subscribe(){\n        require_once('stripe-php-6.43.0\/init.php');\n        \\Stripe\\Stripe::setApiKey('sk_XXXX');\n\t$stripe_token = $_POST['stripeToken'];\n\t$conn = $this-&gt;connection;\n\n\tif(isset($_SESSION['email'])){\n\t\t$email = $_SESSION['email'];\n\t}else{\n\t\tdie(\"No email found.\");\n\t}\n\n\tif(strlen($email)&gt;0){\n\t\t$sql = \"SELECT * FROM `account` WHERE email = ?\";\n\t\t$result = $conn-&gt;prepare($sql);\n\t\t$result-&gt;execute(array($email));\n\t\t$row = $result-&gt;fetch(PDO::FETCH_ASSOC);\n\t}\n\t$customer_id = $row['stripe_customer_id'];\n\t\/\/check if this account already has a stripe_customer_id\n\tif(strlen($customer_id) &lt; 1){\n\t\t\/\/if not, create the customer\n\t\t$customer = \\Stripe\\Customer::create([\n\t\t  'email' =&gt; $email,\n\t\t  'source' =&gt; $stripe_token,\n\t\t]);\n\t\t$customer_id = $customer['id'];\n\t\t\/\/write stripe ID to db\n\t\t$sql = \"UPDATE `account_table` SET stripe_customer_id = ? WHERE email = ?\";\n\t\t$result = $conn-&gt;prepare($sql);\n\t\t$result-&gt;execute(array($customer_id, $email));\n\t}\n\n\t\/\/ Create the subscription\n\t$subscription = \\Stripe\\Subscription::create([\n\t  'customer' =&gt; $customer_id,\n\t  'items' =&gt; [\n\t    [\n\t      'plan' =&gt; 'plan_XXX', \/\/setup in Stripe dashboard.\n\t    ],\n\t  ],\n\t  'expand' =&gt; ['latest_invoice.payment_intent'],\n\t  'billing_cycle_anchor' =&gt; time()\n\t]);\n\t$subscription_status = $subscription['status'];\n\t$subscription_id = $subscription['id'];\n\tif($subscription_status == \"active\"){\n\t\t\/\/set current_period_end to 32 days (1 month plus some leeway) in the future. set past_due as false\n\t\t$sql = \"UPDATE `account_table` SET stripe_subscription_id = ?, current_period_end = ?, past_due = 0 WHERE email = ?\";\n\t\t$result = $conn-&gt;prepare($sql);\n\t\t$past_due = false;\n\t\t$current_period_end = new DateTime;\n\t\t$current_period_end-&gt;modify( '+32 day' );\n\t\t$current_period_end = $current_period_end-&gt;format('Y-m-d H:i:s');\n\t\t$result-&gt;execute(array($subscription_id, $current_period_end, $email));\n\t}\n\n}\n?&gt;\n<\/pre>\n<p>On the server side our secret API key is used. A customer record is created in Stripe using the payment token and user&#8217;s email. The Stripe customer ID is then used to create a subscription. We record the the customer ID and subscription ID to our database. The account&#8217;s new subscription period end is updated to 32 days in the future.<\/p>\n<h3>Cancel a subscription<\/h3>\n<p>The user is able to cancel their subscription from the SplitWit account dashboard.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-579\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2019\/06\/Cancel.png\" alt=\"cancel subscription\" width=\"952\" height=\"437\" \/><\/p>\n<p>We retrieve their subscription from Stripe, and cancel it, using their subscription ID. They will no longer be billed. We update our database to turn off the account&#8217;s experiments, delete any Stripe details, mark their\u00a0subscription as delinquent, and re-write their snippet file.<\/p>\n<pre>&lt;?php\n\nfunction cancelSubscription(){\n\trequire_once('stripe-php-6.43.0\/init.php');\n        \\Stripe\\Stripe::setApiKey('sk_XXXX');\n\n\t$conn = $this-&gt;connection;\n\tif(isset($_SESSION['userid'])){\n\t\t$accountid = $_SESSION['userid'];\n\t}else{\n\t\tdie(\"No userid found.\");\n\t}\n\n\tif(strlen($accountid)&gt;0){\n\t\t$sql = \"SELECT * FROM `account` WHERE accountid = ?\";\n\t\t$result = $conn-&gt;prepare($sql);\n\t\t$result-&gt;execute(array($accountid));\n\t\t$row = $result-&gt;fetch(PDO::FETCH_ASSOC);\n\t}\n\t$stripe_subscription_id = $row['stripe_subscription_id'];\n\t$subscription = \\Stripe\\Subscription::retrieve($stripe_subscription_id);\n\t$subscription-&gt;cancel();\n\n\t\/\/turn off experiments and update snippets. clear stripe IDs. set current_period_end to yesterday. set past_due = 1\n\t$current_period_end   = new DateTime;\n\t$current_period_end-&gt;modify( '-1 day' );\n\t$current_period_end = $current_period_end-&gt;format('Y-m-d H:i:s');\n\t$sql = \"UPDATE `account` SET stripe_customer_id = '', stripe_subscription_id = '', past_due = 1, current_period_end = ? WHERE accountid = ?\";\n\t$result = $conn-&gt;prepare($sql);\n\t$result-&gt;execute(array($current_period_end, $accountid));\n\n\t\/\/turn off all experiments\n\t$status = \"Not running\";\n\t$sql = \"UPDATE `experiment` set status = ? where accountid = ?\";\n\t$result2 = $conn-&gt;prepare($sql);\n\t$result2-&gt;execute(array($status, $accountid));\n\n\t\/\/update all snippets for this account (1 snippet per project)\n\t$sql = \"SELECT * FROM `project` WHERE accountid = ?\";\n\t$result3 = $conn-&gt;prepare($sql);\n\t$result3-&gt;execute(array($accountid));\n\t$rows3 = $result3-&gt;fetchAll(PDO::FETCH_ASSOC);\n\tforeach ($rows3 as $key3 =&gt; $value3) {\n\t\t$projectid = $value3['projectid'];\n    \t        $databaseProjectService = new DatabaseProjectService();\n\t\t$databaseProjectService -&gt; writeSnippetFile(false, false, $projectid);\n\t}\n\n\t$this-&gt;status = \"complete\";\n}\n\n?&gt;\n<\/pre>\n<h3>Re-billing subscriptions<\/h3>\n<p>As long as an account has an active subscription in Stripe, they will be automatically re-billed each month. When this event takes place, Stripe can deliver data about it to an end-point of our choice (commonly known as a webhook).<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-585\" src=\"https:\/\/www.antpace.com\/blog\/wp-content\/uploads\/2019\/06\/stripe-webhooks.png\" alt=\"stripe webhooks\" width=\"1104\" height=\"713\" \/><\/p>\n<p>SplitWit listens for an event called &#8220;invoice.payment_succeeded&#8221;, which occurs when a customer&#8217;s monthly payment is successful. When that happens the account&#8217;s subscription period end is updated to 32 days in the future.<\/p>\n<pre>&lt;?php\nfunction webhookPaymentSuccess(){\n\trequire_once('stripe-php-6.43.0\/init.php');\n\t\\Stripe\\Stripe::setApiKey('sk_XXX');\n\t$payload = @file_get_contents(\"php:\/\/input\");\n\n\t$endpoint_secret = \"whsec_XXX\";\n\n\t$sig_header = $_SERVER[\"HTTP_STRIPE_SIGNATURE\"];\n\t$event = null;\n\n\ttry {\n\t  $event = \\Stripe\\Webhook::constructEvent(\n\t    $payload, $sig_header, $endpoint_secret\n\t  );\n\t} catch(\\UnexpectedValueException $e) {\n\t  \/\/ Invalid payload\n\t  http_response_code(400); \/\/ PHP 5.4 or greater\n\t  exit();\n\t} catch(\\Stripe\\Error\\SignatureVerification $e) {\n\t  \/\/ Invalid signature\n\t  http_response_code(400); \/\/ PHP 5.4 or greater\n\t  exit();\n\t}\n\n\tif($event-&gt;type == 'invoice.payment_succeeded'){\n\n\t\t$invoice = $event-&gt;data-&gt;object;\n\t\t$customer_id = $invoice['customer'];\n\t\t\/\/update their accocunt current_period_end\n\t\t$conn = $this-&gt;connection;\n\t\t$sql = \"UPDATE `account` SET  current_period_end = ?, past_due = 0 WHERE stripe_customer_id = ?\";\n\t\t$result = $conn-&gt;prepare($sql);\n\t\t$past_due = false;\n\t\t$current_period_end = new DateTime;\n\t\t$current_period_end-&gt;modify( '+32 day' );\n\t\t$current_period_end = $current_period_end-&gt;format('Y-m-d H:i:s');\n\t\t$result-&gt;execute(array($current_period_end, $customer_id));\n\t}else{\n\t\thttp_response_code(400);\n\t        exit();\n\t}\n\n\thttp_response_code(200);\n}\n\n?&gt;\n<\/pre>\n<p>What if payment fails or never happens? The account&#8217;s subscription period end never gets updated.<\/p>\n<p>A daily scheduled task checks each active account&#8217;s\u00a0subscription period end date. If that date is in the past, we mark the account as past due, turn off all experiments, and update its snippet files.<\/p>\n<h2>The value of experimentation<\/h2>\n<p>Driving digital conversions is a science. Experimentation should be a constant exercise in this respect. Take any field and we can benefit from testing the waters and adjusting our sail. Our ability to interpret that data is the bottle neck to making good decisions. The best lesson I\u2019ve learned is that intuition is usually not enough. It&#8217;s better to look at the numbers and trust data.<\/p>\n<p>Influencing users through a funnel of action, finally leading to a conversion, is a challenge. Optimizing conversions, sales, and leads can be broken down into a system based approach.\u00a0 <a href=\"https:\/\/www.splitwit.com\">SplitWit<\/a>\u00a0focuses on that point.<\/p>\n<p><a href=\"https:\/\/www.SplitWit.com\" target=\"_blank\" rel=\"noopener\">www.SplitWit.com<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Product development and SAAS SplitWit is a digital product. It is a \u201csoftware as a service\u201d platform that helps split test websites and apps. That means it allows us to make changes to a website, that only half of visitors will see, and then determine which version has better results (sales, sign-ups, etc.). Foundational code &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/www.antpace.com\/blog\/building-a-saas-for-a-b-testing\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Building a SAAS for A\/B testing&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":3147,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[6,47,84,97,112,122,125,126,128],"class_list":["post-128","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web-development","tag-a-b-testing","tag-experimentation","tag-metrics","tag-product-development","tag-saas","tag-split-testing","tag-statistical-significance","tag-statistics","tag-stripe"],"_links":{"self":[{"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/posts\/128","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/comments?post=128"}],"version-history":[{"count":1,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/posts\/128\/revisions"}],"predecessor-version":[{"id":3148,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/posts\/128\/revisions\/3148"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/media\/3147"}],"wp:attachment":[{"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/media?parent=128"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/categories?post=128"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.antpace.com\/blog\/wp-json\/wp\/v2\/tags?post=128"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}