1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-29 10:12:41 +01:00

Phortune v0.1: add payment methods

Summary:
Hook @btrahan's Stripe form to the rest of Phortune.

  - Users can add payment methods.
  - They are saved to Stripe and associated with PhortunePaymentMethods on our side.
  - Payment methods appear on account overview.

Test Plan:
{F37548}
{F37549}
{F37550}

Reviewers: chad, btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D5438
This commit is contained in:
epriestley 2013-03-28 09:11:42 -07:00
parent 960ac3b2a6
commit 4f3b5f0ea9
13 changed files with 521 additions and 184 deletions

View file

@ -0,0 +1,14 @@
CREATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethod (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARCHAR(64) NOT NULL COLLATE utf8_bin,
name VARCHAR(255) NOT NULL,
status VARCHAR(64) NOT NULL COLLATE utf8_bin,
accountPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
expiresEpoch INT UNSIGNED,
metadata LONGTEXT NOT NULL COLLATE utf8_bin,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (phid),
KEY `key_account` (accountPHID, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View file

@ -1359,6 +1359,7 @@ phutil_register_library_map(array(
'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
'PhabricatorStripeConfigOptions' => 'applications/phortune/option/PhabricatorStripeConfigOptions.php',
'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php',
'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php',
'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php',
@ -1540,13 +1541,13 @@ phutil_register_library_map(array(
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php',
'PhortunePaymentMethodEditController' => 'applications/phortune/controller/PhortunePaymentMethodEditController.php',
'PhortunePaymentMethodListController' => 'applications/phortune/controller/PhortunePaymentMethodListController.php',
'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php',
'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php',
'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/PhortuneStripeBaseController.php',
'PhortuneStripePaymentFormView' => 'applications/phortune/stripe/view/PhortuneStripePaymentFormView.php',
'PhortuneStripeTestPaymentFormController' => 'applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php',
'PhortuneStripePaymentFormView' => 'applications/phortune/view/PhortuneStripePaymentFormView.php',
'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php',
'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php',
'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php',
@ -2977,6 +2978,7 @@ phutil_register_library_map(array(
'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow',
'PhabricatorStripeConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor',
@ -3194,13 +3196,13 @@ phutil_register_library_map(array(
0 => 'PhortuneDAO',
1 => 'PhabricatorPolicyInterface',
),
'PhortunePaymentMethodEditController' => 'PhortuneController',
'PhortunePaymentMethodListController' => 'PhabricatorController',
'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortunePaymentMethodViewController' => 'PhabricatorController',
'PhortuneProduct' => 'PhortuneDAO',
'PhortunePurchase' => 'PhortuneDAO',
'PhortuneStripeBaseController' => 'PhabricatorController',
'PhortuneStripePaymentFormView' => 'AphrontView',
'PhortuneStripeTestPaymentFormController' => 'PhortuneStripeBaseController',
'PhrictionActionConstants' => 'PhrictionConstants',
'PhrictionChangeType' => 'PhrictionConstants',
'PhrictionContent' =>

View file

@ -32,17 +32,14 @@ final class PhabricatorApplicationPhortune extends PhabricatorApplication {
'' => 'PhortuneLandingController',
'(?P<accountID>\d+)/' => array(
'' => 'PhortuneAccountViewController',
'paymentmethod/' => array(
'edit/' => 'PhortunePaymentMethodEditController',
),
),
'account/' => array(
'' => 'PhortuneAccountListController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneAccountEditController',
),
'paymentmethod/' => array(
'' => 'PhortunePaymentMethodListController',
'view/(?P<id>\d+)/' => 'PhortunePaymentMethodViewController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortunePaymentMethodEditController',
),
'stripe/' => array(
'testpaymentform/' => 'PhortuneStripeTestPaymentFormController',
),

View file

@ -95,6 +95,40 @@ final class PhortuneAccountViewController extends PhortuneController {
->setNoDataString(
pht('No payment methods associated with this account.'));
$methods = id(new PhortunePaymentMethodQuery())
->setViewer($user)
->withAccountPHIDs(array($account->getPHID()))
->withStatus(PhortunePaymentMethodQuery::STATUS_OPEN)
->execute();
if ($methods) {
$this->loadHandles(mpull($methods, 'getAuthorPHID'));
}
foreach ($methods as $method) {
$item = new PhabricatorObjectItemView();
$item->setHeader($method->getName());
switch ($method->getStatus()) {
case PhortunePaymentMethod::STATUS_ACTIVE:
$item->addAttribute(pht('Active'));
$item->setBarColor('green');
break;
}
$item->addAttribute(
pht(
'Added %s by %s',
phabricator_datetime($method->getDateCreated(), $user),
$this->getHandle($method->getAuthorPHID())->renderLink()));
if ($method->getExpiresEpoch() < time() + (60 * 60 * 24 * 30)) {
$item->addAttribute(pht('Expires Soon!'));
}
$list->addItem($item);
}
return array(
$header,
$actions,

View file

@ -0,0 +1,298 @@
<?php
final class PhortunePaymentMethodEditController
extends PhortuneController {
private $accountID;
public function willProcessRequest(array $data) {
$this->accountID = $data['accountID'];
}
/**
* @phutil-external-symbol class Stripe_Token
* @phutil-external-symbol class Stripe_Customer
*/
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$stripe_publishable_key = PhabricatorEnv::getEnvConfig(
'stripe.publishable-key');
if (!$stripe_publishable_key) {
throw new Exception(
"Stripe publishable API key (`stripe.publishable-key`) is ".
"not configured.");
}
$stripe_secret_key = PhabricatorEnv::getEnvConfig('stripe.secret-key');
if (!$stripe_secret_key) {
throw new Exception(
"Stripe secret API kye (`stripe.secret-key`) is not configured.");
}
$account = id(new PhortuneAccountQuery())
->setViewer($user)
->withIDs(array($this->accountID))
->executeOne();
if (!$account) {
return new Aphront404Response();
}
$account_uri = $this->getApplicationURI($account->getID().'/');
$e_card_number = true;
$e_card_cvc = true;
$e_card_exp = true;
$errors = array();
if ($request->isFormPost()) {
$card_errors = $request->getStr('cardErrors');
$stripe_token = $request->getStr('stripeToken');
if ($card_errors) {
$raw_errors = json_decode($card_errors);
list($e_card_number,
$e_card_cvc,
$e_card_exp,
$messages) = $this->parseRawErrors($raw_errors);
$errors = array_merge($errors, $messages);
} else if (!$stripe_token) {
$errors[] = pht('There was an unknown error processing your card.');
}
if (!$errors) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
try {
// First, make sure the token is valid.
$info = id(new Stripe_Token())
->retrieve($stripe_token, $stripe_secret_key);
// Then, we need to create a Customer in order to be able to charge
// the card more than once. We create one Customer for each card;
// they do not map to PhortuneAccounts because we allow an account to
// have more than one active card.
$customer = Stripe_Customer::create(
array(
'card' => $stripe_token,
'description' => $account->getPHID().':'.$user->getUserName(),
), $stripe_secret_key);
$card = $info->card;
} catch (Exception $ex) {
phlog($ex);
$errors[] = pht(
'There was an error communicating with the payments backend.');
}
if (!$errors) {
$payment_method = id(new PhortunePaymentMethod())
->setAccountPHID($account->getPHID())
->setAuthorPHID($user->getPHID())
->setName($card->type.' / '.$card->last4)
->setStatus(PhortunePaymentMethod::STATUS_ACTIVE)
->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month))
->setMetadata(
array(
'type' => 'stripe.customer',
'stripeCustomerID' => $customer->id,
'stripeTokenID' => $stripe_token,
))
->save();
$save_uri = new PhutilURI($account_uri);
$save_uri->setFragment('payment');
return id(new AphrontRedirectResponse())->setURI($save_uri);
}
}
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Error Adding Card'))
->appendChild(id(new AphrontErrorView())->setErrors($errors))
->addCancelButton($request->getRequestURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if ($errors) {
$errors = id(new AphrontErrorView())
->setErrors($errors);
}
$header = id(new PhabricatorHeaderView())
->setHeader(pht('Add New Payment Method'));
$form_id = celerity_generate_unique_node_id();
require_celerity_resource('stripe-payment-form-css');
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips');
$form = id(new AphrontFormView())
->setID($form_id)
->setUser($user)
->setWorkflow(true)
->setAction($request->getRequestURI())
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('')
->setValue(
javelin_tag(
'div',
array(
'class' => 'credit-card-logos',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'We support Visa, Mastercard, American Express, '.
'Discover, JCB, and Diners Club.',
'size' => 440,
)
))))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Card Number')
->setDisableAutocomplete(true)
->setSigil('number-input')
->setError($e_card_number))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('CVC')
->setDisableAutocomplete(true)
->setSigil('cvc-input')
->setError($e_card_cvc))
->appendChild(
id(new PhortuneMonthYearExpiryControl())
->setLabel('Expiration')
->setUser($user)
->setError($e_card_exp))
->appendChild(
javelin_tag(
'input',
array(
'hidden' => true,
'name' => 'stripeToken',
'sigil' => 'stripe-token-input',
)))
->appendChild(
javelin_tag(
'input',
array(
'hidden' => true,
'name' => 'cardErrors',
'sigil' => 'card-errors-input'
)))
->appendChild(
phutil_tag(
'input',
array(
'hidden' => true,
'name' => 'stripeKey',
'value' => $stripe_publishable_key,
)))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Add Payment Method')
->addCancelButton($account_uri));
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishKey' => $stripe_publishable_key,
'root' => $form_id,
));
$title = pht('Add Payment Method');
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName(pht('Account'))
->setHref($account_uri));
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName(pht('Payment Methods'))
->setHref($request->getRequestURI()));
return
$this->buildStandardPageResponse(
array(
$crumbs,
$header,
$errors,
$form,
),
array(
'title' => $title,
'device' => true,
'dust' => true,
));
}
/**
* Stripe JS and calls to Stripe handle all errors with processing this
* form. This function takes the raw errors - in the form of an array
* where each elementt is $type => $message - and figures out what if
* any fields were invalid and pulls the messages into a flat object.
*
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
private function parseRawErrors($errors) {
$card_number_error = null;
$card_cvc_error = null;
$card_expiration_error = null;
$messages = array();
foreach ($errors as $index => $error) {
$type = key($error);
$msg = reset($error);
$messages[] = $msg;
switch ($type) {
case 'number':
case 'invalid_number':
case 'incorrect_number':
$card_number_error = pht('Invalid');
break;
case 'cvc':
case 'invalid_cvc':
case 'incorrect_cvc':
$card_cvc_error = pht('Invalid');
break;
case 'expiry':
case 'invalid_expiry_month':
case 'invalid_expiry_year':
$card_expiration_error = pht('Invalid');
break;
case 'card_declined':
case 'expired_card':
case 'duplicate_transaction':
case 'processing_error':
// these errors don't map well to field(s) being bad
break;
case 'invalid_amount':
case 'missing':
default:
// these errors only happen if we (not the user) messed up so log it
$error = sprintf(
'error_type: %s error_message: %s',
$type,
$msg);
$this->logStripeError($error);
break;
}
}
return array(
$card_number_error,
$card_cvc_error,
$card_expiration_error,
$messages
);
}
private function logStripeError($message) {
phlog('STRIPE-ERROR '.$message);
}
}

View file

@ -0,0 +1,25 @@
<?php
final class PhabricatorStripeConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht("Integration with Stripe");
}
public function getDescription() {
return pht("Configure Stripe payments.");
}
public function getOptions() {
return array(
$this->newOption('stripe.publishable-key', 'string', null)
->setDescription(
pht('Stripe publishable key.')),
$this->newOption('stripe.secret-key', 'string', null)
->setDescription(
pht('Stripe secret key.')),
);
}
}

View file

@ -0,0 +1,117 @@
<?php
final class PhortunePaymentMethodQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountPHIDs;
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
private $status = self::STATUS_ANY;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAccountPHIDs(array $phids) {
$this->accountPHIDs = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
protected function loadPage() {
$table = new PhortunePaymentMethod();
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $methods) {
if (!$methods) {
return array();
}
$accounts = id(new PhortuneAccountQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getAccountPHID'))
->execute();
$accounts = mpull($accounts, null, 'getPHID');
foreach ($methods as $key => $method) {
$account = idx($accounts, $method->getAccountPHID());
if (!$account) {
unset($methods[$key]);
continue;
}
$method->attachAccount($account);
}
return $methods;
}
private function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->accountPHIDs) {
$where[] = qsprintf(
$conn,
'accountPHID IN (%Ls)',
$this->accountPHIDs);
}
switch ($this->status) {
case self::STATUS_ANY;
break;
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn,
'status in (%Ls)',
array(
PhortunePaymentMethod::STATUS_ACTIVE,
PhortunePaymentMethod::STATUS_FAILED,
));
break;
default:
throw new Exception("Unknown status '{$this->status}'!");
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($where);
}
}

View file

@ -7,9 +7,15 @@
final class PhortunePaymentMethod extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_ACTIVE = 'payment:active';
const STATUS_FAILED = 'payment:failed';
const STATUS_REMOVED = 'payment:removed';
protected $name;
protected $status;
protected $accountPHID;
protected $authorPHID;
protected $expiresEpoch;
protected $metadata;
private $account;

View file

@ -1,17 +0,0 @@
<?php
abstract class PhortuneStripeBaseController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Phortune - Stripe');
$page->setBaseURI('/phortune/stripe/');
$page->setTitle(idx($data, 'title'));
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}

View file

@ -1,146 +0,0 @@
<?php
final class PhortuneStripeTestPaymentFormController
extends PhortuneStripeBaseController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$title = 'Test Payment Form';
$error_view = null;
$card_number_error = null;
$card_cvc_error = null;
$card_expiration_error = null;
$stripe_key = $request->getStr('stripeKey');
if (!$stripe_key) {
$error_view = id(new AphrontErrorView())
->setTitle('Missing stripeKey parameter in URI');
}
if (!$error_view && $request->isFormPost()) {
$card_errors = $request->getStr('cardErrors');
$stripe_token = $request->getStr('stripeToken');
if ($card_errors) {
$raw_errors = json_decode($card_errors);
list($card_number_error,
$card_cvc_error,
$card_expiration_error,
$messages) = $this->parseRawErrors($raw_errors);
$error_view = id(new AphrontErrorView())
->setTitle('There were errors processing your card.')
->setErrors($messages);
} else if (!$stripe_token) {
// this shouldn't happen, so show the user a very generic error
// message and log that this error occurred...!
$error_view = id(new AphrontErrorView())
->setTitle('There was an unknown error processing your card.')
->setErrors(array('Please try again.'));
$error = 'payment form submitted but no stripe token and no errors';
$this->logStripeError($error);
} else {
// success -- do something with $stripe_token!!
}
} else if (!$error_view) {
$error_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle(
'If you are using a test stripe key, use 4242424242424242, '.
'any three digits for CVC, and any valid expiration date to '.
'test!');
}
$view = id(new AphrontPanelView())
->setWidth(AphrontPanelView::WIDTH_FORM)
->setHeader($title);
$form = id(new PhortuneStripePaymentFormView())
->setUser($user)
->setStripeKey($stripe_key)
->setCardNumberError($card_number_error)
->setCardCVCError($card_cvc_error)
->setCardExpirationError($card_expiration_error);
$view->appendChild($form);
return
$this->buildStandardPageResponse(
array(
$error_view,
$view,
),
array(
'title' => $title,
));
}
/**
* Stripe JS and calls to Stripe handle all errors with processing this
* form. This function takes the raw errors - in the form of an array
* where each elementt is $type => $message - and figures out what if
* any fields were invalid and pulls the messages into a flat object.
*
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
private function parseRawErrors($errors) {
$card_number_error = null;
$card_cvc_error = null;
$card_expiration_error = null;
$messages = array();
foreach ($errors as $index => $error) {
$type = key($error);
$msg = reset($error);
$messages[] = $msg;
switch ($type) {
case 'number':
case 'invalid_number':
case 'incorrect_number':
$card_number_error = true;
break;
case 'cvc':
case 'invalid_cvc':
case 'incorrect_cvc':
$card_cvc_error = true;
break;
case 'expiry':
case 'invalid_expiry_month':
case 'invalid_expiry_year':
$card_expiration_error = true;
break;
case 'card_declined':
case 'expired_card':
case 'duplicate_transaction':
case 'processing_error':
// these errors don't map well to field(s) being bad
break;
case 'invalid_amount':
case 'missing':
default:
// these errors only happen if we (not the user) messed up so log it
$error = sprintf(
'error_type: %s error_message: %s',
$type,
$msg);
$this->logStripeError($error);
break;
}
}
// append a helpful "fix this" to the messages to be displayed to the user
$messages[] = pht(
'Please fix these errors and try again.',
count($messages));
return array(
$card_number_error,
$card_cvc_error,
$card_expiration_error,
$messages
);
}
private function logStripeError($message) {
phlog('STRIPE-ERROR '.$message);
}
}

View file

@ -39,7 +39,7 @@ final class PhortuneStripePaymentFormView extends AphrontView {
}
public function render() {
$form_id = celerity_generate_unique_node_id();
$form_id = celerity_generate_unique_node_id();
require_celerity_resource('stripe-payment-form-css');
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips');
@ -105,7 +105,7 @@ final class PhortuneStripePaymentFormView extends AphrontView {
)))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Submit Payment'));
->setValue('Add Payment Method'));
Javelin::initBehavior(
'stripe-payment-form',

View file

@ -1206,6 +1206,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql',
'name' => $this->getPatchPath('20130322.phortune.sql'),
),
'20130323.phortunepayment.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20130323.phortunepayment.sql'),
),
);
}

View file

@ -3,6 +3,7 @@
* @requires javelin-behavior
* javelin-dom
* javelin-json
* javelin-workflow
* stripe-core
*/
@ -73,8 +74,11 @@ JX.behavior('stripe-payment-form', function(config) {
}
if (errors.length != 0) {
cardErrors.value = JX.JSON.stringify(errors);
root.submit();
return true;
JX.Workflow.newFromForm(root)
.start();
return;
}
// no errors detected so contact Stripe asynchronously
@ -110,14 +114,13 @@ JX.behavior('stripe-payment-form', function(config) {
// success - we can use the token to create a customer object with
// Stripe and let the billing commence!
var token = response['id'];
cardErrors.value = '[]';
stripeToken.value = token;
}
root.submit();
JX.Workflow.newFromForm(root)
.start();
}
JX.DOM.listen(
root,
'submit',
null,
onsubmit);
JX.DOM.listen(root, 'submit', null, onsubmit);
});