Skip to content

Commit

Permalink
Upgrade & Add 3ds2 redirect/native (#192)
Browse files Browse the repository at this point in the history
* Upgrade & add 3ds2 redirect/native

* Upgrade Adyen Drop-in to 5.53.2

* Fix Giving integration-example

* Use util class to set session
  • Loading branch information
Kwok-he-Chu authored Dec 20, 2023
1 parent 6927a5f commit ea74a69
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public ResponseEntity<PaymentResponse> payments(@RequestHeader String host, @Req
var orderRef = UUID.randomUUID().toString();
var amount = new Amount()
.currency("EUR")
.value(10000L); // value is 10€ in minor units
.value(10000L); // value is 100€ in minor units

paymentRequest.setMerchantAccount(this.applicationProperty.getMerchantAccount()); // required
paymentRequest.setChannel(PaymentRequest.ChannelEnum.WEB);
Expand All @@ -89,16 +89,27 @@ public ResponseEntity<PaymentResponse> payments(@RequestHeader String host, @Req
new LineItem().quantity(1L).amountIncludingTax(5000L).description("Sunglasses"),
new LineItem().quantity(1L).amountIncludingTax(5000L).description("Headphones"))
);
// required for 3ds2 native flow
paymentRequest.setAdditionalData(Collections.singletonMap("allow3DS2", "true"));
// required for 3ds2 native flow
paymentRequest.setOrigin(request.getScheme() + "://" + host );

var authenticationData = new AuthenticationData();
authenticationData.setAttemptAuthentication(AuthenticationData.AttemptAuthenticationEnum.ALWAYS);
// add the following lines for Native 3DS2:
//var threeDSRequestData = new ThreeDSRequestData();
//threeDSRequestData.setNativeThreeDS(ThreeDSRequestData.NativeThreeDSEnum.PREFERRED);
//authenticationData.setThreeDSRequestData(threeDSRequestData);

paymentRequest.setAuthenticationData(authenticationData);

// required for 3ds2 redirect flow
paymentRequest.setOrigin(request.getScheme() + "://" + host);
// required for 3ds2
paymentRequest.setBrowserInfo(body.getBrowserInfo());
// required by some issuers for 3ds2
paymentRequest.setShopperIP(request.getRemoteAddr());
paymentRequest.setPaymentMethod(body.getPaymentMethod());

// we strongly recommend that you the billingAddress in your request
// card schemes require this for channel web, iOS, and Android implementations
//paymentRequest.setBillingAddress(new Address());
log.info("REST request to make Adyen payment {}", paymentRequest);
var response = paymentsApi.payments(paymentRequest);
return ResponseEntity.ok()
Expand Down Expand Up @@ -133,6 +144,9 @@ public RedirectView redirect(@RequestParam(required = false) String payload, @Re

PaymentCompletionDetails details = new PaymentCompletionDetails();
if (redirectResult != null && !redirectResult.isEmpty()) {
// for redirect, you are redirected to an Adyen domain to complete the 3DS2 challenge
// after completing the 3DS2 challenge, you get the redirect result from Adyen in the returnUrl
// we then pass on the redirectResult
details.redirectResult(redirectResult);
} else if (payload != null && !payload.isEmpty()) {
details.payload(payload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ async function initCheckout() {
holderNameRequired: true,
name: "Credit or debit card",
amount: {
value: 1000,
value: 10000,
currency: "EUR",
},
},
paypal: {
amount: {
value: 1000,
value: 10000,
currency: "USD",
},
environment: "test", // Change this to "live" when you're ready to accept live PayPal payments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
/>

<!-- Adyen JS from TEST environment (change to live for production)-->
<script src="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/5.40.0/adyen.js"
integrity="sha384-ds1t0hgFCe636DXFRL6ciadL2Wb4Yihh27R4JO7d9CF7sFY3NJE4aPCK0EpzaYXD"
<script src="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/5.53.2/adyen.js"
integrity="sha384-ng3HLoZIlQ3BLgyGyGNiwWSx6LEPIlmxVuGRw72skZFt9mL8OweRjp7vcPzSqxTj"
crossorigin="anonymous"></script>

<!-- Adyen CSS from TEST environment (change to live for production)-->
<link rel="stylesheet"
href="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/5.40.0/adyen.css"
integrity="sha384-BRZCzbS8n6hZVj8BESE6thGk0zSkUZfUWxL/vhocKu12k3NZ7xpNsIK39O2aWuni"
crossorigin="anonymous">
href="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/5.53.2/adyen.css"
integrity="sha384-9EdBqZRrjozkt+Be5ycjHBTi+4DYrafpC1KyPnNyTBfjBIZ5+oMp8BbgvPLGgsE0"
crossorigin="anonymous"/>

<link rel="stylesheet" href="/css/application.css" />
</head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ <h2>Cart</h2>
</ul>
</div>
<div class="cart-footer"><span class="cart-footer-label">Total:</span><span
class="cart-footer-amount">10.00</span>
class="cart-footer-amount">100.00</span>
<a th:href="@{/checkout(type=${type})}">
<p class="button">Continue to checkout</p>
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import com.adyen.Client;
import com.adyen.giving.ApplicationProperty;
import com.adyen.enums.Environment;
import com.adyen.giving.util.DonationUtil;
import com.adyen.model.checkout.*;
import com.adyen.service.checkout.PaymentsApi;
import com.adyen.service.exception.ApiException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.ws.rs.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
Expand All @@ -31,10 +32,6 @@ public class CheckoutResource {

private final PaymentsApi paymentsApi;

private static final String DONATION_TOKEN = "DonationToken";

private static final String PAYMENT_ORIGINAL_PSPREFERENCE = "PaymentOriginalPspReference";

public CheckoutResource(ApplicationProperty applicationProperty) {

this.applicationProperty = applicationProperty;
Expand All @@ -58,35 +55,32 @@ public CheckoutResource(ApplicationProperty applicationProperty) {
*/
@PostMapping("/donations")
public ResponseEntity<DonationPaymentResponse> donations(@RequestBody Amount body, @RequestHeader String host, HttpServletRequest request) throws IOException, ApiException {
DonationPaymentRequest donationRequest = new DonationPaymentRequest();
HttpSession session = request.getSession();
var pspReference = session.getAttribute(PAYMENT_ORIGINAL_PSPREFERENCE);
var donationToken = session.getAttribute(DONATION_TOKEN);

if (pspReference == null) {
log.info("Could not find the PspReference in the stored session.");
return ResponseEntity.badRequest().build();
}

if (donationToken == null) {
log.info("Could not find the DonationToken in the stored session.");
return ResponseEntity.badRequest().build();
try {
DonationPaymentRequest donationRequest = new DonationPaymentRequest();

String pspReference = DonationUtil.getPaymentOriginalPspReference(request.getSession());
String donationToken = DonationUtil.getDonationToken(request.getSession());

donationRequest.amount(body);
donationRequest.reference(UUID.randomUUID().toString());
donationRequest.setPaymentMethod(new DonationPaymentMethod(new CardDetails()));
donationRequest.setDonationToken(donationToken);
donationRequest.donationOriginalPspReference(pspReference);
donationRequest.setDonationAccount(this.applicationProperty.getDonationMerchantAccount());
donationRequest.returnUrl(request.getScheme() + "://" + host);
donationRequest.setMerchantAccount(this.applicationProperty.getMerchantAccount());
donationRequest.shopperInteraction(DonationPaymentRequest.ShopperInteractionEnum.CONTAUTH);

DonationPaymentResponse result = this.paymentsApi.donations(donationRequest);

return ResponseEntity.ok().body(result);
} catch (NotFoundException e) {
log.warn(e.getMessage());
return ResponseEntity.notFound().build();
} catch (Exception e) {
log.error(e.getMessage());
return ResponseEntity.status(500).build();
}

donationRequest.amount(body);
donationRequest.reference(UUID.randomUUID().toString());
donationRequest.setPaymentMethod(new CheckoutPaymentMethod(new CardDetails()));
donationRequest.setDonationToken(donationToken.toString());
donationRequest.donationOriginalPspReference(pspReference.toString());
donationRequest.setDonationAccount(this.applicationProperty.getDonationMerchantAccount());
donationRequest.returnUrl(request.getScheme() + "://" + host);
donationRequest.setMerchantAccount(this.applicationProperty.getMerchantAccount());
donationRequest.shopperInteraction(DonationPaymentRequest.ShopperInteractionEnum.CONTAUTH);

DonationPaymentResponse result = this.paymentsApi.donations(donationRequest);

return ResponseEntity.ok()
.body(result);
}

/**
Expand All @@ -98,14 +92,19 @@ public ResponseEntity<DonationPaymentResponse> donations(@RequestBody Amount bod
*/
@PostMapping("/getPaymentMethods")
public ResponseEntity<PaymentMethodsResponse> paymentMethods() throws IOException, ApiException {
var paymentMethodsRequest = new PaymentMethodsRequest();
paymentMethodsRequest.setMerchantAccount(this.applicationProperty.getMerchantAccount());
paymentMethodsRequest.setChannel(PaymentMethodsRequest.ChannelEnum.WEB);

log.info("REST request to get Adyen payment methods {}", paymentMethodsRequest);
var response = paymentsApi.paymentMethods(paymentMethodsRequest);
return ResponseEntity.ok()
.body(response);
try {
var paymentMethodsRequest = new PaymentMethodsRequest();
paymentMethodsRequest.setMerchantAccount(this.applicationProperty.getMerchantAccount());
paymentMethodsRequest.setChannel(PaymentMethodsRequest.ChannelEnum.WEB);

log.info("REST request to get Adyen payment methods {}", paymentMethodsRequest);
var response = paymentsApi.paymentMethods(paymentMethodsRequest);
return ResponseEntity.ok()
.body(response);
} catch (Exception e) {
log.error(e.getMessage());
return ResponseEntity.status(500).build();
}
}

/**
Expand All @@ -117,47 +116,48 @@ public ResponseEntity<PaymentMethodsResponse> paymentMethods() throws IOExceptio
*/
@PostMapping("/initiatePayment")
public ResponseEntity<PaymentResponse> payments(@RequestHeader String host, @RequestBody PaymentRequest body, HttpServletRequest request) throws IOException, ApiException {
var paymentRequest = new PaymentRequest();

var orderRef = UUID.randomUUID().toString();
var amount = new Amount()
.currency("EUR")
.value(10000L); // value is 100€ in minor units

paymentRequest.setMerchantAccount(this.applicationProperty.getMerchantAccount()); // required
paymentRequest.setChannel(PaymentRequest.ChannelEnum.WEB);
paymentRequest.setReference(orderRef); // required
paymentRequest.setReturnUrl(request.getScheme() + "://" + host + "/api/handleShopperRedirect?orderRef=" + orderRef);

paymentRequest.setAmount(amount);
// set lineItems required for some payment methods (ie Klarna)
paymentRequest.setLineItems(Arrays.asList(
new LineItem().quantity(1L).amountIncludingTax(5000L).description("Sunglasses"),
new LineItem().quantity(1L).amountIncludingTax(5000L).description("Headphones"))
);
// required for 3ds2 native flow
paymentRequest.setAdditionalData(Collections.singletonMap("allow3DS2", "true"));
// required for 3ds2 native flow
paymentRequest.setOrigin(request.getScheme() + "://" + host );
// required for 3ds2
paymentRequest.setBrowserInfo(body.getBrowserInfo());
// required by some issuers for 3ds2
paymentRequest.setShopperIP(request.getRemoteAddr());
paymentRequest.setPaymentMethod(body.getPaymentMethod());

log.info("REST request to make Adyen payment {}", paymentRequest);
var response = paymentsApi.payments(paymentRequest);

var session = request.getSession();
if (response.getDonationToken() == null) {
log.error("The payments endpoint did not return a donationToken, please enable this in your Customer Area. See README.");
}
else {
session.setAttribute(PAYMENT_ORIGINAL_PSPREFERENCE, response.getPspReference());
session.setAttribute(DONATION_TOKEN, response.getDonationToken());
try {
var paymentRequest = new PaymentRequest();

var orderRef = UUID.randomUUID().toString();
var amount = new Amount()
.currency("EUR")
.value(10000L); // value is 100€ in minor units

paymentRequest.setMerchantAccount(this.applicationProperty.getMerchantAccount()); // required
paymentRequest.setChannel(PaymentRequest.ChannelEnum.WEB);
paymentRequest.setReference(orderRef); // required
paymentRequest.setReturnUrl(request.getScheme() + "://" + host + "/api/handleShopperRedirect?orderRef=" + orderRef);

paymentRequest.setAmount(amount);
// set lineItems required for some payment methods (ie Klarna)
paymentRequest.setLineItems(Arrays.asList(
new LineItem().quantity(1L).amountIncludingTax(5000L).description("Sunglasses"),
new LineItem().quantity(1L).amountIncludingTax(5000L).description("Headphones"))
);
// required for 3ds2 native flow
paymentRequest.setAdditionalData(Collections.singletonMap("allow3DS2", "true"));
// required for 3ds2 native flow
paymentRequest.setOrigin(request.getScheme() + "://" + host);
// required for 3ds2
paymentRequest.setBrowserInfo(body.getBrowserInfo());
// required by some issuers for 3ds2
paymentRequest.setShopperIP(request.getRemoteAddr());
paymentRequest.setPaymentMethod(body.getPaymentMethod());

log.info("REST request to make Adyen payment {}", paymentRequest);
var response = paymentsApi.payments(paymentRequest);

DonationUtil.setDonationTokenAndOriginalPspReference(request.getSession(), response.getDonationToken(), response.getPspReference());

return ResponseEntity.ok().body(response);
} catch (NotFoundException e) {
log.warn(e.getMessage());
return ResponseEntity.notFound().build();
} catch (Exception e) {
log.error(e.getMessage());
return ResponseEntity.status(500).build();
}
return ResponseEntity.ok()
.body(response);
}

/**
Expand All @@ -169,10 +169,14 @@ public ResponseEntity<PaymentResponse> payments(@RequestHeader String host, @Req
*/
@PostMapping("/submitAdditionalDetails")
public ResponseEntity<PaymentDetailsResponse> payments(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException {
log.info("REST request to make Adyen payment details {}", detailsRequest);
var response = paymentsApi.paymentsDetails(detailsRequest);
return ResponseEntity.ok()
.body(response);
try {
log.info("REST request to make Adyen payment details {}", detailsRequest);
var response = paymentsApi.paymentsDetails(detailsRequest);
return ResponseEntity.ok().body(response);
} catch (Exception e) {
log.error(e.getMessage());
return ResponseEntity.status(500).build();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.adyen.giving.util;

import jakarta.servlet.http.HttpSession;
import jakarta.ws.rs.NotFoundException;

public final class DonationUtil {
private static final String DONATION_TOKEN = "DonationToken";

private static final String PAYMENT_ORIGINAL_PSPREFERENCE = "PaymentOriginalPspReference";

public static void setDonationTokenAndOriginalPspReference(HttpSession session, String donationToken, String originalPspReference) throws NullPointerException {
if (donationToken == null) {
throw new NullPointerException("No donationToken is found. The payments endpoint did not return a donationToken, please enable this in your Customer Area. See README.");
}

session.setAttribute(PAYMENT_ORIGINAL_PSPREFERENCE, originalPspReference);
session.setAttribute(DONATION_TOKEN, donationToken);
}

public static String getDonationToken(HttpSession session) throws NotFoundException {
var donationToken = session.getAttribute(DONATION_TOKEN);
if (donationToken == null) {
throw new NotFoundException("Could not find donationToken in the sessions");
}
return (String) donationToken;
}

public static String getPaymentOriginalPspReference(HttpSession session) throws NotFoundException {
var pspReference = session.getAttribute(PAYMENT_ORIGINAL_PSPREFERENCE);
if (pspReference == null) {
throw new NotFoundException("Could not find originalPspReference in the sessions");
}
return (String) pspReference;
}
}
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
adyenVersion="21.4.0"
adyenVersion="22.1.0"
springVersion="3.1.4"
springDependendyManagementVersion="1.1.3"

Expand Down

0 comments on commit ea74a69

Please sign in to comment.