Monday, May 19, 2025

GCP PubSub Receive Example

This is part two of the GCP (Google Cloud Platform) PubSub example. Part one is GCP PubSub Send Example. In part one, we learned how to set up GCP admin stuff (e.g. billing), GCP PubSub (e.g. topic), Google Cloud SDK Shell, and a lot more. So if you want to know how to make this example work, please read part one first. I am going straight into the GCP PubSub receive code and will not be discussing about how things are set up. That's covered in part one, please do have a read.

Tools and Materials

These are the tools I used to make this example:

  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • openjdk-17.0.10
  • Spring Boot v3.4.5
  • Windows 11
  • Google account
  • Google Cloud SDK Shell
  • Git Bash

The example code is here, github.com/jpllosa/gcp-pubsub. You may grab a copy of it.

Spring Boot GCP PubSub Receive Code

As with part one, GCP PubSub Send Example. There are no changes to our pom.xml. It will have the same dependencies.

  • Spring Web (<artifactId>spring-boot-starter-web</artifactId>) - Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
  • Spring Integration (<artifactId>spring-boot-starter-integration</artifactId>, <artifactId>spring-integration-http</artifactId>) - Adds support for Enterprise Integration Patterns. Enables lightweight messaging and supports integration with external systems via declarative adapters.
  • Google Cloud Messaging (<artifactId>spring-cloud-gcp-starter-pubsub</artifactId>) - Adds the Google Cloud Support entry and all the required dependencies so that the Google Cloud Pub/Sub integration work out of the box.

Our GCP PubSub message receive code is short and sweet. Just like the send code. We subscribe to a GCP PubSub topic and when a message is receive, we print it out to the console. Here's the code. Explanation follows after.

  
package com.blogspot.jpllosa;

// imports snipped...

@SpringBootApplication
@RestController
public class GcpPubsubApplication {

// some send code snipped...

	@PostMapping("/postMessage")
	public RedirectView postMessage(@RequestParam("message") String message) {
		this.messagingGateway.sendToGcpPubsub(message);
		return new RedirectView("/");
	}

	// Receive
	@Bean
	public MessageChannel gcpPubsubInputChannel() {
		return new DirectChannel();
	}

	@Bean
	public PubSubInboundChannelAdapter messageChannelAdapter(
			@Qualifier("gcpPubsubInputChannel") MessageChannel inputChannel,
			PubSubTemplate pubSubTemplate) {
		PubSubInboundChannelAdapter adapter =
				new PubSubInboundChannelAdapter(pubSubTemplate, "gcp-pubsub-example-sub");
		adapter.setOutputChannel(inputChannel);

		return adapter;
	}

	@ServiceActivator(inputChannel = "gcpPubsubInputChannel")
	public void messageReceiver(String message) {
		System.out.println("Message received: " + message);
	}

}
  

The same with part one, only the GCP PubSub receive bits will be explained. Incoming messages will arrive on the MessageChannel (DirectChannel) we created. Simliar to sending, we have an inbound channel adapter that receives messages from GCP PubSub and forwards it to gcpPubsubInputChannel. The inbound channel adapter is linked to the GCP PubSub subscription name. The adapter is bound and listens for new messages from GCP PubSub gcp-pubsub-example-sub subscription. The method annotated with @ServiceActivator is called when new messages arrive at gcpPubsubInputChannel and we simply print the message on the console. Short and sweet.

Spring Boot GCP PubSub Receive in Action

Let's see our code in action. Run the Spring Boot app (e.g. mvnw spring-boot:run) or thru IntelliJ. When the app is ready, hit the /postMessage endpoint and you should have something like below printed on the console.

  
$ curl --data "message=GCP Pubsub send and receive!" localhost:8080/postMessage
  

We won't be able to see any messages in the GCP PubSub messages section because this happens very quick. If you want to see the messages sent on the messages tab, click PULL to delay the message being consumed. The delay is about 5-ish seconds. It would look like the below.

Spring Boot GCP PubSub Receive Wrap Up

There you have it. Your travel to GCP PubSub has come full circle, assuming you read part one of course. As per usual, we give thanks to Spring for making it easy for us by abstracting whatever it is happening under the hood. Alternatively, you can use other client libaries to connect to GCP PubSub. We created an input channel and attached it to a channel adapter that has subscribed to GCP PubSub. Viola and that is all there is to it! Thank you for reading.

Monday, May 12, 2025

GCP PubSub Send Example

If you are working on Shopify and you need to monitor webhook events (e.g. an order has been created), chances are you'll be directed to use Google Pub/Sub messaging service. In this blog, I'll show you how to setup PubSub on the Google Cloud Platform (GCP), then send a message and check the message on GCP PubSub cloud console.

Tools and Materials

These are the tools I used to make this example:

  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • openjdk-17.0.10
  • Spring Boot v3.4.5
  • Windows 11
  • Google account
  • Google Cloud SDK Shell
  • Git Bash

The example code is here, github.com/jpllosa/gcp-pubsub. You may grab a copy of it.

GCP PubSub Setup

On GCP, create a new project. Like so.

Assign your new project with billing. As of this writing, Google provides free credits to new customers and free usage limits. For Google PubSub, it is 10 GB of messages per month. Please verify for yourselves if this is still the case. Here's what creating a billing account looks like and it being connected to the project ID.

Now that the administrative stuff has all been set up, it's time to create a PubSub topic and subscription.

Google Cloud SDK Shell Setup

Install and initialize the Google Cloud SDK. For installing the Google Cloud SDK Shell, it is better to get it from the horses' mouth. Please follow the Google Cloud documentation on how to install and initialiaze it for your project. Once initialized, enable GCP PubSub. Here are a few checkpoints so you'll know you are heading in the right direction. First and foremost, you must log in with application default credentials using the following command, gcloud auth application-default login. When you are logged, you should have something like below.

Next is to set the shell to your project, gcloud config set project <PROJECT-NAME>. Here's the command to see the available projects, gcloud config projects list. When you have done those, you should have something like below.

Lastly, enable GCP PubSub, gcloud services enable pubsub.googleapis.com. Output would look like below.

For Windows, we can verify that our Google credentials are saved by looking under C:\Users\username\AppData\Roaming\gcloud\application_default_credentials.json. This file should exist.

Spring Boot GCP PubSub Send Code

I used Spring Initialzr to create the skeleton project. The following are the dependencies we need:

  • Spring Web - Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
  • Spring Integration - Adds support for Enterprise Integration Patterns. Enables lightweight messaging and supports integration with external systems via declarative adapters.
  • Google Cloud Messaging - Adds the Google Cloud Support entry and all the required dependencies so that the Google Cloud Pub/Sub integration work out of the box.

Our pom.xml should at least have the following dependencies.

  
<!-- snipped -->

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>com.google.cloud</groupId>
	<artifactId>spring-cloud-gcp-starter-pubsub</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.integration</groupId>
	<artifactId>spring-integration-http</artifactId>
</dependency>

<!-- snipped -->
  

Our GCP PubSub message sender code is short and sweet. When an HTTP POST is received, it then submits the message to GCP PubSub. Here's the code. Explanation follows after.

  
package com.blogspot.jpllosa;

// imports snipped...

@SpringBootApplication
@RestController
public class GcpPubsubApplication {

	public static void main(String[] args) {
		SpringApplication.run(GcpPubsubApplication.class, args);
	}

	@MessagingGateway(defaultRequestChannel = "gcpPubsubOutputChannel")
	public interface GcpPubsubOutboundGateway {
		void sendToGcpPubsub(String text);
	}

	@Bean
	@ServiceActivator(inputChannel = "gcpPubsubOutputChannel")
	public MessageHandler messageSender(PubSubTemplate pubsubTemplate) {
		return new PubSubMessageHandler(pubsubTemplate, "gcp-pubsub-example");
	}

	@Autowired
	private GcpPubsubOutboundGateway messagingGateway;

	@PostMapping("/postMessage")
	public RedirectView postMessage(@RequestParam("message") String message) {
		this.messagingGateway.sendToGcpPubsub(message);
		return new RedirectView("/");
	}

}
  

Since this is a blog about GCP PubSub, I'll focus on explaining the code related to it. I won't be talking about the other annotations (e.g. @SpringBootApplication, @PostMapping, etc.). For example, @PostMapping is related to web and nothing to do with GCP PubSub. We just need that endpoint to trigger a send message to GCP PubSub. We can use other client libraries to communicate with GCP PubSub, not just Spring.

We use Spring Integration messaging gateway to write to a channel, gcpPubsubOutputChannel. This channel is still in Spring land. When a message is in this channel, it is then consumed by the MessageHandler (outbound channel adapter) which in turn publishes the message to GCP PubSub. The outbound channel adapter converts the Spring message into a GCP PubSub message and is published to the topic, gcp-pubsub-example. The @ServiceActivator annotation causes this MessageHandler to be applied to any new messages in inputChannel. Lastly, we got a REST endpoint listening for messages which then forward the message to GCP PubSub. Short and sweet.

Spring Boot GCP PubSub Send in Action

Alrighty, run the Spring Boot app. We can do it command line (e.g. mvn spring-boot:run) or thru IntelliJ. I would suggest running it first on Google Cloud SDK Shell. If it works there then I don't see any reason why it won't work on IntelliJ. When the app is ready, hit the /postMessage endpoint.

  
$ curl --data "message=Eagle has landed!" localhost:8080/postMessage
  

Now, head over to GCP PubSub messages section and we should see the messages appear. If not, try reloading the web page or do a PULL. It should look like below.

Spring Boot GCP PubSub Send Wrap Up

Did you enjoy playing around the GCP PubSub service? We now have an idea on how to send messages via the GCP PubSub messaging service. Spring has made it easy for us by abstracting whatever it is happening under the hood. Thank you for reading.

Friday, April 25, 2025

Managing HttpOnly Cookie Example

In my previous blog, Spring Boot HttpOnly Cookie Example, I demonstrated how to use the HttpOnly flag to protect your cookie. Since an HttpOnly cookie cannot be managed on the front-end, here is a demonstration on how to manage it via the back-end. For this example we'll just expire the cookie when the user clicks a button. Shouldn't be tricky so let's get right to it.

Demonstration

Before we begin, these are the tools I used to make this example:

  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • openjdk-17.0.10
  • Spring Boot v3.4.1
  • Windows 11

The example code is here, github.com/jpllosa/httponly-cookie/tree/manage-httponly. Grab a copy of this branch or if you have done the Spring Boot HttpOnly Cookie Example, you can make updates to the code as you wish.

Code Changes

A couple of changes to the code. First, the endpoint to hit that then tells the browser to expire the cookie. Second, an update on the UI to trigger the cookie deletion.

LoginController.java

  
... code snipped ...
import org.springframework.web.bind.annotation.*;

@Controller
public class LoginController {

... code snipped ...

    @GetMapping("/clear-my-session")
    public @ResponseBody String clearMySession(HttpServletResponse response) {

        Cookie cookie = new Cookie("MY_SESSION", "deleted");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(0);
        response.addCookie(cookie);

        return "";
    }
}

  

The /clear-my-session resource simply responds by setting the MaxAge of the cookie to zero for it to be deleted on the browser. In some other languages, in Go for example, you'll have to set the MaxAge to -1 to expire the cookie. So please read the API documentation if you're wondering why MaxAge zero doesn't work. Since we are just running on our local machhine, we don't provide the Path and Domain. When not running on your local machine, most likely you'll need to supply a Path (e.g. "/") and a Domain (e.g. example.com) to make things work.

welcome.html

  
... code snipped ...
<body>
... code snipped ...
<button id="clearMySession">JS Clear MY_SESSION</button>
<button id="beClearMySession">BE Clear MY_SESSION</button>
</body>

