Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 149 additions & 143 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ repositories {

dependencies {
// [Step 1] Add the Adyen Java library here
implementation 'com.adyen:adyen-java-api-library:25.1.0'
implementation 'com.adyen:adyen-java-api-library:31.3.0'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public DependencyInjectionConfiguration(ApplicationConfiguration applicationConf
Client client() {
// Step 4
var config = new Config();
config.setApiKey(applicationConfiguration.getAdyenApiKey()); // We now use the Adyen API Key
config.setEnvironment(Environment.TEST); // Sets the environment to TEST
return new Client(config);
}

Expand Down
114 changes: 100 additions & 14 deletions src/main/java/com/adyen/workshop/controllers/ApiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.adyen.service.checkout.PaymentsApi;
import com.adyen.service.exception.ApiException;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.coyote.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
Expand All @@ -32,34 +31,121 @@ public ApiController(ApplicationConfiguration applicationConfiguration, Payments
this.paymentsApi = paymentsApi;
}

// Step 0
@GetMapping("/hello-world")
public ResponseEntity<String> helloWorld() throws Exception {
// Step 0
return ResponseEntity.ok()
.body("This is the 'Hello World' from the workshop - You've successfully finished step 0!");
return ResponseEntity.ok().body("This is the 'Hello World' from the workshop - You've successfully finished step 0!");
}

// Step 7
@PostMapping("/api/paymentMethods")
public ResponseEntity<PaymentMethodsResponse> 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);
}

// Step 9 - Implement the /payments call to Adyen.
@PostMapping("/api/payments")
public ResponseEntity<PaymentResponse> 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());

// 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 the following lines, if you want 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);

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);

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 + "/handleShopperRedirect"); // 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);
}

// Step 13 - Handle details call (triggered after Native 3DS2 flow)
@PostMapping("/api/payments/details")
public ResponseEntity<PaymentDetailsResponse> paymentsDetails(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException {
// Step 13
return null;
public ResponseEntity<PaymentDetailsResponse> 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);
}

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

PaymentCompletionDetails paymentCompletionDetails = new PaymentCompletionDetails();

// Handle redirect result or payload
if (redirectResult != null && !redirectResult.isEmpty()) {
// For redirect, you are redirected to an Adyen domain to complete the 3DS2 challenge
// After completing the 3DS2 challenge, you get the redirect result from Adyen in the returnUrl
// We then pass on the redirectResult
paymentCompletionDetails.redirectResult(redirectResult);
} else if (payload != null && !payload.isEmpty()) {
paymentCompletionDetails.payload(payload);
}

paymentDetailsRequest.setDetails(paymentCompletionDetails);

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

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

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

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();
}

// Success, log it for now
log.info("Received webhook with event {}", item.toString());

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();
}
}
}
160 changes: 141 additions & 19 deletions src/main/resources/static/adyenWebImplementation.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,151 @@
const clientKey = document.getElementById("clientKey").innerHTML;
const type = document.getElementById("type").innerHTML;
const { AdyenCheckout, Dropin } = window.AdyenWeb;

// Starts the (Adyen.Web) AdyenCheckout with your specified configuration by calling the `/paymentMethods` endpoint.
async function startCheckout() {
// Step 8
}
try {
// Step 8 - Retrieve the available payment methods
const paymentMethodsResponse = await fetch("/api/paymentMethods", {
method: "POST",
headers: {
"Content-Type": "application/json",
}
}).then(response => response.json());

// Step 10 - Handles responses, do a simple redirect based on the result.
function handleResponse(response, component) {
// We'll leave this empty for now and fix this in step 10.
}
const configuration = {
paymentMethodsResponse: paymentMethodsResponse,
clientKey,
locale: "en_US",
countryCode: 'NL',
environment: "test",
showPayButton: true,
translations: {
'en-US': {
'creditCard.securityCode.label': 'CVV/CVC'
}
},
// Step 10 - Add the onSubmit handler by telling it what endpoint to call when the pay button is pressed.
onSubmit: async (state, component, actions) => {
console.info("onSubmit", state, component, actions);
try {
if (state.isValid) {
const { action, order, resultCode } = await fetch("/api/payments", {
method: "POST",
body: state.data ? JSON.stringify(state.data) : "",
headers: {
"Content-Type": "application/json",
}
}).then(response => response.json());

if (!resultCode) {
console.warn("reject");
actions.reject();
}

actions.resolve({
resultCode,
action,
order
});
}
} catch (error) {
console.error(error);
actions.reject();
}
},
onPaymentCompleted: (result, component) => {
console.info("onPaymentCompleted", result, component);
handleOnPaymentCompleted(result, component);
},
onPaymentFailed: (result, component) => {
console.info("onPaymentFailed", result, component);
handleOnPaymentFailed(result, component);
},
onError: (error, component) => {
console.error("onError", error.name, error.message, error.stack, component);
window.location.href = "/result/error";
},
// Step 13 onAdditionalDetails(...) - Use this to finalize the payment, after Native 3DS2 authentication.
onAdditionalDetails: async (state, component, actions) => {
console.info("onAdditionalDetails", state, component);
try {
const { resultCode } = await fetch("/api/payments/details", {
method: "POST",
body: state.data ? JSON.stringify(state.data) : "",
headers: {
"Content-Type": "application/json",
}
}).then(response => response.json());

if (!resultCode) {
console.warn("reject");
actions.reject();
}

actions.resolve({ resultCode });
} catch (error) {
console.error(error);
actions.reject();
}
}
};

// 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) {
const res = await fetch(url, {
method: "POST",
body: data ? JSON.stringify(data) : "",
headers: {
"Content-Type": "application/json",
},
});
const paymentMethodsConfiguration = {
card: {
showBrandIcon: true,
hasHolderName: true,
holderNameRequired: true,
name: "Credit or debit card",
amount: {
value: 9998,
currency: "EUR",
},
placeholders: {
cardNumber: '1234 5678 9012 3456',
expiryDate: 'MM/YY',
securityCodeThreeDigits: '123',
securityCodeFourDigits: '1234',
holderName: 'Developer Relations Team'
}
}
};

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

// Step 10 - Function to handle payment completion redirects
function handleOnPaymentCompleted(response) {
switch (response.resultCode) {
case "Authorised":
window.location.href = "/result/success";
break;
case "Pending":
case "Received":
window.location.href = "/result/pending";
break;
default:
window.location.href = "/result/error";
break;
}
}

return await res.json();
// Step 10 - Function to handle payment failure redirects
function handleOnPaymentFailed(response) {
switch (response.resultCode) {
case "Cancelled":
case "Refused":
window.location.href = "/result/failed";
break;
default:
window.location.href = "/result/error";
break;
}
}

startCheckout();
startCheckout();
Loading