mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #2020 from overleaf/ta-recurly-sca
SCA Integration GitOrigin-RevId: d7935584f87ec7c7339d050430efc87420a39de1
This commit is contained in:
parent
b90d07fc6f
commit
03460ba229
10 changed files with 327 additions and 108 deletions
14
services/web/app/src/Features/Subscription/Errors.js
Normal file
14
services/web/app/src/Features/Subscription/Errors.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
const OError = require('@overleaf/o-error')
|
||||
|
||||
class RecurlyTransactionError extends OError {
|
||||
constructor(options) {
|
||||
super({
|
||||
message: 'Unknown transaction error',
|
||||
...options
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RecurlyTransactionError
|
||||
}
|
|
@ -24,6 +24,7 @@ const Settings = require('settings-sharelatex')
|
|||
const xml2js = require('xml2js')
|
||||
const logger = require('logger-sharelatex')
|
||||
const Async = require('async')
|
||||
const SubscriptionErrors = require('./Errors')
|
||||
|
||||
module.exports = RecurlyWrapper = {
|
||||
apiUrl: Settings.apis.recurly.url || 'https://api.recurly.com/v2',
|
||||
|
@ -31,10 +32,9 @@ module.exports = RecurlyWrapper = {
|
|||
_paypal: {
|
||||
checkAccountExists(cache, next) {
|
||||
const { user } = cache
|
||||
const { recurly_token_id } = cache
|
||||
const { subscriptionDetails } = cache
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
{ user_id: user._id },
|
||||
'checking if recurly account exists for user'
|
||||
)
|
||||
return RecurlyWrapper.apiRequest(
|
||||
|
@ -46,7 +46,7 @@ module.exports = RecurlyWrapper = {
|
|||
function(error, response, responseBody) {
|
||||
if (error) {
|
||||
logger.warn(
|
||||
{ error, user_id: user._id, recurly_token_id },
|
||||
{ error, user_id: user._id },
|
||||
'error response from recurly while checking account'
|
||||
)
|
||||
return next(error)
|
||||
|
@ -54,25 +54,19 @@ module.exports = RecurlyWrapper = {
|
|||
if (response.statusCode === 404) {
|
||||
// actually not an error in this case, just no existing account
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
{ user_id: user._id },
|
||||
'user does not currently exist in recurly, proceed'
|
||||
)
|
||||
cache.userExists = false
|
||||
return next(null, cache)
|
||||
}
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
'user appears to exist in recurly'
|
||||
)
|
||||
logger.log({ user_id: user._id }, 'user appears to exist in recurly')
|
||||
return RecurlyWrapper._parseAccountXml(responseBody, function(
|
||||
err,
|
||||
account
|
||||
) {
|
||||
if (err) {
|
||||
logger.warn(
|
||||
{ err, user_id: user._id, recurly_token_id },
|
||||
'error parsing account'
|
||||
)
|
||||
logger.warn({ err, user_id: user._id }, 'error parsing account')
|
||||
return next(err)
|
||||
}
|
||||
cache.userExists = true
|
||||
|
@ -84,7 +78,6 @@ module.exports = RecurlyWrapper = {
|
|||
},
|
||||
createAccount(cache, next) {
|
||||
const { user } = cache
|
||||
const { recurly_token_id } = cache
|
||||
const { subscriptionDetails } = cache
|
||||
const { address } = subscriptionDetails
|
||||
if (!address) {
|
||||
|
@ -93,16 +86,10 @@ module.exports = RecurlyWrapper = {
|
|||
)
|
||||
}
|
||||
if (cache.userExists) {
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
'user already exists in recurly'
|
||||
)
|
||||
logger.log({ user_id: user._id }, 'user already exists in recurly')
|
||||
return next(null, cache)
|
||||
}
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
'creating user in recurly'
|
||||
)
|
||||
logger.log({ user_id: user._id }, 'creating user in recurly')
|
||||
const data = {
|
||||
account_code: user._id,
|
||||
email: user.email,
|
||||
|
@ -128,7 +115,7 @@ module.exports = RecurlyWrapper = {
|
|||
(error, response, responseBody) => {
|
||||
if (error) {
|
||||
logger.warn(
|
||||
{ error, user_id: user._id, recurly_token_id },
|
||||
{ error, user_id: user._id },
|
||||
'error response from recurly while creating account'
|
||||
)
|
||||
return next(error)
|
||||
|
@ -138,10 +125,7 @@ module.exports = RecurlyWrapper = {
|
|||
account
|
||||
) {
|
||||
if (err) {
|
||||
logger.warn(
|
||||
{ err, user_id: user._id, recurly_token_id },
|
||||
'error creating account'
|
||||
)
|
||||
logger.warn({ err, user_id: user._id }, 'error creating account')
|
||||
return next(err)
|
||||
}
|
||||
cache.account = account
|
||||
|
@ -152,12 +136,9 @@ module.exports = RecurlyWrapper = {
|
|||
},
|
||||
createBillingInfo(cache, next) {
|
||||
const { user } = cache
|
||||
const { recurly_token_id } = cache
|
||||
const { recurlyTokenIds } = cache
|
||||
const { subscriptionDetails } = cache
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
'creating billing info in recurly'
|
||||
)
|
||||
logger.log({ user_id: user._id }, 'creating billing info in recurly')
|
||||
const accountCode = __guard__(
|
||||
cache != null ? cache.account : undefined,
|
||||
x1 => x1.account_code
|
||||
|
@ -165,7 +146,7 @@ module.exports = RecurlyWrapper = {
|
|||
if (!accountCode) {
|
||||
return next(new Error('no account code at createBillingInfo stage'))
|
||||
}
|
||||
const data = { token_id: recurly_token_id }
|
||||
const data = { token_id: recurlyTokenIds.billing }
|
||||
const requestBody = RecurlyWrapper._buildXml('billing_info', data)
|
||||
return RecurlyWrapper.apiRequest(
|
||||
{
|
||||
|
@ -176,7 +157,7 @@ module.exports = RecurlyWrapper = {
|
|||
(error, response, responseBody) => {
|
||||
if (error) {
|
||||
logger.warn(
|
||||
{ error, user_id: user._id, recurly_token_id },
|
||||
{ error, user_id: user._id },
|
||||
'error response from recurly while creating billing info'
|
||||
)
|
||||
return next(error)
|
||||
|
@ -187,7 +168,7 @@ module.exports = RecurlyWrapper = {
|
|||
) {
|
||||
if (err) {
|
||||
logger.warn(
|
||||
{ err, user_id: user._id, accountCode, recurly_token_id },
|
||||
{ err, user_id: user._id, accountCode },
|
||||
'error creating billing info'
|
||||
)
|
||||
return next(err)
|
||||
|
@ -201,12 +182,8 @@ module.exports = RecurlyWrapper = {
|
|||
|
||||
setAddress(cache, next) {
|
||||
const { user } = cache
|
||||
const { recurly_token_id } = cache
|
||||
const { subscriptionDetails } = cache
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
'setting billing address in recurly'
|
||||
)
|
||||
logger.log({ user_id: user._id }, 'setting billing address in recurly')
|
||||
const accountCode = __guard__(
|
||||
cache != null ? cache.account : undefined,
|
||||
x1 => x1.account_code
|
||||
|
@ -239,7 +216,7 @@ module.exports = RecurlyWrapper = {
|
|||
(error, response, responseBody) => {
|
||||
if (error) {
|
||||
logger.warn(
|
||||
{ error, user_id: user._id, recurly_token_id },
|
||||
{ error, user_id: user._id },
|
||||
'error response from recurly while setting address'
|
||||
)
|
||||
return next(error)
|
||||
|
@ -250,7 +227,7 @@ module.exports = RecurlyWrapper = {
|
|||
) {
|
||||
if (err) {
|
||||
logger.warn(
|
||||
{ err, user_id: user._id, recurly_token_id },
|
||||
{ err, user_id: user._id },
|
||||
'error updating billing info'
|
||||
)
|
||||
return next(err)
|
||||
|
@ -263,12 +240,8 @@ module.exports = RecurlyWrapper = {
|
|||
},
|
||||
createSubscription(cache, next) {
|
||||
const { user } = cache
|
||||
const { recurly_token_id } = cache
|
||||
const { subscriptionDetails } = cache
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
'creating subscription in recurly'
|
||||
)
|
||||
logger.log({ user_id: user._id }, 'creating subscription in recurly')
|
||||
const data = {
|
||||
plan_code: subscriptionDetails.plan_code,
|
||||
currency: subscriptionDetails.currencyCode,
|
||||
|
@ -288,7 +261,7 @@ module.exports = RecurlyWrapper = {
|
|||
(error, response, responseBody) => {
|
||||
if (error) {
|
||||
logger.warn(
|
||||
{ error, user_id: user._id, recurly_token_id },
|
||||
{ error, user_id: user._id },
|
||||
'error response from recurly while creating subscription'
|
||||
)
|
||||
return next(error)
|
||||
|
@ -299,7 +272,7 @@ module.exports = RecurlyWrapper = {
|
|||
) {
|
||||
if (err) {
|
||||
logger.warn(
|
||||
{ err, user_id: user._id, recurly_token_id },
|
||||
{ err, user_id: user._id },
|
||||
'error creating subscription'
|
||||
)
|
||||
return next(err)
|
||||
|
@ -315,17 +288,17 @@ module.exports = RecurlyWrapper = {
|
|||
_createPaypalSubscription(
|
||||
user,
|
||||
subscriptionDetails,
|
||||
recurly_token_id,
|
||||
recurlyTokenIds,
|
||||
callback
|
||||
) {
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
{ user_id: user._id },
|
||||
'starting process of creating paypal subscription'
|
||||
)
|
||||
// We use `async.waterfall` to run each of these actions in sequence
|
||||
// passing a `cache` object along the way. The cache is initialized
|
||||
// with required data, and `async.apply` to pass the cache to the first function
|
||||
const cache = { user, recurly_token_id, subscriptionDetails }
|
||||
const cache = { user, recurlyTokenIds, subscriptionDetails }
|
||||
return Async.waterfall(
|
||||
[
|
||||
Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache),
|
||||
|
@ -337,7 +310,7 @@ module.exports = RecurlyWrapper = {
|
|||
function(err, result) {
|
||||
if (err) {
|
||||
logger.warn(
|
||||
{ err, user_id: user._id, recurly_token_id },
|
||||
{ err, user_id: user._id },
|
||||
'error in paypal subscription creation process'
|
||||
)
|
||||
return callback(err)
|
||||
|
@ -345,13 +318,13 @@ module.exports = RecurlyWrapper = {
|
|||
if (!result.subscription) {
|
||||
err = new Error('no subscription object in result')
|
||||
logger.warn(
|
||||
{ err, user_id: user._id, recurly_token_id },
|
||||
{ err, user_id: user._id },
|
||||
'error in paypal subscription creation process'
|
||||
)
|
||||
return callback(err)
|
||||
}
|
||||
logger.log(
|
||||
{ user_id: user._id, recurly_token_id },
|
||||
{ user_id: user._id },
|
||||
'done creating paypal subscription for user'
|
||||
)
|
||||
return callback(null, result.subscription)
|
||||
|
@ -362,7 +335,7 @@ module.exports = RecurlyWrapper = {
|
|||
_createCreditCardSubscription(
|
||||
user,
|
||||
subscriptionDetails,
|
||||
recurly_token_id,
|
||||
recurlyTokenIds,
|
||||
callback
|
||||
) {
|
||||
const data = {
|
||||
|
@ -375,37 +348,47 @@ module.exports = RecurlyWrapper = {
|
|||
first_name: subscriptionDetails.first_name || user.first_name,
|
||||
last_name: subscriptionDetails.last_name || user.last_name,
|
||||
billing_info: {
|
||||
token_id: recurly_token_id
|
||||
token_id: recurlyTokenIds.billing
|
||||
}
|
||||
}
|
||||
}
|
||||
if (recurlyTokenIds.threeDSecureActionResult) {
|
||||
data.account.billing_info.three_d_secure_action_result_token_id =
|
||||
recurlyTokenIds.threeDSecureActionResult
|
||||
}
|
||||
const requestBody = RecurlyWrapper._buildXml('subscription', data)
|
||||
|
||||
return RecurlyWrapper.apiRequest(
|
||||
{
|
||||
url: 'subscriptions',
|
||||
method: 'POST',
|
||||
body: requestBody
|
||||
body: requestBody,
|
||||
expect422: true
|
||||
},
|
||||
(error, response, responseBody) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return RecurlyWrapper._parseSubscriptionXml(responseBody, callback)
|
||||
|
||||
if (response.statusCode === 422) {
|
||||
RecurlyWrapper._handle422Response(responseBody, callback)
|
||||
} else {
|
||||
RecurlyWrapper._parseSubscriptionXml(responseBody, callback)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
createSubscription(user, subscriptionDetails, recurly_token_id, callback) {
|
||||
createSubscription(user, subscriptionDetails, recurlyTokenIds, callback) {
|
||||
const { isPaypal } = subscriptionDetails
|
||||
logger.log(
|
||||
{ user_id: user._id, isPaypal, recurly_token_id },
|
||||
{ user_id: user._id, isPaypal },
|
||||
'setting up subscription in recurly'
|
||||
)
|
||||
const fn = isPaypal
|
||||
? RecurlyWrapper._createPaypalSubscription
|
||||
: RecurlyWrapper._createCreditCardSubscription
|
||||
return fn(user, subscriptionDetails, recurly_token_id, callback)
|
||||
return fn(user, subscriptionDetails, recurlyTokenIds, callback)
|
||||
},
|
||||
|
||||
apiRequest(options, callback) {
|
||||
|
@ -418,15 +401,17 @@ module.exports = RecurlyWrapper = {
|
|||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
'X-Api-Version': Settings.apis.recurly.apiVersion
|
||||
}
|
||||
const { expect404 } = options
|
||||
const { expect404, expect422 } = options
|
||||
delete options.expect404
|
||||
delete options.expect422
|
||||
return request(options, function(error, response, body) {
|
||||
if (
|
||||
error == null &&
|
||||
response.statusCode !== 200 &&
|
||||
response.statusCode !== 201 &&
|
||||
response.statusCode !== 204 &&
|
||||
(response.statusCode !== 404 || !expect404)
|
||||
(response.statusCode !== 404 || !expect404) &&
|
||||
(response.statusCode !== 422 || !expect422)
|
||||
) {
|
||||
logger.warn(
|
||||
{
|
||||
|
@ -445,6 +430,12 @@ module.exports = RecurlyWrapper = {
|
|||
'got 404 response from recurly, expected as valid response'
|
||||
)
|
||||
}
|
||||
if (response.statusCode === 422 && expect422) {
|
||||
logger.log(
|
||||
{ url: options.url, method: options.method },
|
||||
'got 422 response from recurly, expected as valid response'
|
||||
)
|
||||
}
|
||||
return callback(error, response, body)
|
||||
})
|
||||
},
|
||||
|
@ -846,6 +837,43 @@ module.exports = RecurlyWrapper = {
|
|||
)
|
||||
},
|
||||
|
||||
_handle422Response(body, callback) {
|
||||
RecurlyWrapper._parseErrorsXml(body, (error, data) => {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
|
||||
let errorData = {}
|
||||
if (data.transaction_error) {
|
||||
errorData = {
|
||||
message: data.transaction_error.merchant_message,
|
||||
info: {
|
||||
category: data.transaction_error.error_category,
|
||||
gatewayCode: data.transaction_error.gateway_error_code,
|
||||
public: {
|
||||
code: data.transaction_error.error_code,
|
||||
message: data.transaction_error.customer_message
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.transaction_error.three_d_secure_action_token_id) {
|
||||
errorData.info.public.threeDSecureActionTokenId =
|
||||
data.transaction_error.three_d_secure_action_token_id
|
||||
}
|
||||
} else if (data.error && data.error._) {
|
||||
// fallback for errors that don't have a `transaction_error` field, but
|
||||
// instead a `error` field with a message (e.g. VATMOSS errors)
|
||||
errorData = {
|
||||
info: {
|
||||
public: {
|
||||
message: data.error._
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(new SubscriptionErrors.RecurlyTransactionError(errorData))
|
||||
})
|
||||
},
|
||||
_parseSubscriptionsXml(xml, callback) {
|
||||
return RecurlyWrapper._parseXmlAndGetAttribute(
|
||||
xml,
|
||||
|
@ -882,6 +910,10 @@ module.exports = RecurlyWrapper = {
|
|||
return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'coupon', callback)
|
||||
},
|
||||
|
||||
_parseErrorsXml(xml, callback) {
|
||||
return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'errors', callback)
|
||||
},
|
||||
|
||||
_parseXmlAndGetAttribute(xml, attribute, callback) {
|
||||
return RecurlyWrapper._parseXml(xml, function(error, data) {
|
||||
if (error != null) {
|
||||
|
|
|
@ -29,6 +29,8 @@ const FeaturesUpdater = require('./FeaturesUpdater')
|
|||
const planFeatures = require('./planFeatures')
|
||||
const GroupPlansData = require('./GroupPlansData')
|
||||
const V1SubscriptionManager = require('./V1SubscriptionManager')
|
||||
const SubscriptionErrors = require('./Errors')
|
||||
const HttpErrors = require('@overleaf/o-error/http')
|
||||
|
||||
module.exports = SubscriptionController = {
|
||||
plansPage(req, res, next) {
|
||||
|
@ -199,10 +201,14 @@ module.exports = SubscriptionController = {
|
|||
|
||||
createSubscription(req, res, next) {
|
||||
const user = AuthenticationController.getSessionUser(req)
|
||||
const { recurly_token_id } = req.body
|
||||
const recurlyTokenIds = {
|
||||
billing: req.body.recurly_token_id,
|
||||
threeDSecureActionResult:
|
||||
req.body.recurly_three_d_secure_action_result_token_id
|
||||
}
|
||||
const { subscriptionDetails } = req.body
|
||||
logger.log(
|
||||
{ recurly_token_id, user_id: user._id, subscriptionDetails },
|
||||
{ user_id: user._id, subscriptionDetails },
|
||||
'creating subscription'
|
||||
)
|
||||
|
||||
|
@ -220,16 +226,23 @@ module.exports = SubscriptionController = {
|
|||
return SubscriptionHandler.createSubscription(
|
||||
user,
|
||||
subscriptionDetails,
|
||||
recurly_token_id,
|
||||
recurlyTokenIds,
|
||||
function(err) {
|
||||
if (err != null) {
|
||||
logger.warn(
|
||||
{ err, user_id: user._id },
|
||||
'something went wrong creating subscription'
|
||||
)
|
||||
return next(err)
|
||||
if (!err) {
|
||||
return res.sendStatus(201)
|
||||
}
|
||||
return res.sendStatus(201)
|
||||
|
||||
if (err instanceof SubscriptionErrors.RecurlyTransactionError) {
|
||||
return next(
|
||||
new HttpErrors.UnprocessableEntityError({}).withCause(err)
|
||||
)
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
{ err, user_id: user._id },
|
||||
'something went wrong creating subscription'
|
||||
)
|
||||
next(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
@ -56,7 +56,7 @@ module.exports = {
|
|||
})
|
||||
},
|
||||
|
||||
createSubscription(user, subscriptionDetails, recurly_token_id, callback) {
|
||||
createSubscription(user, subscriptionDetails, recurlyTokenIds, callback) {
|
||||
const self = this
|
||||
const clientTokenId = ''
|
||||
return this.validateNoSubscriptionInRecurly(user._id, function(
|
||||
|
@ -72,7 +72,7 @@ module.exports = {
|
|||
return RecurlyWrapper.createSubscription(
|
||||
user,
|
||||
subscriptionDetails,
|
||||
recurly_token_id,
|
||||
recurlyTokenIds,
|
||||
function(error, recurlySubscription) {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
|
|
|
@ -15,7 +15,7 @@ block content
|
|||
.container(ng-controller="NewSubscriptionController" ng-cloak)
|
||||
.row.card-group
|
||||
.col-md-5.col-md-push-4
|
||||
.card.card-highlighted.card-border
|
||||
.card.card-highlighted.card-border(ng-hide="threeDSecureFlow")
|
||||
.alert.alert-danger(ng-show="recurlyLoadError")
|
||||
strong #{translate('payment_provider_unreachable_error')}
|
||||
.page-header(ng-hide="recurlyLoadError")
|
||||
|
@ -212,6 +212,12 @@ block content
|
|||
| {{ monthlyBilling ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_now")}'}}
|
||||
span(ng-if="paymentMethod.value !== 'credit_card'") #{translate("upgrade_paypal_btn")}
|
||||
|
||||
div.three-d-secure-container.card.card-highlighted.card-border(ng-show="threeDSecureFlow")
|
||||
.alert.alert-info.small(aria-live="assertive")
|
||||
strong #{translate('card_must_be_authenticated_by_3dsecure')}
|
||||
div.three-d-secure-recurly-container
|
||||
|
||||
|
||||
|
||||
.col-md-3.col-md-pull-4
|
||||
if showStudentPlan == 'true'
|
||||
|
|
|
@ -64,6 +64,14 @@ define(['base', 'directives/creditCards'], App =>
|
|||
|
||||
$scope.processing = false
|
||||
|
||||
$scope.threeDSecureFlow = false
|
||||
$scope.threeDSecureContainer = document.querySelector(
|
||||
'.three-d-secure-container'
|
||||
)
|
||||
$scope.threeDSecureRecurlyContainer = document.querySelector(
|
||||
'.three-d-secure-recurly-container'
|
||||
)
|
||||
|
||||
recurly.configure({
|
||||
publicKey: window.recurlyApiKey,
|
||||
style: {
|
||||
|
@ -172,7 +180,17 @@ define(['base', 'directives/creditCards'], App =>
|
|||
$scope.genericError = ''
|
||||
}
|
||||
|
||||
const completeSubscription = function(err, recurly_token_id) {
|
||||
let cachedRecurlyBillingToken
|
||||
const completeSubscription = function(
|
||||
err,
|
||||
recurlyBillingToken,
|
||||
recurly3DSecureResultToken
|
||||
) {
|
||||
if (recurlyBillingToken) {
|
||||
// temporary store the billing token as it might be needed when
|
||||
// re-sending the request after SCA authentication
|
||||
cachedRecurlyBillingToken = recurlyBillingToken
|
||||
}
|
||||
$scope.validation.errorFields = {}
|
||||
if (err != null) {
|
||||
event_tracking.sendMB('subscription-error', err)
|
||||
|
@ -190,7 +208,9 @@ define(['base', 'directives/creditCards'], App =>
|
|||
} else {
|
||||
const postData = {
|
||||
_csrf: window.csrfToken,
|
||||
recurly_token_id: recurly_token_id.id,
|
||||
recurly_token_id: cachedRecurlyBillingToken.id,
|
||||
recurly_three_d_secure_action_result_token_id:
|
||||
recurly3DSecureResultToken && recurly3DSecureResultToken.id,
|
||||
subscriptionDetails: {
|
||||
currencyCode: pricing.items.currency,
|
||||
plan_code: pricing.items.plan.code,
|
||||
|
@ -232,9 +252,16 @@ define(['base', 'directives/creditCards'], App =>
|
|||
)
|
||||
window.location.href = '/user/subscription/thank-you'
|
||||
})
|
||||
.catch(function() {
|
||||
.catch(response => {
|
||||
$scope.processing = false
|
||||
$scope.genericError = 'Something went wrong processing the request'
|
||||
const { data } = response
|
||||
$scope.genericError =
|
||||
(data && data.message) ||
|
||||
'Something went wrong processing the request'
|
||||
|
||||
if (data.threeDSecureActionTokenId) {
|
||||
initThreeDSecure(data.threeDSecureActionTokenId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -249,6 +276,44 @@ define(['base', 'directives/creditCards'], App =>
|
|||
}
|
||||
}
|
||||
|
||||
const initThreeDSecure = function(threeDSecureActionTokenId) {
|
||||
// instanciate and configure Recurly 3DSecure flow
|
||||
const risk = recurly.Risk()
|
||||
const threeDSecure = risk.ThreeDSecure({
|
||||
actionTokenId: threeDSecureActionTokenId
|
||||
})
|
||||
|
||||
// on SCA verification error: show payment UI with the error message
|
||||
threeDSecure.on('error', error => {
|
||||
$scope.genericError = `Error: ${error.message}`
|
||||
$scope.threeDSecureFlow = false
|
||||
$scope.$apply()
|
||||
})
|
||||
|
||||
// on SCA verification success: show payment UI in processing mode and
|
||||
// resubmit the payment with the new token final success or error will be
|
||||
// handled by `completeSubscription`
|
||||
threeDSecure.on('token', recurly3DSecureResultToken => {
|
||||
completeSubscription(null, null, recurly3DSecureResultToken)
|
||||
$scope.genericError = null
|
||||
$scope.threeDSecureFlow = false
|
||||
$scope.processing = true
|
||||
$scope.$apply()
|
||||
})
|
||||
|
||||
// make sure the threeDSecureRecurlyContainer is empty (in case of
|
||||
// retries) and show 3DSecure UI
|
||||
$scope.threeDSecureRecurlyContainer.innerHTML = ''
|
||||
$scope.threeDSecureFlow = true
|
||||
threeDSecure.attach($scope.threeDSecureRecurlyContainer)
|
||||
|
||||
// scroll the UI into view (timeout needed to make sure the element is
|
||||
// visible)
|
||||
window.setTimeout(() => {
|
||||
$scope.threeDSecureContainer.scrollIntoView()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
$scope.countries = [
|
||||
{ code: 'AF', name: 'Afghanistan' },
|
||||
{ code: 'AL', name: 'Albania' },
|
||||
|
|
|
@ -80,3 +80,13 @@
|
|||
.capitalised {
|
||||
text-transform:capitalize;
|
||||
}
|
||||
|
||||
.three-d-secure-container {
|
||||
> .three-d-secure-recurly-container {
|
||||
height: 400px;
|
||||
|
||||
> div[data-recurly="three-d-secure-container"] {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ const querystring = require('querystring')
|
|||
const modulePath = '../../../../app/src/Features/Subscription/RecurlyWrapper'
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const tk = require('timekeeper')
|
||||
const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
|
||||
|
||||
const fixtures = {
|
||||
'subscriptions/44f83d7cba354d5b84812419f923ea96':
|
||||
|
@ -156,7 +157,8 @@ describe('RecurlyWrapper', function() {
|
|||
log: sinon.stub()
|
||||
},
|
||||
request: sinon.stub(),
|
||||
xml2js: require('xml2js')
|
||||
xml2js: require('xml2js'),
|
||||
'./Errors': SubscriptionErrors
|
||||
}
|
||||
}
|
||||
))
|
||||
|
@ -498,12 +500,15 @@ describe('RecurlyWrapper', function() {
|
|||
}
|
||||
}
|
||||
this.subscription = {}
|
||||
this.recurly_token_id = 'a-token-id'
|
||||
this.recurlyTokenIds = {
|
||||
billing: 'a-token-id',
|
||||
threeDSecureActionResult: 'a-3d-token-id'
|
||||
}
|
||||
return (this.call = callback => {
|
||||
return this.RecurlyWrapper.createSubscription(
|
||||
this.user,
|
||||
this.subscriptionDetails,
|
||||
this.recurly_token_id,
|
||||
this.recurlyTokenIds,
|
||||
callback
|
||||
)
|
||||
})
|
||||
|
@ -647,7 +652,10 @@ describe('RecurlyWrapper', function() {
|
|||
}
|
||||
}
|
||||
this.subscription = {}
|
||||
this.recurly_token_id = 'a-token-id'
|
||||
this.recurlyTokenIds = {
|
||||
billing: 'a-token-id',
|
||||
threeDSecureActionResult: 'a-3d-token-id'
|
||||
}
|
||||
this.apiRequest = sinon.stub(this.RecurlyWrapper, 'apiRequest')
|
||||
this.response = { statusCode: 200 }
|
||||
this.body = '<xml>is_bad</xml>'
|
||||
|
@ -661,7 +669,7 @@ describe('RecurlyWrapper', function() {
|
|||
return this.RecurlyWrapper._createCreditCardSubscription(
|
||||
this.user,
|
||||
this.subscriptionDetails,
|
||||
this.recurly_token_id,
|
||||
this.recurlyTokenIds,
|
||||
callback
|
||||
)
|
||||
})
|
||||
|
@ -687,6 +695,7 @@ describe('RecurlyWrapper', function() {
|
|||
<last_name>Johnson</last_name>
|
||||
<billing_info>
|
||||
<token_id>a-token-id</token_id>
|
||||
<three_d_secure_action_result_token_id>a-3d-token-id</three_d_secure_action_result_token_id>
|
||||
</billing_info>
|
||||
</account>
|
||||
</subscription>\
|
||||
|
@ -724,6 +733,41 @@ describe('RecurlyWrapper', function() {
|
|||
})
|
||||
})
|
||||
|
||||
describe('when api request returns 422', function() {
|
||||
beforeEach(function() {
|
||||
const body = `\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<errors>
|
||||
<transaction_error>
|
||||
<error_code>three_d_secure_action_required</error_code>
|
||||
<error_category>3d_secure_action_required</error_category>
|
||||
<merchant_message>Your payment gateway is requesting that the transaction be completed with 3D Secure in accordance with PSD2.</merchant_message>
|
||||
<customer_message>Your card must be authenticated with 3D Secure before continuing.</customer_message>
|
||||
<gateway_error_code nil="nil"></gateway_error_code>
|
||||
<three_d_secure_action_token_id>mock_three_d_secure_action_token</three_d_secure_action_token_id>
|
||||
</transaction_error>
|
||||
<error field="subscription.account.base" symbol="three_d_secure_action_required">Your card must be authenticated with 3D Secure before continuing.</error>
|
||||
</errors>
|
||||
`
|
||||
this.apiRequest.yields(null, { statusCode: 422 }, body)
|
||||
})
|
||||
|
||||
it('should produce an error', function(done) {
|
||||
return this.call((err, sub) => {
|
||||
expect(err).to.be.instanceof(
|
||||
SubscriptionErrors.RecurlyTransactionError
|
||||
)
|
||||
expect(err.info.public.message).to.be.equal(
|
||||
'Your card must be authenticated with 3D Secure before continuing.'
|
||||
)
|
||||
expect(err.info.public.threeDSecureActionTokenId).to.be.equal(
|
||||
'mock_three_d_secure_action_token'
|
||||
)
|
||||
return done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when api request produces an error', function() {
|
||||
beforeEach(function() {
|
||||
return this.apiRequest.callsArgWith(1, new Error('woops'))
|
||||
|
@ -802,31 +846,34 @@ describe('RecurlyWrapper', function() {
|
|||
}
|
||||
}
|
||||
this.subscription = {}
|
||||
this.recurly_token_id = 'a-token-id'
|
||||
this.recurlyTokenIds = {
|
||||
billing: 'a-token-id',
|
||||
threeDSecureActionResult: 'a-3d-token-id'
|
||||
}
|
||||
|
||||
// set up data callbacks
|
||||
const { user } = this
|
||||
const { subscriptionDetails } = this
|
||||
const { recurly_token_id } = this
|
||||
const { recurlyTokenIds } = this
|
||||
|
||||
this.checkAccountExists.callsArgWith(1, null, {
|
||||
user,
|
||||
subscriptionDetails,
|
||||
recurly_token_id,
|
||||
recurlyTokenIds,
|
||||
userExists: false,
|
||||
account: { accountCode: 'xx' }
|
||||
})
|
||||
this.createAccount.callsArgWith(1, null, {
|
||||
user,
|
||||
subscriptionDetails,
|
||||
recurly_token_id,
|
||||
recurlyTokenIds,
|
||||
userExists: false,
|
||||
account: { accountCode: 'xx' }
|
||||
})
|
||||
this.createBillingInfo.callsArgWith(1, null, {
|
||||
user,
|
||||
subscriptionDetails,
|
||||
recurly_token_id,
|
||||
recurlyTokenIds,
|
||||
userExists: false,
|
||||
account: { accountCode: 'xx' },
|
||||
billingInfo: { token_id: 'abc' }
|
||||
|
@ -834,7 +881,7 @@ describe('RecurlyWrapper', function() {
|
|||
this.setAddress.callsArgWith(1, null, {
|
||||
user,
|
||||
subscriptionDetails,
|
||||
recurly_token_id,
|
||||
recurlyTokenIds,
|
||||
userExists: false,
|
||||
account: { accountCode: 'xx' },
|
||||
billingInfo: { token_id: 'abc' }
|
||||
|
@ -842,7 +889,7 @@ describe('RecurlyWrapper', function() {
|
|||
this.createSubscription.callsArgWith(1, null, {
|
||||
user,
|
||||
subscriptionDetails,
|
||||
recurly_token_id,
|
||||
recurlyTokenIds,
|
||||
userExists: false,
|
||||
account: { accountCode: 'xx' },
|
||||
billingInfo: { token_id: 'abc' },
|
||||
|
@ -853,7 +900,7 @@ describe('RecurlyWrapper', function() {
|
|||
return this.RecurlyWrapper._createPaypalSubscription(
|
||||
this.user,
|
||||
this.subscriptionDetails,
|
||||
this.recurly_token_id,
|
||||
this.recurlyTokenIds,
|
||||
callback
|
||||
)
|
||||
})
|
||||
|
@ -937,7 +984,10 @@ describe('RecurlyWrapper', function() {
|
|||
first_name: 'Foo',
|
||||
last_name: 'Bar'
|
||||
}),
|
||||
recurly_token_id: (this.recurly_token_id = 'some_token'),
|
||||
recurlyTokenIds: (this.recurlyTokenIds = {
|
||||
billing: 'a-token-id',
|
||||
threeDSecureActionResult: 'a-3d-token-id'
|
||||
}),
|
||||
subscriptionDetails: (this.subscriptionDetails = {
|
||||
currencyCode: 'EUR',
|
||||
plan_code: 'some_plan_code',
|
||||
|
@ -1255,7 +1305,7 @@ describe('RecurlyWrapper', function() {
|
|||
const { body } = this.apiRequest.lastCall.args[0]
|
||||
expect(body).to.equal(`\
|
||||
<billing_info>
|
||||
<token_id>some_token</token_id>
|
||||
<token_id>a-token-id</token_id>
|
||||
</billing_info>\
|
||||
`)
|
||||
return done()
|
||||
|
|
|
@ -19,6 +19,9 @@ const MockRequest = require('../helpers/MockRequest')
|
|||
const MockResponse = require('../helpers/MockResponse')
|
||||
const modulePath =
|
||||
'../../../../app/src/Features/Subscription/SubscriptionController'
|
||||
const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const HttpErrors = require('@overleaf/o-error/http')
|
||||
|
||||
const mockSubscriptions = {
|
||||
'subscription-123-active': {
|
||||
|
@ -89,7 +92,9 @@ describe('SubscriptionController', function() {
|
|||
gaExperiments: {}
|
||||
}
|
||||
this.GeoIpLookup = { getCurrencyCode: sinon.stub() }
|
||||
this.UserGetter = { getUser: sinon.stub().callsArgWith(2, null, this.user) }
|
||||
this.UserGetter = {
|
||||
getUser: sinon.stub().callsArgWith(2, null, this.user)
|
||||
}
|
||||
this.SubscriptionController = SandboxedModule.require(modulePath, {
|
||||
globals: {
|
||||
console: console
|
||||
|
@ -111,7 +116,9 @@ describe('SubscriptionController', function() {
|
|||
'./RecurlyWrapper': (this.RecurlyWrapper = {}),
|
||||
'./FeaturesUpdater': (this.FeaturesUpdater = {}),
|
||||
'./GroupPlansData': (this.GroupPlansData = {}),
|
||||
'./V1SubscriptionManager': (this.V1SubscriptionManager = {})
|
||||
'./V1SubscriptionManager': (this.V1SubscriptionManager = {}),
|
||||
'./Errors': SubscriptionErrors,
|
||||
'@overleaf/o-error/http': HttpErrors
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -378,7 +385,12 @@ describe('SubscriptionController', function() {
|
|||
card: '1234',
|
||||
cvv: '123'
|
||||
}
|
||||
this.req.body.recurly_token_id = '1234'
|
||||
this.recurlyTokenIds = {
|
||||
billing: '1234',
|
||||
threeDSecureActionResult: '5678'
|
||||
}
|
||||
this.req.body.recurly_token_id = this.recurlyTokenIds.billing
|
||||
this.req.body.recurly_three_d_secure_action_result_token_id = this.recurlyTokenIds.threeDSecureActionResult
|
||||
this.req.body.subscriptionDetails = this.subscriptionDetails
|
||||
this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
|
||||
return this.SubscriptionController.createSubscription(this.req, this.res)
|
||||
|
@ -386,10 +398,10 @@ describe('SubscriptionController', function() {
|
|||
|
||||
it('should send the user and subscriptionId to the handler', function(done) {
|
||||
this.SubscriptionHandler.createSubscription
|
||||
.calledWith(
|
||||
.calledWithMatch(
|
||||
this.user,
|
||||
this.subscriptionDetails,
|
||||
this.req.body.recurly_token_id
|
||||
this.recurlyTokenIds
|
||||
)
|
||||
.should.equal(true)
|
||||
return done()
|
||||
|
@ -401,6 +413,27 @@ describe('SubscriptionController', function() {
|
|||
})
|
||||
})
|
||||
|
||||
describe('createSubscription with errors', function() {
|
||||
it('should handle 3DSecure errors', function(done) {
|
||||
this.next = sinon.stub()
|
||||
this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
|
||||
this.SubscriptionHandler.createSubscription.yields(
|
||||
new SubscriptionErrors.RecurlyTransactionError({})
|
||||
)
|
||||
this.SubscriptionController.createSubscription(this.req, null, error => {
|
||||
expect(error).to.exist
|
||||
expect(error).to.be.instanceof(HttpErrors.UnprocessableEntityError)
|
||||
expect(
|
||||
OError.hasCauseInstanceOf(
|
||||
error,
|
||||
SubscriptionErrors.RecurlyTransactionError
|
||||
)
|
||||
).to.be.true
|
||||
})
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSubscription via post', function() {
|
||||
beforeEach(function(done) {
|
||||
this.res = {
|
||||
|
|
|
@ -118,7 +118,7 @@ describe('SubscriptionHandler', function() {
|
|||
cvv: '123',
|
||||
number: '12345'
|
||||
}
|
||||
this.recurly_token_id = '45555666'
|
||||
this.recurlyTokenIds = { billing: '45555666' }
|
||||
return (this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
|
||||
.stub()
|
||||
.yields(null, true))
|
||||
|
@ -129,18 +129,14 @@ describe('SubscriptionHandler', function() {
|
|||
return this.SubscriptionHandler.createSubscription(
|
||||
this.user,
|
||||
this.subscriptionDetails,
|
||||
this.recurly_token_id,
|
||||
this.recurlyTokenIds,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should create the subscription with the wrapper', function() {
|
||||
return this.RecurlyWrapper.createSubscription
|
||||
.calledWith(
|
||||
this.user,
|
||||
this.subscriptionDetails,
|
||||
this.recurly_token_id
|
||||
)
|
||||
.calledWith(this.user, this.subscriptionDetails, this.recurlyTokenIds)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
|
@ -163,7 +159,7 @@ describe('SubscriptionHandler', function() {
|
|||
return this.SubscriptionHandler.createSubscription(
|
||||
this.user,
|
||||
this.subscriptionDetails,
|
||||
this.recurly_token_id,
|
||||
this.recurlyTokenIds,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue