diff --git a/src/main/java/com/adyen/workshop/configurations/DependencyInjectionConfiguration.java b/src/main/java/com/adyen/workshop/configurations/DependencyInjectionConfiguration.java index 1af7243..ba22844 100644 --- a/src/main/java/com/adyen/workshop/configurations/DependencyInjectionConfiguration.java +++ b/src/main/java/com/adyen/workshop/configurations/DependencyInjectionConfiguration.java @@ -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); } diff --git a/src/main/java/com/adyen/workshop/controllers/ApiController.java b/src/main/java/com/adyen/workshop/controllers/ApiController.java index a73ce91..cf99cec 100644 --- a/src/main/java/com/adyen/workshop/controllers/ApiController.java +++ b/src/main/java/com/adyen/workshop/controllers/ApiController.java @@ -41,25 +41,121 @@ public ResponseEntity helloWorld() throws Exception { @PostMapping("/api/paymentMethods") public ResponseEntity 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 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 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()); } } diff --git a/src/main/java/com/adyen/workshop/controllers/WebhookController.java b/src/main/java/com/adyen/workshop/controllers/WebhookController.java index 246caad..1d622cb 100644 --- a/src/main/java/com/adyen/workshop/controllers/WebhookController.java +++ b/src/main/java/com/adyen/workshop/controllers/WebhookController.java @@ -37,7 +37,37 @@ public WebhookController(ApplicationConfiguration applicationConfiguration, HMAC @PostMapping("/webhooks") public ResponseEntity 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(); + } } } \ No newline at end of file diff --git a/src/main/resources/static/adyenWebImplementation.js b/src/main/resources/static/adyenWebImplementation.js index 97fd2d5..080792d 100644 --- a/src/main/resources/static/adyenWebImplementation.js +++ b/src/main/resources/static/adyenWebImplementation.js @@ -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; + } + } } diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index 1fa3aaf..d0f60ec 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -23,10 +23,16 @@ + - +