<script th:inline="javascript">
    $(document).ready(function() {
        $("#clearMySession").on("click", function() {
            console.log("JS removing MY_SESSION");
            document.cookie = "MY_SESSION=; expires=Thu, 01-Jan-70 00:00:01 GMT;";
        });

        $("#beClearMySession").on("click", function() {
            console.log("BE removing MY_SESSION");

            $.get("/clear-my-session", function() {
                console.log("MY_SESSION removed");
            });
        });
    });
</script>
</html>

  

Here, we added a button that hits the endpoint we created above. Simples.

Ready, Set, Go

Run the Spring Boot app. Go to the login page. Just type any username and password. You should have something like below. Open Web Tools and head over to the Storage tab (it could be called a different name on a different browser). Take note of the cookie named MY_SESSION.

As you already know from the previous blog, clicking on "JS Clear MY_SESSION" (JS for JavaScript) will not do anything. Now, try clicking on "BE Clear MY_SESSION", BE for back-end :). The MY_SESSION cookie disappears right before our eyes and you should have something like below. You can also check the network and console tabs to see what's happening behind the scenes.

Managing HttpOnly Cookie Wrap Up

There you have it. A nice way of managing your cookie from the back-end. Having the HttpOnly flag set prevents thrid parties from accessing your very important cookie. Now, it's only the back-end that can manipulate it. Thank you for reading.

Sunday, January 12, 2025

Spring Boot HttpOnly Cookie Example

Want to protect your cookie? Well, not from Santa Claus. I'm talking about HTTP Cookie. Using the HttpOnly flag when generating a cookie helps mitigate the risk of a client side script accessing the protected cookie. In other words, it won't be accessible programmatically on the client side (e.g. JavaScript). The cookie will be driven by the backend.

Why do this? To mitigate cross-site scripting (XSS) attacks. If a cross-site scripting flaw exists, and a user accidentally accesses a link that exploits this flaw, the browser will not reveal the cookie to a third party. Currently, every major browser supports HttpOnly cookies. If a browser does not support HttpOnly and a website attempts to set an HttpOnly cookie, the HttpOnly flag will be ignored by the browser, thus creating a traditional, script accessible cookie. As a result, the cookie (typically your session cookie) becomes vulnerable to theft or modification by a malicious script. Majority of XSS attacks target theft of session cookies. A server could help mitigate this issue by setting the HttpOnly flag on a cookie it creates, indicating the cookie should not be accessible on the client. If a browser that supports HttpOnly detects a cookie containing the HttpOnly flag, and client side script code attempts to read the cookie, the browser returns an empty string as the result. This causes the attack to fail by preventing the malicious (usually XSS) code from sending the data to an attacker’s website.

Demonstration

Before we begin, these are the tools I used to make this example:

  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • openjdk-17.0.10
  • Spring Boot v3.4.1
  • Windows 11

The example code is here, github.com/jpllosa/httponly-cookie. Download it as you please. Now, on with the show. Run the Spring Boot app. Go to the login page, you should see something like below. Please forgive the look, this demo is about HttpOnly and not about the UI.

Without HttpOnly

Log in. Just type any user name and password. You should have something like below. Open Web Tools and head over to the Storage tab (it could be called a different name on a different browser). Take note of the HttpOnly column of the cookie named MY_SESSION.

This is the code of the Controller serving the page. Take note that we have commented out the line that sets the HttpOnly flag.

  
package com.blogspot.jpllosa.httponly_cookie;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.CookieValue;

@Controller
public class LoginController {

    @GetMapping("/login")
    public String getLogin() {
        return "login";
    }

    @PostMapping("/login")
    public String postLogin(
            @RequestParam(name="username", required=true) String username,
            @RequestParam(name="password", required=true) String password,
            HttpServletResponse response,
            HttpSession session) {

        session.setAttribute("username", username);
        session.setAttribute("password", password);

        Cookie cookie = new Cookie("MY_SESSION", "supercalifragilisticexpialidocious");
//        cookie.setHttpOnly(true);
        response.addCookie(cookie);

        return "redirect:/welcome";
    }

    @GetMapping("/welcome")
    public String welcomeUser(
            @CookieValue(value = "MY_SESSION") String mySession,
            Model model,
            HttpSession session) {

        model.addAttribute("username", session.getAttribute("username"));
        model.addAttribute("password", session.getAttribute("password"));
        model.addAttribute("mySession", mySession);

        return "welcome";
    }

}
  

Now, click "Clear MY_SESSION" button. Notice that the cookie is gone as below.

With HttpOnly

Alright, this time, we'll put in the HttpOnly flag. Uncomment cookie.setHttpOnly(true); and restart the Spring Boot app. Log in again and as usual, have the Storage tab open. What happens when you click the "Clear MY_SESSION" button now? It's still there no matter how many times we click the button as shown on the console logs.

Spring Boot HttpOnly Cookie Conclusion

There you have it. A nice way of protecting your cookie. Having the HttpOnly flag set prevents thrid parties from accessing you very important cookie. It's now the task of the backend to manage the cookie. Thank you for reading.

Sunday, December 1, 2024

Custom Font Shopify Checkout Example

Now that you got your Shopify website up and running. Why not add some branding on your checkout page? In this example, we'll add a custom font to our checkout page. This blog assumes you have read my past blogs about Shopify Checkout Extensibility and Shopify in general. I will not write about how to set up a development store and a checkout app. Please read my past blogs on how to do it. I will just go straight into how to configure the custom font for your checkout app.

Default Font

Below is the default font for the checkout page. This is what it looks like before branding our checkout page.

Upload the WOFF File

Upload the custom font (e.g. Playmaker_D.woff) to Shopify Admin > Content > Files. Custom fonts must be in either Web Open Font Format (WOFF) or WOFF2. Once uploaded, we'll need the gid of the WOFF file. There are two ways of grabbing the gid. One is by inspecting it in web tools and it's usually the ID of the TR element. Second is by sending a GraphQL query. We use GraphiQL to run our queries. Make sure the GraphiQL app has the read_checkout_branding_settings and write_checkout_branding_settings access scopes enabled. As you will see below, they both show the same gid.

