From a866a551371aaa81d3a99ac5271d38c83947d340 Mon Sep 17 00:00:00 2001 From: Tyler Douglas Date: Fri, 3 Apr 2020 17:59:27 -0700 Subject: [PATCH] initial commit --- .idea/.gitignore | 2 + .idea/compiler.xml | 6 + .idea/gradle.xml | 18 + .idea/jarRepositories.xml | 20 + .idea/misc.xml | 7 + .idea/uiDesigner.xml | 124 +++++ .idea/vcs.xml | 6 + build.gradle | 23 + settings.gradle | 1 + src/.DS_Store | Bin 0 -> 6148 bytes src/main/.DS_Store | Bin 0 -> 6148 bytes src/main/java/controller/FrontendParser.java | 64 +++ src/main/java/controller/Main.java | 183 ++++++ .../PaymentMethodDetailsDeserializer.java | 19 + src/main/java/model/PaymentMethods.java | 43 ++ src/main/java/model/Payments.java | 48 ++ src/main/java/model/PaymentsDetails.java | 51 ++ src/main/java/view/CustomResourceLocator.java | 41 ++ src/main/java/view/RenderUtil.java | 18 + src/main/resources/.DS_Store | Bin 0 -> 6148 bytes src/main/resources/static/css/main.css | 527 ++++++++++++++++++ src/main/resources/static/img/.DS_Store | Bin 0 -> 6148 bytes src/main/resources/static/img/bin.svg | 3 + src/main/resources/static/img/failure.svg | 4 + src/main/resources/static/img/favicon.ico | Bin 0 -> 1150 bytes src/main/resources/static/img/headphones.svg | 9 + .../resources/static/img/mystore-logo.svg | 10 + src/main/resources/static/img/success.svg | 4 + src/main/resources/static/img/sunglasses.svg | 9 + src/main/resources/static/img/thank-you.svg | 11 + .../static/js/adyen_implementations.js | 174 ++++++ src/main/resources/templates/cart.html | 31 ++ .../resources/templates/checkout-failed.html | 13 + .../resources/templates/checkout-success.html | 14 + src/main/resources/templates/component.html | 28 + src/main/resources/templates/error.html | 18 + .../templates/fetch-payment-data.html | 37 ++ src/main/resources/templates/home.html | 95 ++++ src/main/resources/templates/layout.html | 25 + 39 files changed, 1686 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 .idea/vcs.xml create mode 100644 build.gradle create mode 100644 settings.gradle create mode 100644 src/.DS_Store create mode 100644 src/main/.DS_Store create mode 100644 src/main/java/controller/FrontendParser.java create mode 100644 src/main/java/controller/Main.java create mode 100644 src/main/java/model/PaymentMethodDetailsDeserializer.java create mode 100644 src/main/java/model/PaymentMethods.java create mode 100644 src/main/java/model/Payments.java create mode 100644 src/main/java/model/PaymentsDetails.java create mode 100644 src/main/java/view/CustomResourceLocator.java create mode 100644 src/main/java/view/RenderUtil.java create mode 100644 src/main/resources/.DS_Store create mode 100644 src/main/resources/static/css/main.css create mode 100644 src/main/resources/static/img/.DS_Store create mode 100644 src/main/resources/static/img/bin.svg create mode 100644 src/main/resources/static/img/failure.svg create mode 100644 src/main/resources/static/img/favicon.ico create mode 100644 src/main/resources/static/img/headphones.svg create mode 100644 src/main/resources/static/img/mystore-logo.svg create mode 100644 src/main/resources/static/img/success.svg create mode 100644 src/main/resources/static/img/sunglasses.svg create mode 100644 src/main/resources/static/img/thank-you.svg create mode 100644 src/main/resources/static/js/adyen_implementations.js create mode 100644 src/main/resources/templates/cart.html create mode 100644 src/main/resources/templates/checkout-failed.html create mode 100644 src/main/resources/templates/checkout-success.html create mode 100644 src/main/resources/templates/component.html create mode 100644 src/main/resources/templates/error.html create mode 100644 src/main/resources/templates/fetch-payment-data.html create mode 100644 src/main/resources/templates/home.html create mode 100644 src/main/resources/templates/layout.html diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..5c98b42 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..e540bae --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..13a8247 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..fdc392f --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..bc8d0a3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..289acbf --- /dev/null +++ b/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'application' +} + +version "0.1" + +sourceCompatibility = 1.8 + +application { + mainClassName = 'controller.Main' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "com.sparkjava:spark-core:2.8.0" + implementation "org.slf4j:slf4j-simple:1.7.25" + implementation "com.sparkjava:spark-template-jinjava:2.7.1" + implementation 'org.apache.httpcomponents:httpclient:4.5.11' + implementation 'com.adyen:adyen-java-api-library:4.0.1' +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..4d1ff7b --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'adyen-java-online-payments' \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4ec8794e6b2f472772a60028762346a326f5254f GIT binary patch literal 6148 zcmeHK%}T>S5Z-O0rihq>V2`!Gbb#e)!HJ$Mr$dQfQ-Qfweiq)81LBY6#dBcH(6 zab|a0ELHF%VrO9Xo1LB6Wxs@-UB(!1rF!N3a?7iKCqNwTHJFTPB{^;Q`e=43|B&!^LKp~q7r|=5KY=igCI11zF z7JSqAX&fOjKnxHAOT>UW0`>JJ+B0pH7$63I#sHoVCMcq%u#_l{4ruWDh~pX}3fTCT zKr|Iv3QLJF0>Wh~piJfJiNR$$_)QgODJ&(*bjHQXFppZ9s}~9vtApPZ;fz}nsU-%8 zfq4e1vTI=dKl%RsKc7TBVt^RvV0{lB%r>o5Nabg7!gCFs_t1O94ZaVu;03 cTn7~beiIEqOJOMyJRo!t5HwIj4E!ns9|ky3nE(I) literal 0 HcmV?d00001 diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..46afb288789d40620780df7bef24487fd1389ee6 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8CWx4WV2`!Gbb#e)!HJ$Mr$dQfQ-TWla^B}olhD|rolBcH(6 zab|a0EUgz0B4q|Q~4C7uZ$MICa zMCn-7(de*Iui4G!{q|t!@Jf7?Si6UUZHSW k0*1VbAs4UWDyS6j+h_nf8ViNs0ihoONdq;+z@IYk1(>~4<^TWy literal 0 HcmV?d00001 diff --git a/src/main/java/controller/FrontendParser.java b/src/main/java/controller/FrontendParser.java new file mode 100644 index 0000000..8dd43a0 --- /dev/null +++ b/src/main/java/controller/FrontendParser.java @@ -0,0 +1,64 @@ +package controller; + +import com.adyen.model.PaymentResult; +import com.adyen.model.checkout.*; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import model.PaymentMethodDetailsDeserializer; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; + +public class FrontendParser { + + // Deserialize payment information passed from frontend. Requires TypeAdapter for PaymentMethodDetails interface + public static PaymentsRequest parsePayment(String body) { + + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(PaymentMethodDetails.class, new PaymentMethodDetailsDeserializer()); + Gson gson = builder.create(); + PaymentsRequest paymentsRequest = gson.fromJson(body, PaymentsRequest.class); + return paymentsRequest; + + + } + + // Deserialize PaymentDetails generated by component + public static PaymentsDetailsRequest parseDetails(String body) { + + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(PaymentMethodDetails.class, new PaymentMethodDetailsDeserializer()); + Gson gson = builder.create(); + PaymentsDetailsRequest paymentDetailsRequest = gson.fromJson(body, PaymentsDetailsRequest.class); + return paymentDetailsRequest; + } + + // Format response being passed back to frontend. Only leave resultCode and action. Don't need to pass back + // The rest of the information + public static PaymentsResponse formatResponseForFrontend(PaymentsResponse unformattedResponse) throws IOException { + + PaymentsResponse.ResultCodeEnum resultCode = unformattedResponse.getResultCode(); + if (resultCode != null) { + PaymentsResponse newPaymentsResponse = new PaymentsResponse(); + newPaymentsResponse.setResultCode(resultCode); + + CheckoutPaymentsAction action = unformattedResponse.getAction(); + if (action != null) { + newPaymentsResponse.setAction(action); + } + return newPaymentsResponse; + } else { + throw new IOException(); + } + } + + // Doesn't handle nested objects right now. Will see if neccessary + public static List parseQueryParams(String queryString) { + List params = URLEncodedUtils.parse(queryString, Charset.forName("UTF-8")); + + return params; + } +} diff --git a/src/main/java/controller/Main.java b/src/main/java/controller/Main.java new file mode 100644 index 0000000..c3c934e --- /dev/null +++ b/src/main/java/controller/Main.java @@ -0,0 +1,183 @@ +package controller; + +import com.adyen.model.checkout.PaymentDetails; +import com.adyen.model.checkout.PaymentsDetailsRequest; +import com.adyen.model.checkout.PaymentsRequest; +import com.adyen.model.checkout.PaymentsResponse; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closeables; +import com.google.common.net.MediaType; +import org.apache.http.NameValuePair; +import view.RenderUtil; + +import java.io.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +import model.PaymentMethods; +import model.Payments; +import model.PaymentsDetails; + +import static spark.Spark.*; +import spark.Response; + + +public class Main { + + private static final File FAVICON_PATH = new File("src/main/resources/static/img/favicon.ico"); + + public static String merchantAccount = ""; + public static String apiKey = ""; + public static String originKey = ""; + public static String paymentMethodsUrl = ""; + public static String paymentsUrl = ""; + public static String paymentsDetailsUrl = ""; + + public static void main(String[] args) { + port(8080); + staticFiles.location("/static"); + initalizeConstants(); + + // Routes + get("/", (req, res) -> { + Map context = new HashMap<>(); + return RenderUtil.render(context, "templates/home.html"); + }); + + get("/cart/:integration", (req, res) -> { + String integrationType = req.params(":integration"); + + Map context = new HashMap<>(); + context.put("integrationType", "/checkout/" + integrationType); + + return RenderUtil.render(context, "templates/cart.html"); + }); + + get("/checkout/:integration", (req, res) -> { + String integrationType = req.params(":integration"); + + Map context = new HashMap<>(); + context.put("paymentMethods", PaymentMethods.getPaymentMethods()); + context.put("originKey", originKey); + context.put("integrationType", integrationType); + + return RenderUtil.render(context, "templates/component.html"); + }); + + post("/api/getPaymentMethods", (req, res) -> { + String paymentMethods = PaymentMethods.getPaymentMethods(); + return paymentMethods; + }); + + post("/api/initiatePayment", (req, res) -> { + PaymentsRequest request = FrontendParser.parsePayment(req.body()); + String response = Payments.makePayment(request); + + return response; + }); + + post("/api/submitAdditionalDetails", (req, res) -> { + PaymentsDetailsRequest details = FrontendParser.parseDetails(req.body()); + String response = PaymentsDetails.getPaymentsDetails(details); + + return response; + }); + + get("/api/handleShopperRedirect", (req, res) -> { + System.out.println("GET result\n" + req.body()); + res.redirect("/error"); //TODO: Evaluate contents of res at this point to handle redirect + return res; + }); + + post("/api/handleShopperRedirect", (req, res) -> { + System.out.println("POST result\n" + req.body() + "\n"); + + // Triggers when POST contains query params. Triggers on call back from issuer after 3DS2 challenge w/ MD & PaRes + if (req.body().contains("&")) { + List params = FrontendParser.parseQueryParams(req.body()); + System.out.println(params.toString()); + String md = params.get(0).getValue(); + String paRes = params.get(1).getValue(); + + Map context = new HashMap<>(); + context.put("MD", md); + context.put("PaRes", paRes); + return RenderUtil.render(context, "templates/fetch-payment-data.html"); // Get paymentData from localStorage + + } else { + PaymentsDetailsRequest pdr = FrontendParser.parseDetails(req.body()); + + PaymentsResponse paymentResult = PaymentsDetails.getPaymentsDetailsObject(pdr); + PaymentsResponse.ResultCodeEnum result = paymentResult.getResultCode(); + + switch (result) { + case AUTHORISED: + res.redirect("/success"); + case RECEIVED: case PENDING: + res.redirect("/pending"); + default: + res.redirect("/failed"); + } + return res; + } + }); + + get("/success", (req, res) -> { + Map context = new HashMap<>(); + return RenderUtil.render(context, "templates/checkout-success.html"); + }); + + get("/failed", (req, res) -> { + Map context = new HashMap<>(); + return RenderUtil.render(context, "templates/checkout-failed.html"); + }); + + get("/pending", (req, res) -> { + Map context = new HashMap<>(); + return RenderUtil.render(context, "templates/checkout-success.html"); + }); + + get("/error", (req, res) -> { + Map context = new HashMap<>(); + return RenderUtil.render(context, "templates/checkout-failed.html"); + }); + + get("/favicon.ico", (req, res) -> { + return getFavicon(res); + }); + } + + private static void initalizeConstants() { + merchantAccount = "TylerDouglas"; + apiKey = "AQEyhmfxL4jIYhVBw0m/n3Q5qf3VaY9UCJ1+XWZe9W27jmlZiiSaWGoa4mOFeQne5hiuhQsQwV1bDb7kfNy1WIxIIkxgBw==-hJYG90gqLYPclLs6We+q8CUtAsa+KgXr/iWftd+rrCM=-89EKHpWfW8ABmGF3"; + originKey = "pub.v2.8115499067697722.aHR0cDovL2xvY2FsaG9zdDo4MDgw.I4ixvXum4JGOjgI0Nd3YQ49P4AWvIncxMv41suCoW1Y"; + paymentMethodsUrl = "https://checkout-test.adyen.com/v52/paymentMethods"; + paymentsUrl = "https://checkout-test.adyen.com/v52/payments"; + paymentsDetailsUrl = "https://checkout-test.adyen.com/v52/payments/details"; + } + + private static Object getFavicon(Response res) { + try { + InputStream in = null; + OutputStream out; + try { + in = new BufferedInputStream(new FileInputStream(FAVICON_PATH)); + out = new BufferedOutputStream(res.raw().getOutputStream()); + res.raw().setContentType(MediaType.ICO.toString()); + ByteStreams.copy(in, out); + out.flush(); + return ""; + } finally { + Closeables.close(in, true); + } + } catch (FileNotFoundException ex) { + res.status(404); + return ex.getMessage(); + } catch (IOException ex) { + res.status(500); + return ex.getMessage(); + } + } +} diff --git a/src/main/java/model/PaymentMethodDetailsDeserializer.java b/src/main/java/model/PaymentMethodDetailsDeserializer.java new file mode 100644 index 0000000..1abe0bc --- /dev/null +++ b/src/main/java/model/PaymentMethodDetailsDeserializer.java @@ -0,0 +1,19 @@ +package model; + +import com.adyen.model.checkout.DefaultPaymentMethodDetails; +import com.adyen.model.checkout.PaymentMethodDetails; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; + +public class PaymentMethodDetailsDeserializer implements JsonDeserializer { + + @Override + public PaymentMethodDetails deserialize(JsonElement jsonElement, Type typeOfT, + JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + return jsonDeserializationContext.deserialize(jsonElement, DefaultPaymentMethodDetails.class); + } +} \ No newline at end of file diff --git a/src/main/java/model/PaymentMethods.java b/src/main/java/model/PaymentMethods.java new file mode 100644 index 0000000..3ce536b --- /dev/null +++ b/src/main/java/model/PaymentMethods.java @@ -0,0 +1,43 @@ +package model; + +import com.adyen.Client; +import com.adyen.enums.Environment; +import com.adyen.model.Amount; +import com.adyen.model.checkout.PaymentMethodDetails; +import com.adyen.model.checkout.PaymentMethodsRequest; +import com.adyen.model.checkout.PaymentMethodsResponse; +import com.adyen.service.Checkout; +import com.adyen.service.exception.ApiException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import controller.Main; + +import java.io.IOException; + +public class PaymentMethods { + + public static String getPaymentMethods() { + Client client = new Client(Main.apiKey, Environment.TEST); + Checkout checkout = new Checkout(client); + + PaymentMethodsRequest paymentMethodsRequest = new PaymentMethodsRequest(); + paymentMethodsRequest.setMerchantAccount(Main.merchantAccount); + paymentMethodsRequest.setCountryCode("NL"); + Amount amount = new Amount(); + amount.setCurrency("EUR"); + amount.setValue(1000L); + paymentMethodsRequest.setAmount(amount); + paymentMethodsRequest.setChannel(PaymentMethodsRequest.ChannelEnum.WEB); + System.out.println("/paymentMethods context:\n" + paymentMethodsRequest.toString()); + + try { + PaymentMethodsResponse response = checkout.paymentMethods(paymentMethodsRequest); + Gson gson = new GsonBuilder().create(); + String paymentMethodsResponseStringified = gson.toJson(response); + System.out.println("/paymentMethods response:\n" + paymentMethodsResponseStringified); + return paymentMethodsResponseStringified; + } catch (ApiException | IOException e) { + return e.toString(); + } + } +} diff --git a/src/main/java/model/Payments.java b/src/main/java/model/Payments.java new file mode 100644 index 0000000..6db5c9b --- /dev/null +++ b/src/main/java/model/Payments.java @@ -0,0 +1,48 @@ +package model; + +import com.adyen.Client; +import com.adyen.enums.Environment; +import com.adyen.model.Address; +import com.adyen.model.Amount; +import com.adyen.model.checkout.PaymentsRequest; +import com.adyen.model.checkout.PaymentsResponse; +import com.adyen.service.Checkout; +import com.adyen.service.exception.ApiException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import controller.FrontendParser; +import controller.Main; + +import java.io.IOException; + +public class Payments { + + public static String makePayment(PaymentsRequest paymentsRequest) { + Client client = new Client(Main.apiKey, Environment.TEST); + Checkout checkout = new Checkout(client); + + paymentsRequest.setMerchantAccount(Main.merchantAccount); + paymentsRequest.setCountryCode("NL"); + Amount amount = new Amount(); + amount.setCurrency("EUR"); + amount.setValue(1000L); + paymentsRequest.setAmount(amount); + paymentsRequest.setReference("Test Reference"); + paymentsRequest.setReturnUrl("http://localhost:8080/api/handleShopperRedirect"); + paymentsRequest.setChannel(PaymentsRequest.ChannelEnum.WEB); + System.out.println("/paymentMethods response:\n" + paymentsRequest.toString()); + + try { + PaymentsResponse response = checkout.payments(paymentsRequest); + PaymentsResponse formattedResponse = FrontendParser.formatResponseForFrontend(response); + + GsonBuilder builder = new GsonBuilder(); + Gson gson = builder.create(); + String paymentsResponse = gson.toJson(formattedResponse); + System.out.println("/payments response:\n" + paymentsResponse); + return paymentsResponse; + } catch (ApiException | IOException e) { + return e.toString(); + } + } +} diff --git a/src/main/java/model/PaymentsDetails.java b/src/main/java/model/PaymentsDetails.java new file mode 100644 index 0000000..b0030bd --- /dev/null +++ b/src/main/java/model/PaymentsDetails.java @@ -0,0 +1,51 @@ +package model; + +import com.adyen.Client; +import com.adyen.enums.Environment; +import com.adyen.model.checkout.PaymentsDetailsRequest; +import com.adyen.model.checkout.PaymentsResponse; +import com.adyen.service.Checkout; +import com.adyen.service.exception.ApiException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import controller.Main; + +import java.io.IOException; +import java.util.HashMap; + +public class PaymentsDetails { + + public static String getPaymentsDetails(PaymentsDetailsRequest paymentsDetailsRequest) { + + PaymentsResponse paymentsDetailsResponse = makePaymentDetailsRequest(paymentsDetailsRequest); + + Gson gson = new GsonBuilder().create(); + return gson.toJson(paymentsDetailsResponse); + } + + + public static PaymentsResponse getPaymentsDetailsObject(PaymentsDetailsRequest paymentsDetailsRequest) { + + return makePaymentDetailsRequest(paymentsDetailsRequest); + } + + + private static PaymentsResponse makePaymentDetailsRequest(PaymentsDetailsRequest paymentsDetailsRequest) { + Client client = new Client(Main.apiKey, Environment.TEST); + Checkout checkout = new Checkout(client); + + System.out.println("/paymentsDetails request:" + paymentsDetailsRequest.toString()); + PaymentsResponse paymentsDetailsResponse = null; + try { + paymentsDetailsResponse = checkout.paymentsDetails(paymentsDetailsRequest); + + } catch (ApiException | IOException e) { + e.printStackTrace(); + } finally { + if (paymentsDetailsResponse != null) { + System.out.println("paymentsDetails response:\n" + paymentsDetailsResponse.toString()); + } + } + return paymentsDetailsResponse; + } +} diff --git a/src/main/java/view/CustomResourceLocator.java b/src/main/java/view/CustomResourceLocator.java new file mode 100644 index 0000000..6642ee4 --- /dev/null +++ b/src/main/java/view/CustomResourceLocator.java @@ -0,0 +1,41 @@ +package view; + +import com.google.common.io.Files; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.loader.ResourceLocator; +import com.hubspot.jinjava.loader.ResourceNotFoundException; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; + +public class CustomResourceLocator implements ResourceLocator { + + public static final String jinjaRoot = "src/main/resources/templates/"; + private File baseDir; + + public CustomResourceLocator() { + this.baseDir = new File("."); + } + + private File resolveFileName(String name) { + File f = new File(name); + + if (f.isAbsolute()) { + return f; + } + + return new File(baseDir, name); + } + + @Override + public String getString(String name, Charset encoding, JinjavaInterpreter interpreter) throws IOException { + File file = resolveFileName(jinjaRoot + name); + + if (!file.exists() || !file.isFile()) { + throw new ResourceNotFoundException("Couldn't find resource: " + file); + } + + return Files.toString(file, encoding); + } +} diff --git a/src/main/java/view/RenderUtil.java b/src/main/java/view/RenderUtil.java new file mode 100644 index 0000000..8ca0aa5 --- /dev/null +++ b/src/main/java/view/RenderUtil.java @@ -0,0 +1,18 @@ +package view; + +import java.util.Map; + +import com.hubspot.jinjava.JinjavaConfig; +import spark.ModelAndView; +import spark.template.jinjava.JinjavaEngine; + + +public class RenderUtil { + + public static String render(Map model, String templatePath) { + JinjavaConfig config = new JinjavaConfig(); + CustomResourceLocator customResourceLocator = new CustomResourceLocator(); + + return new JinjavaEngine(config, customResourceLocator).render(new ModelAndView(model, templatePath)); + } +} diff --git a/src/main/resources/.DS_Store b/src/main/resources/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..851e17c2815eecaf209e4e54de6fd7c87ee9ca6f GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8rihq>V2`!Gbb#e)!HJ$Mr$dQfQ-Q*0n6rAZAMBY6#dBcH(6 zab|a02-SlZ5jz93-|XznF8d|y>@vo5MTA6tPl=<_p1b)CH+14EQnHDSOUeUL~^}enKIe3TN;JMro4|_9P6V z@DBX5oLMX(F+dCu153bwI{LKrCD`iSE-L=>>` zErDn#G!+&SVFZNBR6v=^)f0oubnqK0&Qw@Pl;J>A;K9qB~nWa z5CdffsX1D~TEQf2@E literal 0 HcmV?d00001 diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css new file mode 100644 index 0000000..2c352be --- /dev/null +++ b/src/main/resources/static/css/main.css @@ -0,0 +1,527 @@ +/* General page body */ + +html, +body { + width: 100%; + margin: 0; + font-family: "Fakt", sans-serif, Helvetica, Arial; +} + +*, +:after, +:before { + box-sizing: border-box; +} + +a, +u { + text-decoration: none; +} + +a:hover { + text-decoration: none; +} + +.hidden { + display: none; +} + +#header { + background: #fff; + border-bottom: 1px solid #e6e9eb; + height: 44px; + left: 0; + margin-bottom: 24px; + padding: 14px 26px; + position: fixed; + text-align: center; + top: 0; + width: 100%; + z-index: 2; + box-sizing: border-box; +} + +/* Buttons */ + +.button { + background: #00112c; + border: 0; + border-radius: 6px; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: 1em; + font-weight: 500; + margin: 0; + padding: 15px; + text-align: center; + transition: background 0.3s ease-out, box-shadow 0.3s ease-out; + width: 100%; +} + +.button:hover { + background: #1c3045; + box-shadow: 0 3px 4px rgba(0, 15, 45, 0.2); +} + +.button:active { + background: #3a4a5c; +} + +.button:disabled { + background: #e6e9eb; + box-shadow: none; + cursor: not-allowed; + -webkit-user-select: all; + -moz-user-select: all; + -ms-user-select: all; + user-select: all; +} + +/* end General page body */ + +/* Index page */ + +.main-container { + margin: auto; + max-width: 1048px; + padding: 0 16px; + display: flex; + flex-direction: column; +} + +.integration-list { + display: flex; + margin-top: 6%; + max-width: 1048px; + flex-wrap: wrap; + justify-content: space-between; + list-style: none; + margin: 0; + padding: 0; +} + +@media (min-width: 768px) { + .integration-list { + margin-left: -8px; + margin-bottom: -8px; + margin-right: -8px; + } +} + +@media (min-width: 1024px) { + .integration-list { + margin-left: -16px; + margin-bottom: -16px; + margin-right: -16px; + } +} + +.integration-list-item { + background: #f7f8f9; + border-radius: 6px; + flex: 1 1 0; + margin: 4px; + min-width: 40%; + position: relative; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid #f7f8f9; +} + +.integration-list-item:hover { + box-shadow: 0 16px 24px 0 #e5eaef; + text-decoration: none; + border: 1px solid #06f; +} + +@media (min-width: 768px) { + .integration-list-item { + margin-left: 16px; + margin-bottom: 16px; + margin-right: 16px; + margin-top: 16px; + min-width: 25%; + } +} + +.integration-list-item-link { + padding: 20px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +@media (min-width: 768px) { + .integration-list-item-link { + padding-left: 28px; + padding-bottom: 28px; + padding-right: 28px; + padding-top: 28px; + } +} + +.integration-list-item-title { + margin: 0; + text-align: center; + color: #00112c; + font-size: 1em; + font-weight: 700; + margin: 10px 0 0; +} + +@media (min-width: 768px) { + .integration-list-item-title { + font-size: 24px; + margin-left: 0; + margin-bottom: 6px; + margin-right: 0; + } +} + +.integration-list-item-subtitle { + color: #00112c; + font-size: 0.67em; + font-weight: 700; + margin: 10px 0 0; +} + +@media (min-width: 768px) { + .integration-list-item-subtitle { + font-size: 16px; + margin-left: 0; + margin-bottom: 6px; + margin-right: 0; + } +} + +.title-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.info { + margin-top: 10%; + color: #00112c; +} + +/* end Index page */ + +/* Cart preview page */ + +.shopping-cart { + float: right; +} +@media (min-width: 768px) { + .shopping-cart { + padding-left: 0; + padding-bottom: 0; + padding-right: 0; + padding-top: 3px; + } +} +.shopping-cart-link { + display: inline-block; + position: relative; +} +.order-summary-list { + border-top: 1px solid #e6e9eb; +} +.order-summary-list-list-item { + border-bottom: 1px solid #e6e9eb; + display: flex; + height: 97px; +} +.order-summary-list-list-item-image { + height: 64px; + margin: 16px; + width: 64px; +} +.order-summary-list-list-item-title { + font-weight: 700; + margin: auto auto auto 0; +} +.order-summary-list-list-item-price { + color: #687282; + margin: auto 16px; + text-align: right; + width: 80px; +} +@media (min-width: 768px) { + .order-summary-list-list-item-price { + margin-left: 24px; + margin-bottom: auto; + margin-right: 24px; + margin-top: auto; + } +} +.order-summary-list-list-item-remove-product { + background: none; + border: 0; + cursor: pointer; + height: 25px; + margin: auto 0; + padding: 0; + width: 25px; +} +.cart { + text-align: center; + margin-top: 80px; +} +.cart-footer { + font-weight: 700; + margin-top: 17px; + text-align: right; +} +@media (min-width: 768px) { + .cart-footer { + margin-top: 24px; + } +} +.cart-footer .button { + margin-top: 30px; + width: 100%; +} +@media (min-width: 768px) { + .cart-footer .button { + margin-top: 0; + width: 288px; + } +} +.cart-footer-amount { + margin-left: 16px; + margin-right: 24px; +} +.whole-preview { + margin: auto; + max-width: 1110px; + padding: 0 16px; +} +@media (min-width: 1440px) { + .whole-preview { + padding-left: 0; + padding-bottom: 0; + padding-right: 0; + padding-top: 0; + } +} + +/* end of Cart preview page */ + +/* Payment page */ + +#payment-page { + display: flex; + flex-direction: column; + align-items: center; +} + +#payment-page .container { + margin-top: 100px; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + max-width: 1110px; + padding-left: 8px; + padding-right: 8px; +} + +.checkout-component { + background: #f7f8f9; + border: 1px solid #e6e9eb; + border-radius: 12px; + margin: 8px 0; +} + +/* Adyen Components */ +.payment { + width: 100%; + padding-top: 0px !important; + padding-left: 20px; + padding-right: 20px; +} + +@media screen and (max-width: 1076px) { + #payment-page .container { + display: flex; + flex-direction: column; + align-items: center; + } + + .payment { + align-self: center; + max-width: 610px; + } +} + +.payment-container { + display: flex; + justify-content: center; + background: #f7f8f9; + border: 1px solid #e6e9eb; + border-radius: 12px; + padding-top: 18px; + padding-bottom: 18px; + width: 100%; + max-width: 450px; + height: 100%; +} + +@media screen and (max-width: 1076px) { + .payment-container { + align-self: center; + max-width: 610px; + } +} + +/* end Payments page */ + +/* Dropin page */ + +#dropin { + width: 100%; + max-width: 450px; +} + +@media screen and (max-width: 1076px) { + #dropin { + width: 100%; + max-width: 610px; + } +} + +/* end Dropin page */ + +/* Customer billing information form */ + +.address { + background: #f7f8f9; + border: 1px solid #e6e9eb; + border-radius: 12px; +} + +.billing-header { + padding: 1em 1em 0px 1em; +} + +.address-form { + padding: 1em; + border-radius: 3px; +} + +.address-form * { + box-sizing: border-box; +} + +.address-form input { + border: 1px solid #ccc; + border-radius: 6px; + font-size: 0.8em; + padding: 0.5em; +} + +.address-form input[type="text"] { + width: 100%; + height: 40px; +} + +.address-label { + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.1s ease-out; + white-space: nowrap; + font-size: 0.81em; + font-weight: 400; + line-height: 13px; + padding-bottom: 5px; +} + +.address-input { + display: inline-grid; + width: 100%; +} + +.address-input:first-child { + padding-right: 4px; +} +.address-input:last-child { + padding-left: 4px; +} +.address-input.full-width { + padding-left: 0px; + padding-right: 0px; +} + +.address-line { + padding: 4px; + display: flex; + flex-direction: row; +} + +.address-line4 div { + width: 33.3%; +} + +.customer-form { + width: 100%; + max-width: 610px; + margin-bottom: 16px; +} + +@media screen and (max-width: 1076px) { + .customer-form { + align-self: center; + max-width: 610px; + } +} + +/* end Customer billing information page */ + +/* Status page */ + +.status-container { + height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.status { + margin: 100px 0 126px; + text-align: center; +} + +.status .status-image { + display: block; + height: 100px; + margin: 16px auto 0; +} + +.status .status-image-thank-you { + height: 66px; +} + +.status .status-message { + margin: 8px 0 24px; +} + +.status .button { + max-width: 236px; +} + +@media (min-width: 768px) { + .status .button { + max-width: 200px; + } +} + +.title-status { + margin-top: 10%; + text-align: center; +} + +/* end Status page */ diff --git a/src/main/resources/static/img/.DS_Store b/src/main/resources/static/img/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 + + diff --git a/src/main/resources/static/img/failure.svg b/src/main/resources/static/img/failure.svg new file mode 100644 index 0000000..4db9773 --- /dev/null +++ b/src/main/resources/static/img/failure.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/static/img/favicon.ico b/src/main/resources/static/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0dc0000a834e874d6d355893049be93c1b21a722 GIT binary patch literal 1150 zcmc&zK?;B%5S*Y>U8F + + + + + + + + diff --git a/src/main/resources/static/img/mystore-logo.svg b/src/main/resources/static/img/mystore-logo.svg new file mode 100644 index 0000000..0418b24 --- /dev/null +++ b/src/main/resources/static/img/mystore-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/static/img/success.svg b/src/main/resources/static/img/success.svg new file mode 100644 index 0000000..71c74f4 --- /dev/null +++ b/src/main/resources/static/img/success.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/static/img/sunglasses.svg b/src/main/resources/static/img/sunglasses.svg new file mode 100644 index 0000000..ce7184a --- /dev/null +++ b/src/main/resources/static/img/sunglasses.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/static/img/thank-you.svg b/src/main/resources/static/img/thank-you.svg new file mode 100644 index 0000000..587d3b5 --- /dev/null +++ b/src/main/resources/static/img/thank-you.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/resources/static/js/adyen_implementations.js b/src/main/resources/static/js/adyen_implementations.js new file mode 100644 index 0000000..7c94013 --- /dev/null +++ b/src/main/resources/static/js/adyen_implementations.js @@ -0,0 +1,174 @@ +// Structure credit card payment request +const structureRequest = (data) => { + + const paymentRequest = { + paymentMethod: data.paymentMethod + }; + + if (data.paymentMethod.type === "scheme") { + paymentRequest['billingAddress'] = data.billingAddress; + paymentRequest['browserInfo'] = data.browserInfo; + } + + console.log(paymentRequest); + return paymentRequest; +}; + +// Parse payment response and directing shopper to correct place +const handleFinalState = (resultCode) => { + if (resultCode === 'Authorised') { + window.location.href = "http://localhost:8080/success"; + + } else if (resultCode === 'Pending') { + window.location.href = "http://localhost:8080/pending"; + + } else if (resultCode === 'Error') { + window.location.href = "http://localhost:8080/error"; + + } else { + window.location.href = "http://localhost:8080/failed"; + } +}; + +/* + * Dropin and Component event handlers + * + * And Create Adyen checkout method + */ + +const onSubmit = (state, component) => { + fetch(`/api/initiatePayment`, { + method: 'POST', + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(structureRequest(state.data)) + }).then(response => response.json()) + .then(response => { + if (response.action) { + if (response.resultCode === 'RedirectShopper') { + localStorage.setItem('redirectPaymentData', response.action.paymentData); + } + adyenComponent.handleAction(response.action); + + } else { + handleFinalState(response.resultCode); + } + }) + .catch(error => { + console.log(error); + window.location.href = "http://localhost:8080/failed"; + }); +}; + +const onAdditionalDetails = (state, component) => { + console.log("On additionalDetails triggered"); + fetch(`/api/submitAdditionalDetails`, { + method: 'POST', + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(state.data) + }).then(response => response.json()) + .then(response => { + if (response.action) { + adyenComponent.handleAction(response.action); + } else { + handleFinalState(response.resultCode); + } + }) + .catch(error => { + throw Error(error); + }); +}; + +const onError = (error) => { + console.log(error); +}; + + +// Create Adyen checkout instance and initilize component +const createAdyenCheckout = () => { + + const paymentMethods = JSON.parse(document.getElementById('payment-methods').innerHTML); + const originKey = document.getElementById('origin-key').innerHTML; + + // Placeholder values + const translations = { + // "en-US": { + // "creditCard.numberField.title": "Custom Card Name", + // } + }; + + const paymentMethodsConfiguration = { + // applepay: { // Example required configuration for Apple Pay + // configuration: { + // merchantName: 'Adyen Test merchant', // Name to be displayed on the form + // merchantIdentifier: 'adyen.test.merchant' // Your Apple merchant identifier as described in https://developer.apple.com/documentation/apple_pay_on_the_web/applepayrequest/2951611-merchantidentifier + // }, + // onValidateMerchant: (resolve, reject, validationURL) => { + // // Call the validation endpoint with validationURL. + // // Call resolve(MERCHANTSESSION) or reject() to complete merchant validation. + // } + // }, + // paywithgoogle: { // Example required configuration for Google Pay + // environment: "TEST", // Change this to PRODUCTION when you're ready to accept live Google Pay payments + // configuration: { + // gatewayMerchantId: "TylerDouglas", // Your Adyen merchant or company account name. Remove this field in TEST. + // merchantIdentifier: "12345678910111213141" // Required for PRODUCTION. Remove this field in TEST. Your Google Merchant ID as described in https://developers.google.com/pay/api/web/guides/test-and-deploy/deploy-production-environment#obtain-your-merchantID + // } + // }, + card: { // Example optional configuration for Cards + // hideCVC: false, // Change this to true to hide the CVC field for stored cards. false is default + // placeholders: { # Change placeholder text for the following fields + // encryptedCardNumber: "", + // encryptedSecurityCode: "" + // }, + // billingAddressRequired: true, +// hasHolderName: true, +// holderNameRequired: true, + enableStoreDetails: true, + name: 'Credit or debit card' + }, + ach: { // Default ACH user information + holderName: 'Ach User', + data: { + billingAddress: { + street: 'Infinite Loop', + postalCode: '95014', + city: 'Cupertino', + houseNumberOrName: '1', + country: 'US', + stateOrProvince: 'CA' + } + } + } + }; + + const configObj = { + paymentMethodsConfiguration: paymentMethodsConfiguration, + showPayButton: true, + locale: "en_US", + environment: "test", + originKey: originKey, + paymentMethodsResponse: paymentMethods, + translations: translations, + onSubmit: onSubmit, + onAdditionalDetails: onAdditionalDetails, + onError: onError + }; + return new AdyenCheckout(configObj); +}; +const integrationType = document.getElementById('integration-type').innerHTML; + +// Adjust style for Dropin +if (integrationType === 'dropin') { + document.getElementById('component').style.padding = '0em'; + document.getElementsByClassName('checkout-component')[0].style.border = 'none'; +} + +const checkout = createAdyenCheckout(); +const adyenComponent = checkout.create(integrationType).mount("#component"); + diff --git a/src/main/resources/templates/cart.html b/src/main/resources/templates/cart.html new file mode 100644 index 0000000..37374d1 --- /dev/null +++ b/src/main/resources/templates/cart.html @@ -0,0 +1,31 @@ +{% extends "layout.html" %} + +{% block content %} + +
+
+

