From a75c2d27bf8f3e1ad9560b0f464db1163c9d1445 Mon Sep 17 00:00:00 2001 From: Kwok He Chu <105217051+Kwok-he-Chu@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:02:26 +0200 Subject: [PATCH 1/8] Updated README.md --- README.md | 148 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 93 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index c1e0013..00b37d2 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,124 @@ -# Build Your Own Payment Integration with Adyen - A Step-by-Step Guide +# Build Your Payment Integration with Adyen - A Step-by-Step Guide ### Prerequisites -_Note: For this workshop, we're asking you to start with a clean Adyen Merchant Account._ +_Note: We ask you to start with a clean Adyen Merchant Account for this workshop._ You will need a few things to get started: -* an IDE (like IntelliJ or VsCode) -* Java SDK v17+ - You can use any but the project was tested with Java 17. -* Access to an [Adyen Test Account](https://www.adyen.com/signup). +* an IDE (like IntelliJ or VsCode) and Java SDK v17+, alternatively: you can spin up this workspace in a browser-IDE such as codespaces or [Gitpod](https://gitpod.io/#https://github.com/adyen-examples/adyen-step-by-step-integration-workshop). +* Access to an [Adyen Test Account](https://www.adyen.com/signup) (ECOM). + + -Alternatively, if you do not want to run this locally, you can spin-up this repository in a VM such as [Gitpod](https://gitpod.io/#https://github.com/adyen-examples/adyen-step-by-step-integration-workshop). ## Introduction -This repository is a step-by-step guide to building your own payment integration with Adyen. -We'll guide you through the steps needed to build an Adyen integration and make your first payment on TEST. -This includes the credentials, configuration, API requests (using the Java Adyen library, `/paymentMethods`, `/payments`, `/payments/details` and 3DS2), error handling and webhooks. +This repository is a step-by-step guide to building your payment integration with Adyen. +We'll guide you through the steps to build an Adyen integration and make your first payment on the Adyen TEST environment. We'll cover three main steps: +* The initial setup: Adyen Merchant Account, Adyen API Credentials, and Adyen Client Key. +* The API requests needed: `/paymentMethods`, `/payments`, `/payments/details`, and 3D Secure 2). +* The webhooks: Setup, configuration, and response handling. ### Context of the code repository. -In this workshop, we are using Java and Spring Boot, together with a static (javascript) frontend with a `thymeleaf` template (a server-side Java template engine that can process HTML). +This workshop uses a Java+Spring Boot in the backend with a static (HTML/CSS/Javascript) frontend with a `thymeleaf` template (a server-side Java template engine that can process HTML). -_Note: In case the static frontend environment is not to your liking, feel free to implement your own frontend solution (node) using the framework of your choice._ +_Note: If the static frontend environment is not to your liking, feel free to implement your own frontend solution using the framework of your choice (f.e. using Node.js)._ -In this workshop we are not asking you to build a complete integration from scratch, but rather to fill in the voids based on resources you can find in on the [Adyen Documentation](https://docs.adyen.com) -or other online resources ([GitHub](https://github.com/adyen), [GitHub Examples](https://github.com/adyen-examples), [Adyen Help](https://help.adyen.com) etc). +In this workshop, we're not asking you to build a complete integration from scratch but rather to fill in the voids based on resources you can find in the [Adyen Documentation](https://docs.adyen.com) +or other online resources ([GitHub](https://github.com/adyen), [GitHub Examples](https://github.com/adyen-examples), [Adyen Help](https://help.adyen.com) etc.). We use an empty Spring Boot template at the start, which you'll extend into a fully working application that can accept payments. + + + ### Project Structure The project structure follows a Model-View-Controller (MVC) structure. -* The java code is to be found in `src/main/java/com/adyen/workshop` - * `/controllers`-folder contains your endpoints. - * `/views`-folder contains the view controllers that show the pages (views). +* The Java code is to be found in `src/main/java/com/adyen/workshop` + * `/controllers` folder contains your endpoints. The following example creates the `/hello-world` endpoint; see `/controllers/ApiController.java.` + + ``` + @GetMapping("/hello-world") + public ResponseEntity helloWorld() throws Exception { + return ResponseEntity.ok() + .body("This is the 'Hello World' from the workshop - You've successfully finished step 0!"); + } + ``` + + * `/views`-folder contains view controllers that show the HTML pages in the `/resources/static/` folder * The code you need to update is in the `/controllers` folder. `ApiController.java`. * You can add your environment variables (`ADYEN_API_KEY`, `ADYEN_MERCHANT_ACCOUNT`, `ADYEN_CLIENT_KEY`, `ADYEN_HMAC_KEY`) in the `ApplicationConfiguration.java` class. * The frontend templates are to be found in `src/main/resources/templates` and the static resources in `src/main/resources/static` * The code you need to update is in the `src/main/resources/static/adyenWebImplementation.js` and `src/main/resources/templates/layout.html` * Some additional information: * The `clientKey` in `adyenWebImplementation.js` is automatically passed from the backend to the client side and can be accessed using: `clientKey`. - * In order to play around with multiple payment methods, a `type` value is passed from the client to the server, which contains the name of an adyen payment method and that the adyen web components will recognize. -* To run the project - * `./gradlew bootJar` will build the project. + * To experiment with multiple payment methods, a `type` value is passed from the client to the server. This value contains the name of an Adyen payment method that the Adyen web components will recognize. The value is currently hardcoded as `dropin`, see: `/preview?type=dropin` in `/resources/templates/index.html`. +``` +
  • + +
    +

    Drop-in

    +
    +
    +
  • +``` + +* To run the project, you have two options: * `./gradlew bootRun` will start the server on port 8080. - * To run the project from your IDE (e.g. IntelliJ), go to `src/main/java/com/adyen/workshop/MainApplication.java` -> Right-click and click `Debug` or `Run`. + * To run the project from your IDE, go to `src/main/java/com/adyen/workshop/MainApplication.java` -> Right-click and click `Debug` or `Run`. + + +## Workshop: Accepting Online payments using the Advanced flow +Learn how to integrate with Adyen using the `/paymentMethods`, `/payments` and `/payments/details` endpoints. -# Workshop: Accepting Online payments using the Advanced flow -Learn how to integrate with Adyen using the `/paymentMethods`, `/payments` and `/payments/details` endpoints ### Briefing -You're working as a full-stack developer for an E-Commerce company that sells headphones and sunglasses in the Netherlands. -In fact, they sell the best headphones and sunglasses at 49.99 each and you're extremely excited to take on this challenge. -You're not afraid of Java, and you can code JavaScript, you're ready for this. You've been tasked to implement credit card, iDeal and Klarna payments using Adyen. +You're working as a full-stack developer for an ecommerce company that sells headphones and sunglasses in the Netherlands. +They sell the best headphones and sunglasses at 49.99 each, and you're incredibly excited to take on this challenge. +You're not afraid of Java and can code JavaScript; you're ready for this! You've been tasked with implementing credit card, iDeal, and Klarna payments using Adyen. + + -### Learn +### Learning goals In this workshop, you'll learn how to: -1. Retrieve a list of available payment methods using `/paymentMethods` -2. Make a payment using `/payments` followed by `/payments/details` -3. Handle the 3D Secure challenge -4. Present the payment results to the user -5. Receive updates on the payments through webhooks +1. Set up your Adyen Merchant Account, API Credentials, and Adyen Client Key. +2. Retrieve a list of available payment methods using `/paymentMethods` +3. Make a payment using `/payments` followed by `/payments/details` +4. Handle Strong Customer Authentication using 3DSecure 2. +5. Present the payment results to the user +6. Receive updates on the payments through webhooks + + + ### Start - Step-by-Step Guide: -Step 0. Build the project and run it to see if it works. If you can visit `http://localhost:8080/hello-world`, `https://8080-adyenexampl-adyenstepby-xxxxxx21.ws-eu114.gitpod.io/hello-world` (gitpod) or `https://xxxx.github.dev/hello-world`, this means it works! In the next steps, we'll guide you through getting the paymentMethods and making your first payment! - * `./gradlew bootRun` will build and start the application on port 8080. - * To run the project from your IDE (e.g. IntelliJ), go to `src/main/java/com/adyen/workshop/MainApplication.java` -> Right-click and click `Debug` or `Run`. +**Step 0.** Build the project using `./gradlew bootRun` and see if it works. If you can visit `http://localhost:8080/hello-world`, `https://8080-adyenexampl-adyenstepby-xxxxxx21.ws-eu114.gitpod.io/hello-world` (Gitpod) or `https://xxxx.github.dev/hello-world` (codespaces), this means it works! + -**Step 1.** [Get your Adyen Merchant Account](https://docs.adyen.com/account/manage-account-structure/#request-merchant-account). +**Step 1.** [Get your Adyen Merchant Account](https://docs.adyen.com/account/manage-account-structure/#request-merchant-account) or use the existing one ending with `ECOM`. -**Step 2.** [Get your Adyen API Key](https://docs.adyen.com/development-resources/api-credentials/#generate-api-key). + +**Step 2.** [Get your Adyen API Key](https://docs.adyen.com/development-resources/api-credentials/#generate-api-key). Ensure you've created the API Key on the Merchant Account level (e.g., you've selected your MerchantAccount and created credentials in the API Credentials page in the Customer Area). - Pro-tip #1: Make sure you copy your key correctly. - - Pro-tip #2: Make 101% sure you copy your key correctly :) + - Pro-tip #2: Make 101% sure you copy your key correctly! :) **Step 3.** [Get your Adyen Client Key](https://docs.adyen.com/development-resources/client-side-authentication/#get-your-client-key). - - Do **not** forget to add the correct URL to my allowed origins (e.g. `http://localhost:8080`, `https://*.gitpod.io` or `https://*.github.dev` - This allows Adyen.Web Dropin/Components to load on your page. The `*`-symbol indicates to accept any subdomain. + - Do **not** forget to add the correct URL to my allowed origins (e.g. `http://localhost:8080`, `https://*.gitpod.io`, or `https://*.github.dev`). This allows Adyen.Web Dropin/Components to load on your page. The `*`-symbol indicates to accept any subdomain. + **Step 4.** Add your keys to `ApplicationConfiguration.java` in `/main/java/com/adyen/workshop/configurations`: - - Best practice: export the vars as follows so that the Spring.Boot framework can inject your variables on startup. + - Best practice: export the vars as follows so that the Spring Boot framework can automatically inject your variables on startup. - If you're using gitpod/codespaces, you can export your variables as follows in your terminal: - If you've used gitpod before, the program will inject previously used environment variables as configured in [https://gitpod.io/variables](https://gitpod.io/variables). ``` @@ -96,6 +127,7 @@ export ADYEN_CLIENT_KEY='test_yourclientkey' export ADYEN_MERCHANT_ACCOUNT='YourMerchantAccountName' ``` + You can now access your keys in your application anywhere: - `applicationConfiguration.getAdyenApiKey()` - `applicationConfiguration.getAdyenClientKey()` @@ -105,11 +137,11 @@ You can now access your keys in your application anywhere: In `/com/adyen/workshop/configurations/`, you'll find a `DependencyInjectionConfiguration`. This is where we create our Adyen instances and **re-use** them using Spring's Constructor Dependency Injection (CDI) - A `@Bean` is an object that is instantiated, assembled, and managed by a Spring IoC container. -**Exercise:** Create your Adyen-`Client` by creating a `new Config()`-object, pass your `ADYEN_API_KEY` and specify `Environment.TEST`, which we'll use later on. -We've created `PaymentsApi`-service (communicates with the Adyen endpoints) and `hmacValidator` instances already for you. +**Exercise:** Create your Adyen-`Client` by creating a `new Config()` object, passing your `ADYEN_API_KEY`, and specifying `Environment.TEST`, which we'll use later. +We've already created the `PaymentsApi` service (which communicates with the Adyen endpoints) which uses this client.
    -Show me the answer +Click here to show me the answer ```java @@ -143,8 +175,8 @@ public class DependencyInjectionConfiguration {
    -**Step 5.** **Skip this step**: Install the [Java library](https://github.com/Adyen/adyen-java-api-library) by adding the following line to the `build.gradle` file, build the project to pull-in the Adyen Java API Library. -For your convenience, we've already included this in the project. +**Step 5.** **You can skip this step**: Install the [Java library](https://github.com/Adyen/adyen-java-api-library) by adding the following line to the `build.gradle` file. +For your convenience, we've **already included this in this project**. You can visit the `build.gradle` file and verify whether the following line is included: ``` implementation 'com.adyen:adyen-java-api-library:25.1.0' @@ -153,25 +185,25 @@ For your convenience, we've already included this in the project. **Step 6.** Install the latest [Adyen.Web Dropin/Components](https://docs.adyen.com/online-payments/release-notes/) by adding embed script(`.js`) and stylesheet(`.css`) to `/resources/templates/layout.html`. - Including this allows you to access the AdyenCheckout instance in JavaScript. In this example, we use `Web Components/Drop-in v5.63.0`. - + - Note: If the `Embed script and stylesheet` tab shows ``'Coming soon!'`, don't worry, you can grab an earlier version of `Web Components/Drop-in` **Step 7.** Let's prepare our backend (`com/adyen/workshop/controllers`) to [retrieve a list of available payment methods](https://docs.adyen.com/online-payments/build-your-integration/advanced-flow/?platform=Web&integration=Drop-in&version=5.63.0&programming_language=java#web-advanced-flow-post-payment-methods-request). Go to `ApiController.java` and use the `paymentsApi` to make `/paymentMethods`-request to Adyen. +
    -Show me the answer +Click here to show me the answer ```java @PostMapping("/api/paymentMethods") public ResponseEntity paymentMethods() throws IOException, ApiException { var paymentMethodsRequest = new PaymentMethodsRequest(); - - // Fill in the parameters below - //paymentMethodsRequest.setMerchantAccount(...); + 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); } @@ -179,11 +211,13 @@ For your convenience, we've already included this in the project.
    -**Step 8.** On your frontend (`adyenWebImplementation.js`), let's call this new endpoint and display the payment methods to the shopper. +**Note:** You can send a `curl-request` to test this endpoint. However, let's move on to step 8 to see how the Drop-in (frontend) interacts with this `/api/paymentMethods` endpoint. + +**Step 8.** On your frontend (`adyenWebImplementation.js`), let's make a request to this `/api/paymentMethods` endpoint and display the payment methods to the shopper. We automatically pass on your public `ADYEN_CLIENT_KEY` to your frontend, you can access this variable using `clientKey`. -Create the configuration for the `AdyenCheckout`-instance, call the `/api/paymentMethods/`-endpoint, create the `AdyenCheckOut()`-instance and mount it to `"payment"-div` container. +Create the configuration for the `AdyenCheckout`-instance, call the `/api/paymentMethods/`-endpoint, create the `AdyenCheckOut()`-instance, and mount it to `"payment"-div` container (see `/resources/templates/checkout.html`). -**Note:** We've added a `sendPostRequest(...)` function that can communicate with your backend. +We've added a `sendPostRequest(...)` helper function to communicate with your backend. You can copy and paste the following code below: ```js @@ -240,10 +274,14 @@ startCheckout(); ``` -Run your application to see whether the Dropin is showing a list of payment methods. You'll notice that the application will crash when you try to continue as we haven't implemented the payment yet. +Run your application to see whether the `Adyen Drop-in` is showing a list of payment methods. You'll notice that the Drop-in won't let you click `"Pay"` as we haven't implemented the `/payments` call yet. +Here are some troubleshooting notes if you do not see any payment methods: +* **Empty response:** Have you configured any payment methods in the Customer Area? +* **Invalid origin:** Have you added the correct origin URLs that allow your `Adyen Drop-in` to be loaded by the page? +* **Unauthorized errors:** Have you specified your credentials correctly? **Step 9.** Let's create the `/payments`-request ([see docs](https://docs.adyen.com/online-payments/build-your-integration/advanced-flow/?platform=Web&integration=Drop-in&version=5.63.0&programming_language=java#post-payments-request-web)) on the backend. -We start by defining a new endpoint `/api/payments` that our frontend will send a request to. +We start by defining a new endpoint `/api/payments` to which our frontend will send a request.
    Show me the answer From 92abf97e0e605496e8d1ec42f4b860889148435e Mon Sep 17 00:00:00 2001 From: Kwok He Chu <105217051+Kwok-he-Chu@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:33:36 +0200 Subject: [PATCH 2/8] Update README --- README.md | 240 ++++++++++++------ .../workshop/controllers/ApiController.java | 15 +- 2 files changed, 176 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 00b37d2..517c93b 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ Here are some troubleshooting notes if you do not see any payment methods: We start by defining a new endpoint `/api/payments` to which our frontend will send a request.
    -Show me the answer +Click to show me the answer ```java @PostMapping("/api/payments") @@ -302,7 +302,7 @@ public ResponseEntity payments(@RequestHeader String host, @Req var orderRef = UUID.randomUUID().toString(); paymentRequest.setReference(orderRef); - // The returnUrl is basically: Once done with the payment, where shall we redirect you? + // The returnUrl field basically means: Once done with the payment, where should the application redirect you? paymentRequest.setReturnUrl(request.getScheme() + "://" + host + "/api/handleShopperRedirect?orderRef=" + orderRef); // Example: Turns into http://localhost:8080/api/handleShopperRedirect?orderRef=354fa90e-0858-4d2f-92b9-717cb8e18173 @@ -315,35 +315,34 @@ public ResponseEntity payments(@RequestHeader String host, @Req
    -**Step 10.** Best practices: Add the Idempotency key, see [documentation](https://docs.adyen.com/development-resources/api-idempotency/) to your payment request. + +**Step 10.** **Best practices:** The Adyen API supports idempotent requests, allowing you to retry a request multiple times while only performing the action once. This helps avoid unwanted duplication in case of failures and retries (e.g., you don't want to charge a shopper twice because they've hit the pay button two times, right?). +Add the idempotency key to your payment request, see [documentation](https://docs.adyen.com/development-resources/api-idempotency/). + + +
    +Click to show me the answer + ```java 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 11.** Let's finalize the payment by calling the `/payments/details`-endpoint. We need to create another endpoint `/api/payments/details` - We take the details passed from the frontend and finalize the payment. -```java -@PostMapping("/api/payments/details") -public ResponseEntity paymentsDetails(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException { - log.info("PaymentDetailsRequest {}", detailsRequest); - var response = paymentsApi.paymentsDetails(detailsRequest); - return ResponseEntity.ok() - .body(response); -} -``` +
    -**Step 12.** Let's now send a request to our backend from our frontend, modify the `adyenWebImplementation.js` to override the `onSubmit(...)` function to call `/api/payments` and `onAdditionaDetails(...)` to call `/api/payments/details`. -We've added a helper function `handleResponse(...)` to do a simple redirect. + +**Step 11.** Let's send a request to our backend from our frontend, and modify the `adyenWebImplementation.js` to override the `onSubmit(...)` function to send a request to the `/api/payments` endpoint. +We've added the `onSubmit(...)` event handler here and the `handleResponse(response, component)` function to handle the response (which is performing a simple redirect to the right page).
    -Show me the answer +Click to show me the answer -**Note:** We've added the `onSubmit()` and `onAdditionalDetails()` handlers here. ```js async function startCheckout() { try { @@ -366,17 +365,12 @@ async function startCheckout() { }, } }, - // Step 12 onSubmit(...) + // Step 11 onSubmit(...) onSubmit: async (state, component) => { if (state.isValid) { const response = await sendPostRequest("/api/payments", state.data); handleResponse(response, component); } - }, - // Step 12 onAdditionalDetails(...) - onAdditionalDetails: async (state, component) => { - const response = await sendPostRequest("/api/payments/details", state.data); - handleResponse(response, component); } }; @@ -389,7 +383,7 @@ async function startCheckout() { } } -// Step 12 - Handles responses, do a simple redirect based on the result. +// Step 11 - Handles responses, do a simple redirect based on the result. function handleResponse(response, component) { switch (response.resultCode) { case "Authorised": @@ -413,24 +407,158 @@ function handleResponse(response, component) {
    -You should now be able to make a payment, **however** it will fail when a challenge is presented to the shopper. Let's handle this by adding 3D Secure 2 Authentication support. +You should now be able to make a payment! **However**, we're not there yet! This flow will fail when a challenge is presented to the shopper (Strong Customer Authentication). Let's handle this by adding 3D Secure 2 Authentication support. -3D Secure 2 is an authentication protocol (3DS2) that provides an additional layer of verification for card-not-present (CNP) transactions. -Pick one of these two options: - * [Native](https://docs.adyen.com/online-payments/3d-secure/native-3ds2/web/): The card issuer performs the authentication within your website or mobile app using passive, biometric, and two-factor authentication approaches. For more information, refer to 3D Secure 2 authentication flows. - * [Redirect](https://docs.adyen.com/online-payments/3d-secure/redirect-3ds2/web/): Shoppers are redirected to the card issuer's site to provide additional authentication data, for example a password or an SMS verification code. The redirection might lead to lower conversion rates due to technical errors during the redirection, or shoppers dropping out of the authentication process. +3D Secure 2 is an authentication protocol (3DS2) that provides an additional layer of verification for card-not-present (CNP) transactions. To trigger 3DS2, we'll need to add several parameters to the `PaymentRequest` in the `/api/payments` endpoint. +Pick one of these two options. + * [Native](https://docs.adyen.com/online-payments/3d-secure/native-3ds2/web/): The card issuer performs the authentication within your website or mobile app using passive, biometric, and two-factor authentication approaches. + * [Redirect](https://docs.adyen.com/online-payments/3d-secure/redirect-3ds2/web/): Shoppers are redirected to the card issuer's site to provide additional authentication data, for example, a password or an SMS verification code. The redirection might lead to lower conversion rates due to technical errors during the redirection or shoppers dropping out of the authentication process. -Let's add 3DS2 to our `/payments`-request. Note: New to 3DS2? You can read our [docs](https://docs.adyen.com/online-payments/3d-secure/) or go to this [technical blogpost](https://www.adyen.com/knowledge-hub/a-guide-to-integrating-with-adyen-web-for-3d-secure-2-payments) that will guide you through why/what. -Go back to the `ApiController`, we'll need to [add additional properties](https://docs.adyen.com/online-payments/3d-secure/redirect-3ds2/web-drop-in/#payments-request-3ds-redirect-web) to the `PaymentRequest` in `ApiController.java` (in the function `/api/payments/`). +**Step 12.** Let's add 3DS2 to our `/payments`-request. Note: New to 3DS2? You can read our [docs](https://docs.adyen.com/online-payments/3d-secure/) or go to this [technical blog post](https://www.adyen.com/knowledge-hub/a-guide-to-integrating-with-adyen-web-for-3d-secure-2-payments) that will guide you through the why & whats. +Go back to the `/controller/ApiController`, let's add the following parameters to the Payment Request for the redirect flow: + * Origin + * ShopperIP + * ShopperInteraction + * BrowserInfo + * BillingAddress (due to risk rules, we recommend including the `BillingAddress`, even though it's optional). **Note:** In this example, we use the redirect flow. You can also opt-in for the [Native flow](https://docs.adyen.com/online-payments/3d-secure/native-3ds2/web-drop-in/#make-a-payment). +
    +Click to show me the answer + +```java + @PostMapping("/api/payments") + public ResponseEntity payments(@RequestHeader String host, @RequestBody PaymentRequest body, HttpServletRequest request) throws IOException, ApiException { + var paymentRequest = new PaymentRequest(); + // ... + + // 3DS2 Redirect - Add the following additional parameters to your existing payment request + var authenticationData = new AuthenticationData(); + authenticationData.setAttemptAuthentication(AuthenticationData.AttemptAuthenticationEnum.ALWAYS); + paymentRequest.setAuthenticationData(authenticationData); + + paymentRequest.setOrigin(request.getScheme() + "://" + host); + paymentRequest.setBrowserInfo(body.getBrowserInfo()); + paymentRequest.setShopperIP(request.getRemoteAddr()); + paymentRequest.setShopperInteraction(PaymentRequest.ShopperInteractionEnum.ECOMMERCE); + + var billingAddress = new BillingAddress(); + billingAddress.setCity("Amsterdam"); + billingAddress.setCountry("NL"); + billingAddress.setPostalCode("1012KK"); + billingAddress.setStreet("Rokin"); + billingAddress.setHouseNumberOrName("49"); + paymentRequest.setBillingAddress(billingAddress); + + // ... + } +``` + +
    + -**Step 13.** Let's handle the 3DS2 in our `/payments/details`-request by simply passing the `redirectResult` or `payload` in the `/payments/details`-call. +**Step 13.** Implement the `/payments/details` call in `/controllers/ApiController`.
    -Show me the answer +Click to show me the answer + +```java +@PostMapping("/api/payments/details") +public ResponseEntity paymentsDetails(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException { + log.info("PaymentDetailsRequest {}", detailsRequest); + var response = paymentsApi.paymentsDetails(detailsRequest); + log.info("PaymentDetailsResponse {}", response); + return ResponseEntity.ok() + .body(response); +} +``` + +
    + +Next up, in our frontend, let's override the `onAdditionalDetails(...)` function in `adyenWebImplementation.js` to call `/api/payments/details`. +We've added the `onAdditionalDetails(...)` function in the `configuration` object and modified the `handleResponse(response, component)` function to allow the component to handle the challenge, see `component.handleAction(response.action)`. + + +
    +Click to show me the answer + +```js +async function startCheckout() { + 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", + }, + } + }, + 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."); + } +} + +function handleResponse(response, component) { + // 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; + } + } +} + +// ... +``` + +
    + + +**Step 14.** Let's handle 3DS2 in our `/payments/details`-request by passing the `redirectResult` or `payload` in the `/payments/details`-call. + ```java // Handle redirect during payment. @@ -477,40 +605,9 @@ public RedirectView redirect(@RequestParam(required = false) String payload, @Re ``` -
    - -**Step 14.** We'll have to update our frontend accordingly if there's an action to handle, go to `adyenWebImplementation.js` and modify the `handleResponse(...)`-function: - -**Note:** We've added the `response.action` and `component.handleAction(response.action)` functionality here. - -```js -// Handles responses sent from your server to the client. -function handleResponse(response, component) { - // Step 14 - 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; - } - } -} -``` -**Step 15.** Let's test this flow by making a payment using a regular flow and a 3DS flow. You can find more test card numbers on [Adyen docs](https://docs.adyen.com/development-resources/testing/test-card-numbers/). +**Step 15.** Let's test this flow by making a payment using a special card number that always trigger 3DS2. You can find more test card numbers on [Adyen docs](https://docs.adyen.com/development-resources/testing/test-card-numbers/). We've included a 3DS2 test card below. **Note:** For Cards, use the following Visa Test Card number, to trigger a 3DS2 flow. You can also download the official [Adyen Test Card Extension](https://chromewebstore.google.com/detail/adyen-test-cards/icllkfleeahmemjgoibajcmeoehkeoag) to prefill your card numbers. @@ -521,16 +618,17 @@ function handleResponse(response, component) { ``` -**Step 16.** Receive webhooks by enabling webhooks in the Customer Area and creating your `/webhooks`-endpoint in `Controllers/WebhookController.java`. - - [Read the docs: Enable and verify HMAC signatures](https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures/) - - Create a standard webhook in your Customer Area. Example URL -> `https://caafaf.gitpod.io/webhooks` +**Step 16.** In order to receive payment updates. You need to configure webhooks in the Customer Area. The steps are quite straight forward. + +You can receive webhooks by enabling webhooks in the Customer Area, followed by creating your `/webhooks`-endpoint in `Controllers/WebhookController.java`. + - [Read the documentation first: Enable and verify HMAC signatures](https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures/) + - Create a standard webhook in your Customer Area. Example URL -> `https://xxxx-xx.gitpod.io/webhooks` or `https://xxxx.github.dev/webhooks` - Don't forget to inject your `ADYEN_HMAC_KEY` in your `ApplicationConfiguration.java`, which you can then use to verify the HMAC signature. - Create a new `WebhookController.java` in `/java/com/adyen/workshop/controllers/WebhookController.java` -We highly recommend testing out whether your
    - Show me the answer + Click to show me the answer ```java @PostMapping("/webhooks") @@ -570,12 +668,12 @@ public ResponseEntity webhooks(@RequestBody String json) throws Exceptio
    +Congratulations, you've successfully built an integration with Adyen! You can now add support for different [payment methods](https://docs.adyen.com/payment-methods/). **Step 18.** Enable [iDeal](https://docs.adyen.com/payment-methods/ideal/web-drop-in/). - Do not forget to enable the payment method in your [Customer Area](https://ca-test.adyen.com/) - On the frontend, modify your configuration so that it pre-selects an iDeal issuer + shows the images of the bank logos - **Step 19.** [Enable Klarna](https://docs.adyen.com/payment-methods/klarna/web-drop-in/?tab=_code_payments_code__2), we'll need to add an additional parameter in the payment request. - Do not forget to enable the payment method in your [Customer Area](https://ca-test.adyen.com/) - Do not forget to add lineItems to your payment-request diff --git a/src/main/java/com/adyen/workshop/controllers/ApiController.java b/src/main/java/com/adyen/workshop/controllers/ApiController.java index 33105e4..878b153 100644 --- a/src/main/java/com/adyen/workshop/controllers/ApiController.java +++ b/src/main/java/com/adyen/workshop/controllers/ApiController.java @@ -34,35 +34,34 @@ public ApiController(ApplicationConfiguration applicationConfiguration, Payments @GetMapping("/hello-world") public ResponseEntity helloWorld() throws Exception { + // Step 0 return ResponseEntity.ok() .body("This is the 'Hello World' from the workshop - You've successfully finished step 0!"); } @PostMapping("/api/paymentMethods") public ResponseEntity paymentMethods() throws IOException, ApiException { - var paymentMethodsRequest = new PaymentMethodsRequest(); - // Step 7 - - var response = paymentsApi.paymentMethods(paymentMethodsRequest); - return ResponseEntity.ok() - .body(response); + return null; } @PostMapping("/api/payments") public ResponseEntity payments(@RequestHeader String host, @RequestBody PaymentRequest body, HttpServletRequest request) throws IOException, ApiException { + // Step 9 return null; } @PostMapping("/api/payments/details") public ResponseEntity paymentsDetails(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException { - // Step 11 + // Step 12 + var pay = new PaymentRequest(); + pay.setShopperInteraction(PaymentRequest.ShopperInteractionEnum.ECOMMERCE); return null; } @GetMapping("/api/handleShopperRedirect") public RedirectView redirect(@RequestParam(required = false) String payload, @RequestParam(required = false) String redirectResult) throws IOException, ApiException { - // Step 12 + // Step 13 return null; } } \ No newline at end of file From ae996c69d5126281be0b0c40c6386b5a5369eb18 Mon Sep 17 00:00:00 2001 From: Kwok He Chu <105217051+Kwok-he-Chu@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:19:55 +0200 Subject: [PATCH 3/8] Added solution --- README.md | 197 ++++++++++-------- .../DependencyInjectionConfiguration.java | 12 +- .../workshop/controllers/ApiController.java | 111 +++++++++- .../controllers/WebhookController.java | 32 ++- .../static/adyenWebImplementation.js | 65 +++++- src/main/resources/templates/layout.html | 11 +- 6 files changed, 316 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 517c93b..4f88350 100644 --- a/README.md +++ b/README.md @@ -135,10 +135,10 @@ You can now access your keys in your application anywhere: **Additional context:** -In `/com/adyen/workshop/configurations/`, you'll find a `DependencyInjectionConfiguration`. This is where we create our Adyen instances and **re-use** them using Spring's Constructor Dependency Injection (CDI) - A `@Bean` is an object that is instantiated, assembled, and managed by a Spring IoC container. +In `/com/adyen/workshop/configurations/`, you'll find the `DependencyInjectionConfiguration.java` class. This is where we create our Adyen instances and **re-use** them using Spring's Constructor Dependency Injection (CDI) - A `@Bean` is an object that is instantiated, assembled, and managed by the Spring IoC container. +You should be able to inject these classes similar to how we inject `ApplicationConfiguration.java` in any constructor. -**Exercise:** Create your Adyen-`Client` by creating a `new Config()` object, passing your `ADYEN_API_KEY`, and specifying `Environment.TEST`, which we'll use later. -We've already created the `PaymentsApi` service (which communicates with the Adyen endpoints) which uses this client. +**Exercise:** Create your Adyen-`Client` by creating a `new Config()` object in `configurations/DependencyInjectionConfiguration.java`, passing your `ADYEN_API_KEY`, and specifying `Environment.TEST`, which we'll use later.
    Click here to show me the answer @@ -156,6 +156,7 @@ public class DependencyInjectionConfiguration { @Bean Client client() { var config = new Config(); + // Step 4. config.setApiKey(applicationConfiguration.getAdyenApiKey()); config.setEnvironment(Environment.TEST); return new Client(config); @@ -185,7 +186,7 @@ For your convenience, we've **already included this in this project**. You can v **Step 6.** Install the latest [Adyen.Web Dropin/Components](https://docs.adyen.com/online-payments/release-notes/) by adding embed script(`.js`) and stylesheet(`.css`) to `/resources/templates/layout.html`. - Including this allows you to access the AdyenCheckout instance in JavaScript. In this example, we use `Web Components/Drop-in v5.63.0`. - - Note: If the `Embed script and stylesheet` tab shows ``'Coming soon!'`, don't worry, you can grab an earlier version of `Web Components/Drop-in` + - Note: If the `Embed script and stylesheet` tab shows `'Coming soon!'`, don't worry, you can grab an earlier version of `Web Components/Drop-in` @@ -198,6 +199,7 @@ For your convenience, we've **already included this in this project**. You can v ```java @PostMapping("/api/paymentMethods") public ResponseEntity paymentMethods() throws IOException, ApiException { + // Step 7 var paymentMethodsRequest = new PaymentMethodsRequest(); paymentMethodsRequest.setMerchantAccount(applicationConfiguration.getAdyenMerchantAccount()); @@ -214,13 +216,19 @@ For your convenience, we've **already included this in this project**. You can v **Note:** You can send a `curl-request` to test this endpoint. However, let's move on to step 8 to see how the Drop-in (frontend) interacts with this `/api/paymentMethods` endpoint. **Step 8.** On your frontend (`adyenWebImplementation.js`), let's make a request to this `/api/paymentMethods` endpoint and display the payment methods to the shopper. + We automatically pass on your public `ADYEN_CLIENT_KEY` to your frontend, you can access this variable using `clientKey`. + + Create the configuration for the `AdyenCheckout`-instance, call the `/api/paymentMethods/`-endpoint, create the `AdyenCheckOut()`-instance, and mount it to `"payment"-div` container (see `/resources/templates/checkout.html`). We've added a `sendPostRequest(...)` helper function to communicate with your backend. You can copy and paste the following code below: ```js +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() { try { @@ -271,79 +279,61 @@ async function sendPostRequest(url, data) { startCheckout(); + ``` Run your application to see whether the `Adyen Drop-in` is showing a list of payment methods. You'll notice that the Drop-in won't let you click `"Pay"` as we haven't implemented the `/payments` call yet. -Here are some troubleshooting notes if you do not see any payment methods: +Here are some helpful notes if you do not see any payment methods show up on your website (`http://.../checkout?type=dropin`): * **Empty response:** Have you configured any payment methods in the Customer Area? * **Invalid origin:** Have you added the correct origin URLs that allow your `Adyen Drop-in` to be loaded by the page? * **Unauthorized errors:** Have you specified your credentials correctly? -**Step 9.** Let's create the `/payments`-request ([see docs](https://docs.adyen.com/online-payments/build-your-integration/advanced-flow/?platform=Web&integration=Drop-in&version=5.63.0&programming_language=java#post-payments-request-web)) on the backend. +**Step 9.** Let's create the `/payments` request ([see docs](https://docs.adyen.com/online-payments/build-your-integration/advanced-flow/?platform=Web&integration=Drop-in&version=5.63.0&programming_language=java#post-payments-request-web)) on the backend. We start by defining a new endpoint `/api/payments` to which our frontend will send a request.
    Click to show me the answer ```java -@PostMapping("/api/payments") -public ResponseEntity payments(@RequestHeader String host, @RequestBody PaymentRequest body, HttpServletRequest request) throws IOException, ApiException { - 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 field basically means: Once done with the payment, where should the application redirect you? - paymentRequest.setReturnUrl(request.getScheme() + "://" + host + "/api/handleShopperRedirect?orderRef=" + orderRef); // Example: Turns into http://localhost:8080/api/handleShopperRedirect?orderRef=354fa90e-0858-4d2f-92b9-717cb8e18173 - - - log.info("PaymentsRequest {}", paymentRequest); - var response = paymentsApi.payments(paymentRequest); - log.info("PaymentsResponse {}", response); - return ResponseEntity.ok().body(response); -} -``` - -
    - + @PostMapping("/api/payments") + public ResponseEntity payments(@RequestHeader String host, @RequestBody PaymentRequest body, HttpServletRequest request) throws IOException, ApiException { + var paymentRequest = new PaymentRequest(); -**Step 10.** **Best practices:** The Adyen API supports idempotent requests, allowing you to retry a request multiple times while only performing the action once. This helps avoid unwanted duplication in case of failures and retries (e.g., you don't want to charge a shopper twice because they've hit the pay button two times, right?). -Add the idempotency key to your payment request, see [documentation](https://docs.adyen.com/development-resources/api-idempotency/). + var amount = new Amount() + .currency("EUR") + .value(9998L); + paymentRequest.setAmount(amount); + paymentRequest.setMerchantAccount(applicationConfiguration.getAdyenMerchantAccount()); + paymentRequest.setChannel(PaymentRequest.ChannelEnum.WEB); + paymentRequest.setPaymentMethod(body.getPaymentMethod()); -
    -Click to show me the answer + var orderRef = UUID.randomUUID().toString(); + paymentRequest.setReference(orderRef); + // The returnUrl field basically means: Once done with the payment, where should the application redirect you? + paymentRequest.setReturnUrl(request.getScheme() + "://" + host + "/api/handleShopperRedirect?orderRef=" + orderRef); // Example: Turns into http://localhost:8080/api/handleShopperRedirect?orderRef=354fa90e-0858-4d2f-92b9-717cb8e18173 -```java -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); + log.info("PaymentsRequest {}", paymentRequest); + var response = paymentsApi.payments(paymentRequest); + log.info("PaymentsResponse {}", response); + return ResponseEntity.ok().body(response); + } ```
    -**Step 11.** Let's send a request to our backend from our frontend, and modify the `adyenWebImplementation.js` to override the `onSubmit(...)` function to send a request to the `/api/payments` endpoint. +**Step 10.** Let's send a request to our backend from our frontend, and modify the `adyenWebImplementation.js` to override the `onSubmit(...)` function to send a request to the `/api/payments` endpoint. We've added the `onSubmit(...)` event handler here and the `handleResponse(response, component)` function to handle the response (which is performing a simple redirect to the right page).
    Click to show me the answer ```js +// ... async function startCheckout() { try { let paymentMethodsResponse = await sendPostRequest("/api/paymentMethods"); @@ -365,7 +355,7 @@ async function startCheckout() { }, } }, - // Step 11 onSubmit(...) + // Step 10 onSubmit(...) onSubmit: async (state, component) => { if (state.isValid) { const response = await sendPostRequest("/api/payments", state.data); @@ -383,7 +373,7 @@ async function startCheckout() { } } -// Step 11 - Handles responses, do a simple redirect based on the result. +// Step 10 - Handles responses, do a simple redirect based on the result. function handleResponse(response, component) { switch (response.resultCode) { case "Authorised": @@ -407,6 +397,26 @@ function handleResponse(response, component) {
    + +**Step 11.** **Best practices:** The Adyen API supports idempotent requests, allowing you to retry a request multiple times while only performing the action once. This helps avoid unwanted duplication in case of failures and retries (e.g., you don't want to charge a shopper twice because they've hit the pay button two times, right?). +Add the idempotency key to your payment request, see [documentation](https://docs.adyen.com/development-resources/api-idempotency/). + + +
    +Click to show me the answer + +```java + 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); +``` + +
    + You should now be able to make a payment! **However**, we're not there yet! This flow will fail when a challenge is presented to the shopper (Strong Customer Authentication). Let's handle this by adding 3D Secure 2 Authentication support. 3D Secure 2 is an authentication protocol (3DS2) that provides an additional layer of verification for card-not-present (CNP) transactions. To trigger 3DS2, we'll need to add several parameters to the `PaymentRequest` in the `/api/payments` endpoint. @@ -423,7 +433,7 @@ Go back to the `/controller/ApiController`, let's add the following parameters t * BrowserInfo * BillingAddress (due to risk rules, we recommend including the `BillingAddress`, even though it's optional). -**Note:** In this example, we use the redirect flow. You can also opt-in for the [Native flow](https://docs.adyen.com/online-payments/3d-secure/native-3ds2/web-drop-in/#make-a-payment). +**Note:** In this example, we implement the Redirect 3DS2 flow. You can also opt-in to implement the [Native 3DS2 flow](https://docs.adyen.com/online-payments/3d-secure/native-3ds2/web-drop-in/#make-a-payment), which we've also included (commented-out*) in the answer below.
    Click to show me the answer @@ -434,16 +444,23 @@ Go back to the `/controller/ApiController`, let's add the following parameters t var paymentRequest = new PaymentRequest(); // ... - // 3DS2 Redirect - Add the following additional parameters to your existing payment request + // 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"); @@ -465,26 +482,30 @@ Go back to the `/controller/ApiController`, let's add the following parameters t Click to show me the answer ```java -@PostMapping("/api/payments/details") -public ResponseEntity paymentsDetails(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException { - log.info("PaymentDetailsRequest {}", detailsRequest); - var response = paymentsApi.paymentsDetails(detailsRequest); - log.info("PaymentDetailsResponse {}", response); - return ResponseEntity.ok() - .body(response); -} + @PostMapping("/api/payments/details") + public ResponseEntity paymentsDetails(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException { + // Step 13. + log.info("PaymentDetailsRequest {}", detailsRequest); + var response = paymentsApi.paymentsDetails(detailsRequest); + log.info("PaymentDetailsResponse {}", response); + return ResponseEntity.ok() + .body(response); + } ```
    Next up, in our frontend, let's override the `onAdditionalDetails(...)` function in `adyenWebImplementation.js` to call `/api/payments/details`. -We've added the `onAdditionalDetails(...)` function in the `configuration` object and modified the `handleResponse(response, component)` function to allow the component to handle the challenge, see `component.handleAction(response.action)`.
    Click to show me the answer +We've added the `onAdditionalDetails(...)` function in the `configuration` object and modified the `handleResponse(response, component)` function to allow the component to handle the challenge, see `component.handleAction(response.action)`. + ```js +// ... + async function startCheckout() { try { let paymentMethodsResponse = await sendPostRequest("/api/paymentMethods"); @@ -561,7 +582,7 @@ function handleResponse(response, component) { ```java -// Handle redirect during payment. +// Step 14 - Handle redirect during payment. @GetMapping("/api/handleShopperRedirect") public RedirectView redirect(@RequestParam(required = false) String payload, @RequestParam(required = false) String redirectResult) throws IOException, ApiException { var paymentDetailsRequest = new PaymentDetailsRequest(); @@ -631,39 +652,39 @@ You can receive webhooks by enabling webhooks in the Customer Area, followed by Click to show me the answer ```java -@PostMapping("/webhooks") -public ResponseEntity webhooks(@RequestBody String json) throws Exception { - var notificationRequest = NotificationRequest.fromJson(json); - var notificationRequestItem = notificationRequest.getNotificationItems().stream().findFirst(); + @PostMapping("/webhooks") + public ResponseEntity webhooks(@RequestBody String json) throws Exception { + var notificationRequest = NotificationRequest.fromJson(json); + var notificationRequestItem = notificationRequest.getNotificationItems().stream().findFirst(); - try { - NotificationRequestItem item = notificationRequestItem.get(); + try { + NotificationRequestItem item = notificationRequestItem.get(); - if (!hmacValidator.validateHMAC(item, this.applicationConfiguration.getAdyenHmacKey())) { - log.warn("Could not validate HMAC signature for incoming webhook message: {}", item); + 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(); } - - // 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(); } -} ```
    diff --git a/src/main/java/com/adyen/workshop/configurations/DependencyInjectionConfiguration.java b/src/main/java/com/adyen/workshop/configurations/DependencyInjectionConfiguration.java index a9adfce..2875c50 100644 --- a/src/main/java/com/adyen/workshop/configurations/DependencyInjectionConfiguration.java +++ b/src/main/java/com/adyen/workshop/configurations/DependencyInjectionConfiguration.java @@ -18,20 +18,18 @@ 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); } @Bean PaymentsApi paymentsApi(){ - // Step 4 return new PaymentsApi(client()); } @Bean - HMACValidator hmacValidator() { - // Step 4 - return new HMACValidator(); - } -} + HMACValidator hmacValidator() { return new HMACValidator(); } +} \ No newline at end of file diff --git a/src/main/java/com/adyen/workshop/controllers/ApiController.java b/src/main/java/com/adyen/workshop/controllers/ApiController.java index 878b153..3e318b4 100644 --- a/src/main/java/com/adyen/workshop/controllers/ApiController.java +++ b/src/main/java/com/adyen/workshop/controllers/ApiController.java @@ -42,26 +42,119 @@ public ResponseEntity helloWorld() throws Exception { @PostMapping("/api/paymentMethods") public ResponseEntity paymentMethods() throws IOException, ApiException { // Step 7 - return null; + 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; + 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 field basically means: 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); + + 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 12 - var pay = new PaymentRequest(); - pay.setShopperInteraction(PaymentRequest.ShopperInteractionEnum.ECOMMERCE); - 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 13 - 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()); } } \ No newline at end of file diff --git a/src/main/java/com/adyen/workshop/controllers/WebhookController.java b/src/main/java/com/adyen/workshop/controllers/WebhookController.java index 3e276e0..642d42f 100644 --- a/src/main/java/com/adyen/workshop/controllers/WebhookController.java +++ b/src/main/java/com/adyen/workshop/controllers/WebhookController.java @@ -37,7 +37,35 @@ public WebhookController(ApplicationConfiguration applicationConfiguration, HMAC @PostMapping("/webhooks") public ResponseEntity webhooks(@RequestBody String json) throws Exception { - // Step 17 - return null; + 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 1bb1511..dbc3f53 100644 --- a/src/main/resources/static/adyenWebImplementation.js +++ b/src/main/resources/static/adyenWebImplementation.js @@ -1,16 +1,73 @@ 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", + }, + } + }, + 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 12 - Handles responses, do a simple redirect based on the result. function handleResponse(response, component) { - + // 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; + } + } } + // This function sends a POST request to your specified URL, // the `data`-parameters will be serialized as JSON in the body parameters. async function sendPostRequest(url, data) { diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index 6aa8dbf..d0f60ec 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -21,11 +21,18 @@ /> + + - + - + From 1492229e2540eb17bb07f04f684963cef4cec4d0 Mon Sep 17 00:00:00 2001 From: Kwok He Chu <105217051+Kwok-he-Chu@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:22:04 +0200 Subject: [PATCH 4/8] Added solution --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4f88350..5b2c26a 100644 --- a/README.md +++ b/README.md @@ -691,6 +691,8 @@ You can receive webhooks by enabling webhooks in the Customer Area, followed by Congratulations, you've successfully built an integration with Adyen! You can now add support for different [payment methods](https://docs.adyen.com/payment-methods/). +You can now compare your solution to the solution in the [workshop/solution branch](https://github.com/adyen-examples/adyen-step-by-step-integration-workshop/tree/workshop/solution/src). + **Step 18.** Enable [iDeal](https://docs.adyen.com/payment-methods/ideal/web-drop-in/). - Do not forget to enable the payment method in your [Customer Area](https://ca-test.adyen.com/) - On the frontend, modify your configuration so that it pre-selects an iDeal issuer + shows the images of the bank logos From 0de3ee83ac7c7a1fac6de4323c1757d895ff677c Mon Sep 17 00:00:00 2001 From: Kwok He Chu <105217051+Kwok-he-Chu@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:53:48 +0200 Subject: [PATCH 5/8] Sync README with main --- README.md | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5b2c26a..da4cbe6 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ In this workshop, you'll learn how to: **Step 0.** Build the project using `./gradlew bootRun` and see if it works. If you can visit `http://localhost:8080/hello-world`, `https://8080-adyenexampl-adyenstepby-xxxxxx21.ws-eu114.gitpod.io/hello-world` (Gitpod) or `https://xxxx.github.dev/hello-world` (codespaces), this means it works! -**Step 1.** [Get your Adyen Merchant Account](https://docs.adyen.com/account/manage-account-structure/#request-merchant-account) or use the existing one ending with `ECOM`. +**Step 1.** [Get your Adyen Merchant Account](https://docs.adyen.com/account/manage-account-structure/#request-merchant-account) or use an existing Merchant Account associated with your account (ending with -`ECOM`). **Step 2.** [Get your Adyen API Key](https://docs.adyen.com/development-resources/api-credentials/#generate-api-key). Ensure you've created the API Key on the Merchant Account level (e.g., you've selected your MerchantAccount and created credentials in the API Credentials page in the Customer Area). @@ -263,6 +263,11 @@ async function startCheckout() { } } +// Step 10 - Handles responses, do a simple redirect based on the result. +function handleResponse(response, component) { + +} + // This function sends a POST request to your specified URL, // the `data`-parameters will be serialized as JSON in the body parameters. async function sendPostRequest(url, data) { @@ -296,6 +301,7 @@ We start by defining a new endpoint `/api/payments` to which our frontend will s Click to show me the answer ```java + // Step 9 - Implement the /payments call to Adyen. @PostMapping("/api/payments") public ResponseEntity payments(@RequestHeader String host, @RequestBody PaymentRequest body, HttpServletRequest request) throws IOException, ApiException { var paymentRequest = new PaymentRequest(); @@ -355,7 +361,7 @@ async function startCheckout() { }, } }, - // Step 10 onSubmit(...) + // Step 10 onSubmit(...), this function is executed when you hit the 'Pay' button onSubmit: async (state, component) => { if (state.isValid) { const response = await sendPostRequest("/api/payments", state.data); @@ -439,27 +445,28 @@ Go back to the `/controller/ApiController`, let's add the following parameters t Click to show me the answer ```java - @PostMapping("/api/payments") + // Step 9 - Implement the /payments call to Adyen. + @PostMapping("/api/payments") public ResponseEntity payments(@RequestHeader String host, @RequestBody PaymentRequest body, HttpServletRequest request) throws IOException, ApiException { var paymentRequest = new PaymentRequest(); // ... - // Step 12 3DS2 Redirect - Add the following additional parameters to your existing payment request + // Step 12 3DS2 Redirect - Add the following additional parameters to your existing payment request for 3DS2 Redirect: + // 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 authenticationData = new AuthenticationData(); authenticationData.setAttemptAuthentication(AuthenticationData.AttemptAuthenticationEnum.ALWAYS); paymentRequest.setAuthenticationData(authenticationData); - // Add these lines to enable the Native 3DS2 flow: + // Add these lines, if you like to enable the Native 3DS2 flow: + // 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 //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"); @@ -482,6 +489,7 @@ Go back to the `/controller/ApiController`, let's add the following parameters t Click to show me the answer ```java + // Step 13 - Handle details call (triggered after Native 3DS2 flow) @PostMapping("/api/payments/details") public ResponseEntity paymentsDetails(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException { // Step 13. @@ -527,13 +535,14 @@ async function startCheckout() { }, } }, + // Step 10 onSubmit(...), this function is executed when you hit the 'Pay' button onSubmit: async (state, component) => { if (state.isValid) { const response = await sendPostRequest("/api/payments", state.data); handleResponse(response, component); } }, - // Step 13 onAdditionalDetails(...) + // Step 13 onAdditionalDetails(...), this function is executed when there's f.e. a Native 3DS2 flow onAdditionalDetails: async (state, component) => { const response = await sendPostRequest("/api/payments/details", state.data); handleResponse(response, component); @@ -549,6 +558,7 @@ async function startCheckout() { } } +// Step 10 - Handles responses, do a simple redirect based on the result. function handleResponse(response, component) { // Step 13 - If there's an action, handle it, otherwise redirect the user to the correct page based on the resultCode. if (response.action) { @@ -582,7 +592,7 @@ function handleResponse(response, component) { ```java -// Step 14 - Handle redirect during payment. +// 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 { var paymentDetailsRequest = new PaymentDetailsRequest(); @@ -660,6 +670,7 @@ You can receive webhooks by enabling webhooks in the Customer Area, followed by try { NotificationRequestItem item = notificationRequestItem.get(); + // Step 16 - Validate the HMAC signature using the ADYEN_HMAC_KEY if (!hmacValidator.validateHMAC(item, this.applicationConfiguration.getAdyenHmacKey())) { log.warn("Could not validate HMAC signature for incoming webhook message: {}", item); return ResponseEntity.unprocessableEntity().build(); @@ -693,6 +704,9 @@ Congratulations, you've successfully built an integration with Adyen! You can no You can now compare your solution to the solution in the [workshop/solution branch](https://github.com/adyen-examples/adyen-step-by-step-integration-workshop/tree/workshop/solution/src). + +In future versions of the workshop, we'll use this module (`Module 1`) as a base line. We'll then add different payment method in different modules, for now you can try enabling the following payment methods: + **Step 18.** Enable [iDeal](https://docs.adyen.com/payment-methods/ideal/web-drop-in/). - Do not forget to enable the payment method in your [Customer Area](https://ca-test.adyen.com/) - On the frontend, modify your configuration so that it pre-selects an iDeal issuer + shows the images of the bank logos @@ -704,4 +718,4 @@ You can now compare your solution to the solution in the [workshop/solution bran ## Contacting us -If you have any questions, feel free to contact us at devrel@adyen.com. \ No newline at end of file +If you have any questions, feel free to contact us at devrel@adyen.com. From f70bb42bee6ad6da8a74a819979ad8bc97f8c38b Mon Sep 17 00:00:00 2001 From: kwokhe Date: Thu, 3 Oct 2024 13:28:44 +0200 Subject: [PATCH 6/8] Add e2e test for workshop --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 5b95b3f..178b657 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -42,4 +42,4 @@ jobs: - name: Start workshop container run: docker run --rm -d --name workshop -p 8080:8080 -e ADYEN_API_KEY="${{ secrets.ADYEN_API_KEY }}" -e ADYEN_MERCHANT_ACCOUNT=${{ secrets.ADYEN_MERCHANT_ACCOUNT }} -e ADYEN_CLIENT_KEY=${{ secrets.ADYEN_CLIENT_KEY }} -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} workshop:latest - name: Run testing suite - run: docker run --rm --name adyen-testing-suite -e PLAYWRIGHT_FOLDERNAME=advanced-checkout -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} --network host ghcr.io/adyen-examples/adyen-testing-suite:main + run: docker run --rm --name adyen-testing-suite -e PLAYWRIGHT_FOLDERNAME=workshop -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} --network host ghcr.io/adyen-examples/adyen-testing-suite:main From 0eda2d362cd291e7d16006b652d8bad2b9a2e306 Mon Sep 17 00:00:00 2001 From: kwokhe Date: Thu, 3 Oct 2024 14:09:55 +0200 Subject: [PATCH 7/8] Fix E2E --- .github/workflows/e2e.yml | 9 ++++----- .../adyen/workshop/controllers/WebhookController.java | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 178b657..14f64f3 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -20,8 +20,7 @@ on: - LICENSE jobs: - checkout: - + adyen-step-by-step-integration-workshop: runs-on: ubuntu-latest steps: - name: Checkout project @@ -38,8 +37,8 @@ jobs: - name: Build project with Gradle run: ./gradlew build - name: Build project image - run: docker build -t workshop:latest workshop - - name: Start workshop container - run: docker run --rm -d --name workshop -p 8080:8080 -e ADYEN_API_KEY="${{ secrets.ADYEN_API_KEY }}" -e ADYEN_MERCHANT_ACCOUNT=${{ secrets.ADYEN_MERCHANT_ACCOUNT }} -e ADYEN_CLIENT_KEY=${{ secrets.ADYEN_CLIENT_KEY }} -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} workshop:latest + run: docker build -t adyen-step-by-step-integration-workshop:latest adyen-step-by-step-integration-workshop + - name: Start adyen-step-by-step-integration-workshop container + run: docker run --rm -d --name adyen-step-by-step-integration-workshop -p 8080:8080 -e ADYEN_API_KEY="${{ secrets.ADYEN_API_KEY }}" -e ADYEN_MERCHANT_ACCOUNT=${{ secrets.ADYEN_MERCHANT_ACCOUNT }} -e ADYEN_CLIENT_KEY=${{ secrets.ADYEN_CLIENT_KEY }} -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} adyen-step-by-step-integration-workshop:latest - name: Run testing suite run: docker run --rm --name adyen-testing-suite -e PLAYWRIGHT_FOLDERNAME=workshop -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} --network host ghcr.io/adyen-examples/adyen-testing-suite:main diff --git a/src/main/java/com/adyen/workshop/controllers/WebhookController.java b/src/main/java/com/adyen/workshop/controllers/WebhookController.java index 642d42f..1d622cb 100644 --- a/src/main/java/com/adyen/workshop/controllers/WebhookController.java +++ b/src/main/java/com/adyen/workshop/controllers/WebhookController.java @@ -37,6 +37,8 @@ public WebhookController(ApplicationConfiguration applicationConfiguration, HMAC @PostMapping("/webhooks") public ResponseEntity webhooks(@RequestBody String json) throws Exception { + // 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(); From 7bbe6e087375c625dac075887b6305e76afde213 Mon Sep 17 00:00:00 2001 From: kwokhe Date: Thu, 3 Oct 2024 14:20:54 +0200 Subject: [PATCH 8/8] Take root --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 14f64f3..8116e15 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -37,7 +37,7 @@ jobs: - name: Build project with Gradle run: ./gradlew build - name: Build project image - run: docker build -t adyen-step-by-step-integration-workshop:latest adyen-step-by-step-integration-workshop + run: docker build -t adyen-step-by-step-integration-workshop:latest . - name: Start adyen-step-by-step-integration-workshop container run: docker run --rm -d --name adyen-step-by-step-integration-workshop -p 8080:8080 -e ADYEN_API_KEY="${{ secrets.ADYEN_API_KEY }}" -e ADYEN_MERCHANT_ACCOUNT=${{ secrets.ADYEN_MERCHANT_ACCOUNT }} -e ADYEN_CLIENT_KEY=${{ secrets.ADYEN_CLIENT_KEY }} -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} adyen-step-by-step-integration-workshop:latest - name: Run testing suite