Here's the GraphQL query for it.

  
query queryFiles {
  files(first: 10, query: "media_type:GenericFile") {
    edges {
      node {
        ... on GenericFile {
          id
          url
          fileStatus
        }
      }
    }
  }
}
  

Checkout Profile ID

Next, we'll need the checkout profile ID. This is the target for the custom font changes. Go to Checkout > Customize. The profile ID should appear on the URL address bar after /profiles as can be seen below.

Apply Custom Font

Apply the custom font to primary and secondary surfaces. Primary surfaces include text and buttons. Secondary surfaces include headings. You should see the changes right away. And that is all there is to it. Super simple, isn't it?

GraphQL query:

  
mutation checkoutBrandingUpsert($checkoutBrandingInput: CheckoutBrandingInput!, $checkoutProfileId: ID!) {
  checkoutBrandingUpsert(checkoutBrandingInput: $checkoutBrandingInput, checkoutProfileId: $checkoutProfileId) {
    checkoutBranding {
      designSystem {
        typography {
          primary {
            base {
              sources
              weight
            }
            bold {
              sources
              weight
            }
            name
          }
          secondary {
            base {
              sources
              weight
            }
            bold {
              sources
              weight
            }
            name
          }
        }
      }
    }
    userErrors {
      code
      field
      message
    }
  }
}
  

GraphQL variables:

  
{
  "checkoutProfileId": "gid://shopify/CheckoutProfile/",
  "checkoutBrandingInput": {
    "designSystem": {
      "typography": {
        "primary": {
          "customFontGroup": {
            "base": {
              "genericFileId": "gid://shopify/GenericFile/",
              "weight": 100
            },
            "bold": {
              "genericFileId": "gid://shopify/GenericFile/",
              "weight": 500
            }
          }
        },
        "secondary": {
          "customFontGroup": {
            "base": {
              "genericFileId": "gid://shopify/GenericFile/",
              "weight": 100
            },
            "bold": {
              "genericFileId": "gid://shopify/GenericFile/",
              "weight": 500
            }
          }
        }
      }
    }
  }
}
  

As can be seen below, the custom font has been applied.

More Information

For more information, jump to the ultimate source of this process, Shopify Customize typography. Happy Shopify Checkout Extensibilty branding.

Sunday, October 13, 2024

Securing a Secure Web Application Example

How do you secure an already secure web application? Well, the title is that because we are going to build upon Spring's Securing a Web Application guide. You can follow the Spring guide to build your secure web app and then come back here to make it more secure. Or you can just keep on reading, I'm going to share the repo anyway.

Tools

Before we begin, these are the tools I used to make this example:

  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • openjdk-17.0.10
  • Spring Boot v3.3.3
  • Windows 11
  • Ubuntu 22.04.3 LTS (GNU/Linux 5.15.153.1-microsoft-standard-WSL2)
  • OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)

Prerequisite

Alright. I'm assuming you followed the Spring guide to securing a web app and got it running. What you did was add Spring Security on your Spring Boot app which then protects your resource endpoints. You have secured it with a login page and only authorized users can access certain resources. You should have something like below.

I have added 127.0.0.1 jpllosa.tech on the Windows hosts (C:\Windows\System32\drivers\etc) file. We can't use localhost for our certificate later.

Requirement