Cart

+
+
    +
  • + +

    Sunglasses

    +

    €5.00

    +
  • +
  • + +

    Headphones

    +

    €5.00

    +
  • +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/checkout-failed.html b/src/main/resources/templates/checkout-failed.html new file mode 100644 index 0000000..2ca2a76 --- /dev/null +++ b/src/main/resources/templates/checkout-failed.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} + +{% block content %} + Confirmation + +
+
+ +

Your order has failed. Please try again.

+ Try Again +
+
+{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/checkout-success.html b/src/main/resources/templates/checkout-success.html new file mode 100644 index 0000000..81db8ad --- /dev/null +++ b/src/main/resources/templates/checkout-success.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} + +{% block content %} + Confirmation + +
+
+ + +

Your order has been successfully placed.

+ Return Home +
+
+{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/component.html b/src/main/resources/templates/component.html new file mode 100644 index 0000000..0665395 --- /dev/null +++ b/src/main/resources/templates/component.html @@ -0,0 +1,28 @@ +{% extends "layout.html" %} +{% block content %} + + + + + +
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html new file mode 100644 index 0000000..b84adf2 --- /dev/null +++ b/src/main/resources/templates/error.html @@ -0,0 +1,18 @@ +{% extends "layout.html" %} + +{% block title %}Page Not Found{% endblock %} + +{% block content %} +

+

+ Error + +

+
+

Error!

+

Review response in console and refer to Response handling.

+ Return Home +
+
+{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/fetch-payment-data.html b/src/main/resources/templates/fetch-payment-data.html new file mode 100644 index 0000000..bfbde5a --- /dev/null +++ b/src/main/resources/templates/fetch-payment-data.html @@ -0,0 +1,37 @@ +{% extends "layout.html" %} + +{% block content %} + +

Working on it!

+ + +{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 0000000..2158cf3 --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,95 @@ +{% extends "layout.html" %} +{% block content %} +
+
+

Select a demo

+

Click to view an interactive example of a PCI-compliant UI integration for online payments.

+

+ Make sure the payment method you want to use are enabled for your account. + Refer the documentation to add missing + payment methods. +

+

To learn more about client-side integration solutions, check out Online payments.

+
+ +
+{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html new file mode 100644 index 0000000..081c7cc --- /dev/null +++ b/src/main/resources/templates/layout.html @@ -0,0 +1,25 @@ + + + + + + + + + + + Checkout Demo + + + + +
+ {% block content %} {% endblock %} +
+ +