3D Secure Valuting

Stephen Paul Weber created

Change summary

config.ru                  | 37 +++++++++++++++++++++++++++++--
lib/three_d_secure_repo.rb | 46 ++++++++++++++++++++++++++++++++++++++++
views/credit_cards.slim    | 27 +++++++++++++++++------
3 files changed, 100 insertions(+), 10 deletions(-)

Detailed changes

config.ru 🔗

@@ -16,6 +16,7 @@ if ENV["RACK_ENV"] == "development"
 end
 
 require_relative "lib/auto_top_up_repo"
+require_relative "lib/three_d_secure_repo"
 require_relative "lib/electrum"
 
 require "sentry-ruby"
@@ -35,6 +36,17 @@ DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
 DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
 
 class CreditCardGateway
+	class ErrorResult < StandardError
+		def self.for(result)
+			if result.verification&.status == "gateway_rejected" &&
+			   result.verification&.gateway_rejection_reason == "cvv"
+				new("fieldInvalidForCvv")
+			else
+				new(result.message)
+			end
+		end
+	end
+
 	def initialize(jid, customer_id=nil)
 		@jid = jid
 		@customer_id = customer_id
@@ -86,14 +98,22 @@ class CreditCardGateway
 		!@gateway.customer.find(customer_id).payment_methods.empty?
 	end
 
-	def default_payment_method=(nonce)
-		@gateway.payment_method.create(
+	def default_method(nonce)
+		result = @gateway.payment_method.create(
 			customer_id: customer_id,
 			payment_method_nonce: nonce,
 			options: {
+				verify_card: true,
 				make_default: true
 			}
 		)
+		raise ErrorResult.for(result) unless result.success?
+
+		result
+	end
+
+	def remove_method(token)
+		@gateway.payment_method.delete(token)
 	end
 
 protected
@@ -210,12 +230,23 @@ class JmpPay < Roda
 				end
 
 				r.post do
-					gateway.default_payment_method = params["braintree_nonce"]
+					result = gateway.default_method(params["braintree_nonce"])
+					ThreeDSecureRepo.new.put_from_payment_method(
+						gateway.customer_id,
+						result.payment_method
+					)
 					topup.put(
 						gateway.customer_id,
 						params["auto_top_up_amount"].to_i
 					)
 					"OK"
+				rescue ThreeDSecureRepo::Failed
+					gateway.remove_method($!.message)
+					response.status = 400
+					"hostedFieldsFieldsInvalidError"
+				rescue CreditCardGateway::ErrorResult
+					response.status = 400
+					$!.message
 				end
 			end
 		end

lib/three_d_secure_repo.rb 🔗

@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class ThreeDSecureRepo
+	class Failed < StandardError; end
+
+	def initialize(redis: REDIS)
+		@redis = redis
+	end
+
+	def find(customer_id, token)
+		redis(:hget, customer_id, token)
+	end
+
+	def put(customer_id, token, authid)
+		if !authid || authid.empty?
+			redis(:hdel, customer_id, token)
+		else
+			redis(:hset, customer_id, token, authid)
+		end
+	end
+
+	def put_from_payment_method(customer_id, method)
+		return unless method.verification # Already vaulted
+
+		three_d = method.verification.three_d_secure_info
+		if !three_d ||
+		   (three_d.liability_shift_possible && !three_d.liability_shifted)
+			raise Failed, method.token
+		end
+
+		put(
+			customer_id, method.token,
+			three_d.three_d_secure_authentication_id
+		)
+	end
+
+protected
+
+	def redis(action, customer_id, *args)
+		@redis.public_send(
+			action,
+			"jmp_customer_three_d_secure_authentication_id-#{customer_id}",
+			*args
+		)
+	end
+end

views/credit_cards.slim 🔗

@@ -34,7 +34,7 @@ form method="post" action=""
 	input type="hidden" name="customer_id" value=customer_id
 	input type="hidden" name="braintree_nonce"
 
-script src="https://js.braintreegateway.com/web/dropin/1.26.0/js/dropin.min.js"
+script src="https://js.braintreegateway.com/web/dropin/1.33.0/js/dropin.js"
 javascript:
 	document.querySelector("#braintree").innerHTML = "";
 
@@ -44,7 +44,9 @@ javascript:
 	braintree.dropin.create({
 		authorization: #{{token.to_json}},
 		container: "#braintree",
+		card: { vault: { vaultCard: false } },
 		vaultManager: true,
+		threeDSecure: true,
 		translations: {
 			payWithCard: "Add a Card",
 			payingWith: "Default payment source",
@@ -56,14 +58,17 @@ javascript:
 		document.querySelector("form").addEventListener("submit", function(e) {
 			e.preventDefault();
 			instance._mainView.hideSheetError();
-			instance._mainView.showLoadingIndicator();
 
-			instance.requestPaymentMethod(function(err, payload) {
+			instance.requestPaymentMethod({
+				threeDSecure: {
+					amount: "0.0",
+					requireChallenge: true
+				}
+			}, function(err, payload) {
 				if(err) {
 					console.log(err);
-					instance._mainView.hideLoadingIndicator();
-					instance._mainView.showSheetError();
 				} else {
+					instance._mainView.showLoadingIndicator();
 					e.target.braintree_nonce.value = payload.nonce;
 					fetch("", {
 						"method": "POST",
@@ -76,8 +81,16 @@ javascript:
 						}
 					}).catch(function(err) {
 							console.log(err);
-							instance._mainView.hideLoadingIndicator();
-							instance._mainView.showSheetError();
+							err.text().then(function(msg) {
+								instance._mainView.hideLoadingIndicator();
+								instance.clearSelectedPaymentMethod();
+								instance._mainView.showSheetError(msg);
+							});
+					}).catch(function(err) {
+						console.log(err);
+						instance._mainView.hideLoadingIndicator();
+						instance.clearSelectedPaymentMethod();
+						instance._mainView.showSheetError();
 					});
 				}
 			});