And now the problem we are going to solve. Imagine this secure web app is runnning on our company network. As it stands anyone can see the website on the browser (e.g. by going to http://jpllosa.tech:8080). They would see the welcome and login pages. Now, we don't want just anybody to see the website. We only want, let's say level 1 clearance personnel and machine to see it. What do we do? Any ideas? Yes, we are going to serve the web app over HTTPS (Hypertext Transfer Protocol Secure) and then what's next? Yes, install our own certificate authority as a trusted certificate on the browser of the level 1 personnel's laptop (e.g. an IT services job). Is the requirement clear enough?

Create Self-signed Certificate

We're going to do PEM (Privacy Enhanced Mail) certificates. Thankfully, Spring Boot supports this. We don't have to deal with the Java specific JKS (Java KeyStore) format, which can be tricky to configure sometimes.

Let's start by creating a private key. This is the most important component of our certificate and helps to enable encryption. So open your WSL and generate a private key like so.

  
$ openssl genrsa -out jpllosa.tech.key 2048
$ ls
jpllosa.tech.key
  

Second, create a certificate signing request. A certificate signing request (CSR) includes the public key and some additional information like organization and country. We need this because we want our certificate signed. Create the CSR with our private key like below. I've left the challenge password and optional company name blank. The important field is Common Name, which should be the fully qualified domain name (FQDN) of our domain.

  
$ openssl req -key jpllosa.tech.key -new -out jpllosa.tech.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:AU
State or Province Name (full name) [Some-State]:Some-State
Locality Name (eg, city) []:City
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Internet Widgets
Organizational Unit Name (eg, section) []:section
Common Name (e.g. server FQDN or YOUR name) []:jpllosa.tech
Email Address []:webmaster@jpllosa.tech

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
$ ls
jpllosa.tech.csr  jpllosa.tech.key
  

Next, we create a self-signed certificate with our private key and CSR. This is a certificate signed with its own private key. This certificate isn't trusted since it is self-signed. Create a self-signed certificate like so.

  
$ openssl x509 -signkey jpllosa.tech.key -in jpllosa.tech.csr -req -days 365 -out jpllosa.tech.crt
Certificate request self-signature ok
subject=C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = section, CN = jpllosa.tech, emailAddress = webmaster@jpllosa.tech
$ ls
jpllosa.tech.crt  jpllosa.tech.csr  jpllosa.tech.key
  

Now we need something for the browser (i.e. client side) as we will need to install a trusted certificate for it. We need a self-signed root certificate authority (CA) certificate. We can do this by being our own certificate authority. Let's create a self-signed root CA like so.

  
$ openssl req -x509 -sha256 -days 1825 -newkey rsa:2048 -keyout myCA.key -out myCA.crt
.....+...+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.+....+........+...+...+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*....+..+.............+.................+..........+..+............+.+........+.+..............+....+..+......+.......+..+.............+..+.........+............+....+..............+......+...+...............+...+....+...........+..........+..............+....+...............+..+....+..............+......+.+...+.........+.....+...+.........+....+...........+...+.+.........+..+......+...+....+.....+.+...........+...................+..+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.................+..........+.....+.......+...+...+..+..........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*..+..+.............+..+.+...+..+.......+..+...+.+...........+....+.....+.........+.+.....+.......+..+.......+......+..+.+.....+....+.....+...+.......+..+....+.....+.......+.....+....+........+......+....+.....+...+.......+......+.....+.......+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.+...+...+......+...........+.......+...+..+...+.......+..+....+..+.......+........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Enter PEM pass phrase: password
Verifying - Enter PEM pass phrase: password
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:AU
State or Province Name (full name) [Some-State]:Some-State
Locality Name (eg, city) []:City
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Internet Widgets
Organizational Unit Name (eg, section) []:Section
Common Name (e.g. server FQDN or YOUR name) []:my.com
Email Address []:webmaster@my.com
$ ls
jpllosa.tech.crt  jpllosa.tech.csr  jpllosa.tech.key  myCA.crt  myCA.key
  

Next, we sign our CSR with our root CA. The result will be, the CA-signed certificate will be in the jpllosa.tech.crt file. This is a working certificate. Create like so.

  
$ openssl x509 -req -CA myCA.crt -CAkey myCA.key -in jpllosa.tech.csr -out jpllosa.tech.crt -days 365 -CAcreateserial
Certificate request self-signature ok
subject=C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = section, CN = jpllosa.tech, emailAddress = webmaster@jpllosa.tech
Enter pass phrase for myCA.key: password
$ ls
jpllosa.tech.crt  jpllosa.tech.csr  jpllosa.tech.key  myCA.crt  myCA.key
  

Even though we have created a working certificate, it will still be flagged by the browser. What we need is to add the subjectAltName. We need a SAN extension. X.509 certificates need information about the domain for which this particular certificate is issued for. To align with SAN extension stadards, we need to create a configuration text file then add the configuration to the certificate. Like so. How's your vi skills?

  
$ vi jpllosa.tech.ext
$ cat jpllosa.tech.ext
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:false
subjectAltName = @alt_names
[alt_names]
DNS.1 = jpllosa.tech
$ openssl x509 -req -CA myCA.crt -CAkey myCA.key -in jpllosa.tech.csr -out jpllosa.tech.crt -days 365 -CAcreateserial -extfile jpllosa.tech.ext
Certificate request self-signature ok
subject=C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = section, CN = jpllosa.tech, emailAddress = webmaster@jpllosa.tech
Enter pass phrase for myCA.key: password
$ ls
jpllosa.tech.crt  jpllosa.tech.csr  jpllosa.tech.ext  jpllosa.tech.key  myCA.crt  myCA.key
  

Finally, we got a working certificate that meets all the SAN requirements. SAN makes the certificates more secure and it allows the definition of several domains or IP addresses and we can use a single certificate across multiple domains. View the certificate like so. Spot the important bit, Subject Alternative Name.

  
$ openssl x509 -text -noout -in jpllosa.tech.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            78:db:cd:5a:18:8d:46:0f:74:55:16:86:dc:f2:74:95:1b:c4:58:0e
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = Section, CN = my.com, emailAddress = webmaster@my.com
        Validity
            Not Before: Oct  5 16:41:02 2024 GMT
            Not After : Oct  5 16:41:02 2025 GMT
        Subject: C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = section, CN = jpllosa.tech, emailAddress = webmaster@jpllosa.tech
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:ac:70:3c:57:da:fc:f4:b4:f6:4f:f4:3d:64:9f:
                    27:ca:09:d9:b6:4e:4d:89:2f:db:4b:6e:7c:18:4b:
                    af:5c:b4:80:cc:42:0a:cd:1e:15:1e:2d:be:71:e4:
                    6c:59:40:82:c5:ac:29:3b:fa:51:0b:6b:20:11:e5:
                    7d:1e:92:f7:e9:9d:f4:15:31:47:64:ec:1b:a5:14:
                    00:6e:c0:98:76:be:71:9d:c6:97:14:47:aa:30:b1:
                    ef:c4:b4:6b:b0:31:22:25:65:21:ab:35:21:ac:7b:
                    6a:f7:c0:78:d2:90:7d:33:d0:3c:dc:db:21:8e:75:
                    ff:04:83:bd:e6:9a:5d:79:70:a2:59:21:ff:51:20:
                    ea:74:d1:78:89:61:49:f6:6c:87:85:e2:0f:0c:f7:
                    b4:be:2b:79:88:28:fc:f7:50:ef:c1:e6:63:3e:a4:
                    0e:3c:71:18:97:55:5c:76:18:80:67:af:84:0a:16:
                    98:79:aa:00:00:77:a4:1b:97:bd:9c:41:50:13:89:
                    0c:63:29:51:84:7f:95:67:b7:f0:94:2b:b4:bb:50:
                    5e:6f:66:d1:06:4a:97:d6:3a:ac:6e:90:59:22:2c:
                    d3:09:a1:4b:e7:a1:3c:96:f6:b5:9c:25:5f:5b:cb:
                    be:a5:41:11:da:dc:a5:1b:cd:86:4d:a1:bd:44:1c:
                    44:c5
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Authority Key Identifier:
                17:B0:3C:87:D6:06:3C:54:21:F9:0B:8D:94:46:EF:F9:FA:A9:5C:8C
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Subject Alternative Name:
                DNS:jpllosa.tech
            X509v3 Subject Key Identifier:
                93:B7:A9:CB:E3:56:A7:38:C1:8A:E6:6E:0A:0B:4B:4C:FE:1A:B7:FA
    Signature Algorithm: sha256With
...snipped...
  

Configure Spring Boot with PEM Certificates for TLS/SSL Communication

And now the easy part. Thanks to Spring Boot. All we have to do is add some properties on the application.properties file to enable SSL communication. Update the properties file like so.

  
server.port=8443
server.ssl.certificate=classpath:jpllosa.tech.crt
server.ssl.certificate-private-key=classpath:jpllosa.tech.key
  

Place jpllosa.tech.crt and jpllosa.tech.key under /src/main/resources so it can be picked up in the classpath. Try running the web app and go to the site. What do you see? Do you see something like below?

Our HTTPS site is still getting a warning. You could advance and accept the risk but what we want is for the browser to trust the site.

Browser Configuration

We need to import the root CA into the browser so our site can be trusted. These steps are specific to Firefox. Go to Settings > Privacy & Security > View Certificates.

Import myCA.crt and trust it to identify websites. You should be able to see Internet Widgets installed.

Let's try accessing the web app again. What does it say now? The warning is gone and you can go to the welcome and login pages.

Securing a Secure Web Application Summary

What a journey. To secure a Spring Boot app with PEM certificates for SSL communication was the easy bit. Just a few lines in the properties file. The arduous bit was the creation of the PEM certificates. Lastly we had to import our root CA to the browser so our site can be trusted. There you have it. We got to secure a secured web app. Was the requirement satisfied? . As usual, entire code is available at github.com/jpllosa/cert-web-app. Thank you for reading.

Monday, August 19, 2024

Shopify Function Extension Example

This blog builds upon my previous blog, Shopify Check UI Extension Example. Now that we got our Checkout Extensibility app running, it's time to add another payment method. Why do we need to add another payment method? Surely, that's commen sense is the answer to that, right? Having more payment options will let your store cater to more potential customers.

One popular payment method is Paypal. To activate Paypal, on the Shopify admin page, we go the Settings > Payments > Activate Paypal and then follow the instructions. The payment setup would look like below.

Fantastic. Now that we have activated Paypal, we should see something like below on checkout.

Paypal Express Checkout Problem

Well and good that we can accept Paypal payments. But the issue now is that some customers are abandoning the checkout step because they don't have Paypal and it is right at the top of the page. It is making them believe that it is the only payment option. It is confusing the customers especially on mobile view because it's the first thing they see as it is positioned at the top. Our simple requirement now is keep the Paypal payment method and at the same time remove or hide the Paypal express checkout button. Sounds easy enough?

Shopify Function Solution

As mentioned earlier, we are building the Shopify Function extension from my previous blog, Shopify Check UI Extension Example. I'm assuming you have the GitHub repository for it. If not, you can pull from github.com/jpllosa/checkout-practice-app. First off the bat is to generate the extension like so. I've named the extension hide-paypal-express and coding it in JavaScript.

  
C:\shopify\checkout-practice-app>npm run generate extension

> checkout-practice-app@1.0.0 generate
> shopify app generate extension


To run this command, log in to Shopify.
👉 Press any key to open the login page on your browser
✔ Logged in.
╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Using shopify.app.toml:                                                       │
│                                                                                │
│    • Org:             Joel Patrick Llosa                                       │
│    • App:             checkout-practice-app                                    │
│                                                                                │
│   You can pass `--reset` to your command to reset your app configuration.      │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

?  Type of extension?
√  Payment customization - Function

?  Name your extension:
√  hide-paypal-express

?  What would you like to work in?
√  JavaScript


╭─ success ──────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Your extension was created in extensions/hide-paypal-express.                 │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

  

Update Scopes

Our scopes on the shopify.app.toml should have been updated with the read_payment_customizations and write_payment_customizations. If not, add them like below and do a deploy to push the scopes to the Partner dashboard.

  
# shopify.app.toml
...snipped...
[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "read_payment_customizations,write_payment_customizations"
...snipped...
  
  
C:\shopify\checkout-practice-app>npm run deploy

> checkout-practice-app@1.0.0 deploy
> shopify app deploy

╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Using shopify.app.toml:                                                       │
│                                                                                │
│    • Org:             Joel Patrick Llosa                                       │
│    • App:             checkout-practice-app                                    │
│    • Include config:  Yes                                                      │
│                                                                                │
│   You can pass `--reset` to your command to reset your app configuration.      │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

?  Release a new version of checkout-practice-app?
√  Yes, release this new version


Releasing a new app version as part of checkout-practice-app

free-shirt-1000     │ Bundling UI extension free-shirt-1000...
hide-paypal-express │ Building function hide-paypal-express...
hide-paypal-express │ Building GraphQL types...
free-shirt-1000     │ free-shirt-1000 successfully built
hide-paypal-express │ Bundling JS function...
hide-paypal-express │ Running javy...
hide-paypal-express │ Done!


╭─ success ──────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  New version released to users.                                                │
│                                                                                │
│  checkout-practice-app-2 [1]                                                   │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯
[1] https://partners.shopify.com/31716511/apps/1338089799691/versions/3168605962251
  

Enable the Shopify Function

Next step is to grab the function ID to enable the Shopify function and create the payment customization. For this I've installed GraphiQL app to make executing queries and mutations against my store easier. You can find the function ID the Partners page like below.

Run the paymentCustomizationCreate mutation via the GraphiQL app to enable your Shopify function. Don't forget to replace the function ID. You should have a response like below after that.

  
mutation {
  paymentCustomizationCreate(paymentCustomization: {
    functionId: "<replace with function ID>"
    title: "checkout practice app"
    enabled: true
  }) {
    paymentCustomization {
      id
    }
    userErrors {
      message
    }
  }
}
  

Response

  
{
  "data": {
    "paymentCustomizationCreate": {
      "paymentCustomization": {
        "id": "gid://shopify/PaymentCustomization/283772501"
      },
      "userErrors": []
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 10,
      "actualQueryCost": 10,
      "throttleStatus": {
        "maximumAvailable": 2000,
        "currentlyAvailable": 1990,
        "restoreRate": 100
      }
    }
  }
}
  

Just provide your shop domain to install the GraphiQL App.

To check what payment customizations have been enabled on your store, run the paymentCustomizations query like below.

  
query {
  paymentCustomizations(first: 100) {
    edges {
      node {
        id
        title
      }
    }
  }
}
  

Response

  
{
  "data": {
    "paymentCustomizations": {
      "edges": [
        {
          "node": {
            "id": "gid://shopify/PaymentCustomization/283772501",
            "title": "checkout practice app"
          }
        }
      ]
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 11,
      "actualQueryCost": 3,
      "throttleStatus": {
        "maximumAvailable": 2000,
        "currentlyAvailable": 1997,
        "restoreRate": 100
      }
    }
  }
}
  

Hide Paypal Express Checkout

Before we hide the Paypal Express Checkout button, let's see how it looks without any logic added to the scaffolding code. Run npm run dev. Go to the checkout page, the top of the page will show the Paypal Express checkout and the bottom will have the standard Paypal payment method. Next, go to your Partners dashboard then your app extensions runs. You should see the logs on what goes in, out and errors. It's empty for now because we haven't added a query and applied any logic to the result of the query.

Under extensions/hide-paypal-express, edit the run.graphql with the query below. This will fetch a list of payment methods available on our checkout.

  
query RunInput {
  paymentMethods {
    id
    name
  }
}
  

On the same directory, edit run.js as below. What this code does is fairly simple. Probably does not need any explanation as it is fairly readable and understandable. We just check for a Paypal Express Checkout payment menthod and if it exists apply the hide operation to it by providing the payment method ID and placement. Express checkout is AcceleratedCheckout. Otherwise, return with no changes to the payment methods.

  
// @ts-check

import { PaymentCustomizationPaymentMethodPlacement } from "../generated/api";

/**
 * @typedef {import("../generated/api").RunInput} RunInput
 * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
 */

/**
 * @type {FunctionRunResult}
 */
const NO_CHANGES = {
  operations: [],
};

/**
 * @param {RunInput} input
 * @returns {FunctionRunResult}
 */
export function run(input) {
  const hidePaymentMethod = input.paymentMethods.find(
    (method) =>
      method.name.toLocaleLowerCase().trim() === "paypal express checkout"
  )

  if (hidePaymentMethod) {
    return {
        operations: [
        {
          hide: {
            paymentMethodId: hidePaymentMethod.id,
            placements: [PaymentCustomizationPaymentMethodPlacement.AcceleratedCheckout]
          }
        }
      ]
    }
  }

  return NO_CHANGES;
};
  

Save your changes and the changes should hot reload. If not, do npm run dev again. Go to the checkout page and the Paypal Express Checkout button should be gone now.

Debugging a Shopify Function

To debug a shopify function, go to your Partners dashboard then your app extensions runs as described above. This time you should see some logs because we have added a query in run.graphql and we have returned an operation. You should have something like below. If you want to dump some values of variables for example, calls to console.error are shown under Logs (STDERR).

Input (STDIN)

  
{
  "paymentMethods": [
    {
      "id": "gid://shopify/PaymentCustomizationPaymentMethod/0",
      "name": "(for testing) Bogus Gateway"
    },
    {
      "id": "gid://shopify/PaymentCustomizationPaymentMethod/1",
      "name": "Deferred"
    },
    {
      "id": "gid://shopify/PaymentCustomizationPaymentMethod/2",
      "name": "PayPal Express Checkout"
    }
  ]
}
  

Output (STDOUT)

  
{
  "operations": [
    {
      "hide": {
        "paymentMethodId": "gid://shopify/PaymentCustomizationPaymentMethod/2",
        "placements": [
          "ACCELERATED_CHECKOUT"
        ]
      }
    }
  ]
}
  

Shopify Function Closing

There you have it. A handy way to hide the Paypal Express Checkout option in Shopify Checkout. As usual, entire code is available at github.com/jpllosa/checkout-practice-app. Thank you for reading.

Sunday, July 7, 2024

Shopify Checkout UI Extension Example

As you might have already read on the Shopify website, checkout.liquid is deprecated and stores need to upgrade to Checkout Extensibility by August 13, 2024. So here is an example of how to build a Checkout UI extension.

For this example, our requirement is going to be to provide a free item (e.g. t-shirt) on checkout when the total purchase is greater than a thousand. Clear enough?

Checkout UI Extension Setup

If you have read my past blogs then you probably already have a development store and partner account. In any case, these are the prerequisites:

  • A Shopify Partner account
  • A development store that use the Checkout and Customer Accounts Extensibiliy
  • Shopify CLI

This example was created on a Windows 11 machine with Node v20.11.0, npm v10.2.4 and Shopify CLI v3.60.1. For the final check, on the bottom left of your Shopify Admin page, it should say Checkout and Customer Accounts Extensibility as shown below.

Checkout UI Extension Scaffolding

Super simple, run shopify app init to create you project. You can choose what name you like for your project. In this example, our project name is checkout-practice-app

  
C:\shopify>shopify app init

Welcome. Let’s get started by naming your app project. You can change it later.

?  Your project name?
√  checkout-practice-app

?  Get started building your app:
√  Start by adding your first extension

╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Initializing project with `npm`                                               │
│  Use the `--package-manager` flag to select a different package manager.       │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯


╭─ success ──────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  checkout-practice-app is ready for you to build!                              │
│                                                                                │
│  Next steps                                                                    │
│    • Run `cd checkout-practice-app`                                            │
│    • For extensions, run `shopify app generate extension`                      │
│    • To see your app, run `shopify app dev`                                    │
│                                                                                │
│  Reference                                                                     │
│    • Shopify docs [1]                                                          │
│    • For an overview of commands, run `shopify app --help`                     │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯
[1] https://shopify.dev
  

Checkout UI Extension App

Next step is to create the checkout UI extension. This is the code that will modify the checkout page. Change to you project directory and run npm run generate extension. You can choose what technology stack you'ld like to work in. I have chosen JavaScript React for this example.

  
C:\shopify\checkout-practice-app>npm run generate extension

> checkout-practice-app@1.0.0 generate
> shopify app generate extension


Before proceeding, your project needs to be associated with an app.

?  Create this project as a new app on Shopify?
√  Yes, create it as a new app

?  App name:
√  checkout-practice-app

╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Using shopify.app.toml:                                                       │
│                                                                                │
│    • Org:             Joel Patrick Llosa                                       │
│    • App:             checkout-practice-app                                    │
│                                                                                │
│   You can pass `--reset` to your command to reset your app configuration.      │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

?  Type of extension?
√  Checkout UI

?  Name your extension:
√  free-shirt-1000

?  What would you like to work in?
√  JavaScript React


╭─ success ──────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Your extension was created in extensions/free-shirt-1000.                     │
│                                                                                │
│  Next steps                                                                    │
│    • To preview this extension along with the rest of the project, run `npm    │
│      run shopify app dev`                                                      │
│                                                                                │
│  Reference                                                                     │
│    • For more details, see the docs [1]                                        │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯
[1] https://shopify.dev/api/checkout-extensions/checkout/configuration
  

Take a look at your Shopify Partners portal page and your app should be there.

Checkout UI Extension Logic

Checkout.jsx

  
import {
  BlockLayout,
  InlineLayout,
  Text,
  useTranslate,
  reactExtension,
  useSettings,
  useSubtotalAmount,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
  'purchase.checkout.cart-line-list.render-after',
  () => ,
);

function Extension() {
  const translate = useTranslate();
  const { freebie_title: freeItem } = useSettings();
  const { amount } = useSubtotalAmount();

  if (freeItem && amount > 1000) {
    return (
      <InlineLayout columns={['fill', '20%']}>
          <Text>{ freeItem }</Text>
          <BlockLayout inlineAlignment="end">
              <Text>Free</Text>
          </BlockLayout>
      </InlineLayout>
    );
  }  
}
  

What does this piece of code do? This UI extension targets purchase.checkout.cart-line-list.render-after. Which means this UI will be inserted at that target location. It will be rendered after all line items.

Moving along, we use 3 APIs provided by Shopify. The useTranslate hook returns the I18nTranslate interface used to translate strings. We can do something like <Text>{translate('welcome')}</Text> which pulls the welcome message in en.default.json and renders it in a Text component. But we won't be using it in this example.

The useSettings hook returns the settings values defined by the merchant for the extension. These settings values are found in shopify.extension.toml. The value of the key freebie_title is then assigned to freeItem. We can set the value in the customize mode of the checkout page which is driven by the below extension settings.

shopify.extension.toml

  
...snipped...

[extensions.settings]
  [[extensions.settings.fields]]
    key = "freebie_title"
    type = "single_line_text_field"
    name = "Freebie title"
    description = "Free item name"
  

The useSubtotalAmount API returns a Money value representing the subtotal value of the items in the cart at the current step of checkout. Obviously we'll need the amount to check if it is more than a thousand so we can render the free item. So if there is a free item set and the subtotal is more than a thousand we render the free item using Shopify provided React components. This ends the coding part.

Running and Customizing the App

Time to run the app. Execute npm run dev and follow the instructions (e.g. "P" to preview in your browser).

  
C:\shopify\checkout-practice-app>npm run dev

> checkout-practice-app@1.0.0 dev
> shopify app dev

?  Which store would you like to use to view your project?
√  checkout-practice

╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Using shopify.app.toml:                                                       │
│                                                                                │
│    • Org:             Joel Patrick Llosa                                       │
│    • App:             checkout-practice-app                                    │
│    • Dev store:       checkout-practice.myshopify.com                          │
│    • Update URLs:     Not yet configured                                       │
│                                                                                │
│   You can pass `--reset` to your command to reset your app configuration.      │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

✔ Created extension free-shirt-1000.
07:43:25 │ graphiql   │ GraphiQL server started on port 3457
07:43:25 │ extensions │ Bundling UI extension free-shirt-1000...
07:43:25 │ extensions │ Parsed locales for extension free-shirt-1000 at
C:/shopify/checkout-practice-app/extensions/free-shirt-1000
07:43:25 │ extensions │ free-shirt-1000 successfully built
07:43:25 │ extensions │ Parsed locales for extension free-shirt-1000 at
C:/shopify/checkout-practice-app/extensions/free-shirt-1000
07:43:27 │ extensions │ Draft updated successfully for extension: free-shirt-1000

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
› Press d │ toggle development store preview: ✔ on
› Press g │ open GraphiQL (Admin API) in your browser
› Press p │ preview in your browser
› Press q │ quit

Preview URL: https://damaged-breakfast-campus-syria.trycloudflare.com/extensions/dev-console
GraphiQL URL: http://localhost:3457/graphiql
  

Before we can see our app in action, we need to do some bits and bobs. Go to Settings > Checkout > Customize to add the app block.

Add app block. Can you see the free-shirt-1000 extension?

App block added. Can you see it below the line items?

App block settings. Does it remind you of the extension settings in shopify.extension.toml? We're giving away an Acme Brand T-shirt for purchases over a thousand.

Now, go purchase something over a thousand and go to checkout. You should have something like below.

Try buying something that's belowe a thousand and go to checkout. You are not getting a free item.

Shopify Checkout UI Extension Summary

There you have it. Your first Shopify Checkout UI Extension. To recap, create the scaffolding using Shopify CLI. After that is in place generate an extension. Choose a target location in the checkout page and add your custom logic. Last bit is doing some bit of config to render your UI extension. Good luck creating your own Shopify Checkout UI Extension in the future. Grab the repo here, github.com/jpllosa/checkout-practice-app.