Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade & Add 3ds2 redirect/native #192

Merged
merged 4 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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