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

Workshop/solution-v5 using Adyen.Web Dropin v5.63.0 #4

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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 @@ -18,8 +18,10 @@ public DependencyInjectionConfiguration(ApplicationConfiguration applicationConf

@Bean
Client client() {
// Step 4
var config = new Config();
// Step 4.
config.setApiKey(applicationConfiguration.getAdyenApiKey());
config.setEnvironment(Environment.TEST);
return new Client(config);
}

Expand Down
112 changes: 104 additions & 8 deletions src/main/java/com/adyen/workshop/controllers/ApiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,121 @@ public ResponseEntity<String> helloWorld() throws Exception {

@PostMapping("/api/paymentMethods")
public ResponseEntity<PaymentMethodsResponse> paymentMethods() throws IOException, ApiException {
// Step 7
return null;
// Step 7 - Add payment methods call
var paymentMethodsRequest = new PaymentMethodsRequest();
paymentMethodsRequest.setMerchantAccount(applicationConfiguration.getAdyenMerchantAccount());

log.info("Retrieving available Payment Methods from Adyen {}", paymentMethodsRequest);
var response = paymentsApi.paymentMethods(paymentMethodsRequest);
log.info("Payment Methods response from Adyen {}", response);
return ResponseEntity.ok()
.body(response);
}

@PostMapping("/api/payments")
public ResponseEntity<PaymentResponse> payments(@RequestHeader String host, @RequestBody PaymentRequest body, HttpServletRequest request) throws IOException, ApiException {
// Step 9
return null;
// Step 9 - Add payments call
var paymentRequest = new PaymentRequest();

var amount = new Amount()
.currency("EUR")
.value(9998L);
paymentRequest.setAmount(amount);
paymentRequest.setMerchantAccount(applicationConfiguration.getAdyenMerchantAccount());
paymentRequest.setChannel(PaymentRequest.ChannelEnum.WEB);

paymentRequest.setPaymentMethod(body.getPaymentMethod());

var orderRef = UUID.randomUUID().toString();
paymentRequest.setReference(orderRef);
// The returnUrl: Once done with the payment, where should the application redirect you?
paymentRequest.setReturnUrl(request.getScheme() + "://" + host + "/api/handleShopperRedirect"); // Example: Turns into http://localhost:8080/api/handleShopperRedirect

// Step 12 3DS2 Redirect - Add the following additional parameters to your existing payment request
var authenticationData = new AuthenticationData();
authenticationData.setAttemptAuthentication(AuthenticationData.AttemptAuthenticationEnum.ALWAYS);
paymentRequest.setAuthenticationData(authenticationData);

// Add these lines to enable the Native 3DS2 flow:
//authenticationData.setThreeDSRequestData(new ThreeDSRequestData().nativeThreeDS(ThreeDSRequestData.NativeThreeDSEnum.PREFERRED));
//paymentRequest.setAuthenticationData(authenticationData);
// Note: Visa requires additional properties to be sent in the request, see documentation for Native 3DS2: https://docs.adyen.com/online-payments/3d-secure/native-3ds2/web-drop-in/#make-a-payment

paymentRequest.setOrigin(request.getScheme() + "://" + host);
paymentRequest.setBrowserInfo(body.getBrowserInfo());
paymentRequest.setShopperIP(request.getRemoteAddr());
paymentRequest.setShopperInteraction(PaymentRequest.ShopperInteractionEnum.ECOMMERCE);

// Note: Visa requires additional properties to be sent in the request, see documentation for Redirect 3DS2: https://docs.adyen.com/online-payments/3d-secure/redirect-3ds2/web-drop-in/#make-a-payment

var billingAddress = new BillingAddress();
billingAddress.setCity("Amsterdam");
billingAddress.setCountry("NL");
billingAddress.setPostalCode("1012KK");
billingAddress.setStreet("Rokin");
billingAddress.setHouseNumberOrName("49");
paymentRequest.setBillingAddress(billingAddress);

// Step 11 - Add idempotency key
var requestOptions = new RequestOptions();
requestOptions.setIdempotencyKey(UUID.randomUUID().toString());

log.info("PaymentsRequest {}", paymentRequest);
var response = paymentsApi.payments(paymentRequest, requestOptions);
log.info("PaymentsResponse {}", response);
return ResponseEntity.ok().body(response);
}

// Step 13 - Handle details call (triggered f.e. when Native 3DS2)
@PostMapping("/api/payments/details")
public ResponseEntity<PaymentDetailsResponse> paymentsDetails(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException {
// Step 13
return null;
// Step 13.
log.info("PaymentDetailsRequest {}", detailsRequest);
var response = paymentsApi.paymentsDetails(detailsRequest);
log.info("PaymentDetailsResponse {}", response);
return ResponseEntity.ok()
.body(response);
}

// Step 14 - Handle Redirect 3DS2 during payment.
@GetMapping("/api/handleShopperRedirect")
public RedirectView redirect(@RequestParam(required = false) String payload, @RequestParam(required = false) String redirectResult) throws IOException, ApiException {
// Step 14
return null;
var paymentDetailsRequest = new PaymentDetailsRequest();

PaymentCompletionDetails paymentCompletionDetails = new PaymentCompletionDetails();

// Handle redirect result or payload
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
paymentCompletionDetails.redirectResult(redirectResult);
} else if (payload != null && !payload.isEmpty()) {
paymentCompletionDetails.payload(payload);
}

paymentDetailsRequest.setDetails(paymentCompletionDetails);

var paymentsDetailsResponse = paymentsApi.paymentsDetails(paymentDetailsRequest);
log.info("PaymentsDetailsResponse {}", paymentsDetailsResponse);

// Handle response and redirect user accordingly
var redirectURL = "/result/";
switch (paymentsDetailsResponse.getResultCode()) {
case AUTHORISED:
redirectURL += "success";
break;
case PENDING:
case RECEIVED:
redirectURL += "pending";
break;
case REFUSED:
redirectURL += "failed";
break;
default:
redirectURL += "error";
break;
}
return new RedirectView(redirectURL + "?reason=" + paymentsDetailsResponse.getResultCode());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,37 @@ public WebhookController(ApplicationConfiguration applicationConfiguration, HMAC

@PostMapping("/webhooks")
public ResponseEntity<String> webhooks(@RequestBody String json) throws Exception {
// Step 16
return null;
// Step 16 - Receive webhooks and verify the HMAC signature
log.info("Received webhook request: {}", json);
var notificationRequest = NotificationRequest.fromJson(json);
var notificationRequestItem = notificationRequest.getNotificationItems().stream().findFirst();

try {
NotificationRequestItem item = notificationRequestItem.get();

if (!hmacValidator.validateHMAC(item, this.applicationConfiguration.getAdyenHmacKey())) {
log.warn("Could not validate HMAC signature for incoming webhook message: {}", item);
return ResponseEntity.unprocessableEntity().build();
}

// Success, log it for now
log.info("""
Received webhook with event {} :\s
Merchant Reference: {}
Alias : {}
PSP reference : {}""",
item.getEventCode(),
item.getMerchantReference(),
item.getAdditionalData().get("alias"),
item.getPspReference());

return ResponseEntity.accepted().build();
} catch (SignatureException e) {
// Handle invalid signature
return ResponseEntity.unprocessableEntity().build();
} catch (Exception e) {
// Handle all other errors
return ResponseEntity.status(500).build();
}
}
}
66 changes: 62 additions & 4 deletions src/main/resources/static/adyenWebImplementation.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,72 @@
const clientKey = document.getElementById("clientKey").innerHTML;
const type = document.getElementById("type").innerHTML;

// Starts the (Adyen.Web) AdyenCheckout with your specified configuration by calling the `/paymentMethods` endpoint.
async function startCheckout() {
// Step 8
try {
let paymentMethodsResponse = await sendPostRequest("/api/paymentMethods");

const configuration = {
paymentMethodsResponse: paymentMethodsResponse,
clientKey,
locale: "en_US",
environment: "test",
showPayButton: true,
paymentMethodsConfiguration: {
card: {
hasHolderName: true,
holderNameRequired: true,
name: "Credit or debit card",
amount: {
value: 9998,
currency: "EUR",
},
}
},
// Step 8 onSubmit(...)
onSubmit: async (state, component) => {
if (state.isValid) {
const response = await sendPostRequest("/api/payments", state.data);
handleResponse(response, component);
}
},
// Step 13 onAdditionalDetails(...)
onAdditionalDetails: async (state, component) => {
const response = await sendPostRequest("/api/payments/details", state.data);
handleResponse(response, component);
}
};

// Start the AdyenCheckout and mount the element onto the `payment`-div.
let adyenCheckout = await new AdyenCheckout(configuration);
adyenCheckout.create(type).mount(document.getElementById("payment"));
} catch (error) {
console.error(error);
alert("Error occurred. Look at console for details.");
}
}

// Step 10 - Handles responses, do a simple redirect based on the result.
// Step 10 - Handles responses, do a redirect based on result.
function handleResponse(response, component) {
// We'll leave this empty for now and fix this in step 10.
// Step 13 - If there's an action, handle it, otherwise redirect the user to the correct page based on the resultCode.
if (response.action) {
component.handleAction(response.action);
} else {
switch (response.resultCode) {
case "Authorised":
window.location.href = "/result/success";
break;
case "Pending":
case "Received":
window.location.href = "/result/pending";
break;
case "Refused":
window.location.href = "/result/failed";
break;
default:
window.location.href = "/result/error";
break;
}
}
}


Expand Down
8 changes: 7 additions & 1 deletion src/main/resources/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@

<!-- Step 6 -->
<!-- Add Adyen.Web Components/Drop-in stylesheet -->
<link rel="stylesheet"
href="https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/5.63.0/adyen.css"
integrity="sha384-Dk62669n9Ic7V6K8X7MBAOEZ5IQ9Qq29nW/zPkfwg1ghqyZLiuSc5QYQJ6M72iNR"
crossorigin="anonymous">

<!-- Step 6 -->
<!-- Add Adyen.Web Components/Drop-in embed script -->

<script src="https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/5.63.0/adyen.js"
integrity="sha384-wEt/Yz/g97VSgMpNDVCPTyr8FYuIpeOgh42UNr346/yK3z2yfaIixeuLWXG6q0XU"
crossorigin="anonymous"></script>

</head>
<body>
Expand Down