Showing posts with label node. Show all posts
Showing posts with label node. Show all posts

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.

Sunday, August 13, 2023

React FE Spring Boot Example

In this article we will demonstrate a React Front-End app served through Spring Boot's embedded Tomcat. Why do it this way? We can serve a React app in different ways (e.g. Apache Web Server, Nginx, etc.). What is so special about serving a React app through Spring Boot's Tomcat? There is nothing special. A reason could be that the development team is already well versed in Spring Boot with Tomcat. They have mastered configuring Tomcat and investing in a new web server (e.g. Nginx) is of little value. Another could be the deployment pipeline is already set up to do Java/Maven deployment (e.g. Jenkins). Lastly, it could be that you are a new hire and the development team already do it that way. That is, we don't have a choice but to support an existing React FE app served through Spring Boot.

Our focus will be on building the React app so that it can be deployed through a Spring Boot fat jar. We will lightly touch on starting a Spring Boot and React apps. It is assumed that the reader has knowledge regarding Node, React, Java, Maven, Spring Boot, etc. The reader should know how to set up the mentioned technology stack.

If you have been reading my past blogs, then the previous paragraphs sound so familiar. That is because I have done a similar thing for an Angular app. Essentially this article is similar to Angular FE Spring Boot Example. But I added in an extra. In my Angular FE Spring Boot Example, we had to do two steps to build the fat jar. In this article, we will build the fat jar in one step. Sounds good? Let's do it!

Building

Same as in my Angular FE Spring Boot Example, there were two ways to build a React app so that it can be deployed as single Spring Boot fat jar. First one is to use the exec-maven-plugin. Second is to use the maven-resources-plugin.

But wait. There's more. We'll be using the maven-resource-plugin in combination with frontend-maven-plugin. With these two plugins working together, we only need to run one command to build the fat jar. This means, we have leveled up from Angular FE Spring Boot Example.

Spring Initializr

Head over to Spring Initializr and it should look like below:

React FE Sprint Boot Example
Spring Initializr

React Bit

Under the root directory, create a dev folder. This is where all the React stuff goes. Assuming you have Node, Node Version Manager and Node Package Manager ready, run npx create-react-app my-app to set up a web app. We should have something like below:


D:\workspace\react-fe-spring-boot\dev>npx create-react-app my-app
Need to install the following packages:
  create-react-app@5.0.1
Ok to proceed? (y) y

Creating a new React app in D:\workspace\react-fe-spring-boot\dev\my-app.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...

Created git commit.

Success! Created my-app at D:\workspace\react-fe-spring-boot\dev\my-app
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd my-app
  npm start

Happy hacking!

We follow the bottom two commands (i.e. cd my-app, npm start) and we should have something like below. For more details, head over to Create React App. Thank you Create React App for creating our project. The React app should be available at localhost:3000.

React FE Sprint Boot Example
React app on port 3000

Build the React project, npm run build. This will create files in the build directory. Take note of this folder as we will need it in our POM file. We should see something like below after the build:


D:\workspace\react-fe-spring-boot\dev>npm run build

> my-app@0.1.0 build
> react-scripts build

Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  46.61 kB  build\static\js\main.46f5c8f5.js
  1.78 kB   build\static\js\787.28cb0dcd.chunk.js
  541 B     build\static\css\main.073c9b0a.css

The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  https://cra.link/deployment

Maven Bit

We don't need to change any Java code. The key is in the POM. We will copy the build files over to the classes/static folder and then Maven will take care of making the fat jar. Add the maven-resources-plugin like below (make sure you get the diretories right ;) ):


<plugin> 
	<artifactId>maven-resources-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-resources</id>
			<phase>validate</phase>
			<goals>
				<goal>copy-resources</goal>
			</goals> 
			<configuration>
				<outputDirectory>${build.directory}/classes/static/</outputDirectory>
				<resources>
					<resource>
						<directory>dev/my-app/build</directory>
					</resource>
				</resources>
			</configuration>
		</execution>
	</executions>
</plugin>

Running the Fat Jar

Do a mvn clean package. Now, run java -jar ./target/react-fe-spring-boot-0.0.1-SNAPSHOT.jar and we should be able to see the it running on localhost:8080. It's now being served by Tomcat.

React FE Sprint Boot Example
React app on port 8080

Wait? What?! That was 2 steps to build it into a fat jar! What gives?

One Command to Build Them All

To build it all in one command, we'll enlist the help of the frontend-maven-plugin and a minor change on maven-resource-plugin. Before we do that, let's review the goals bound to the package phase, run mvn help:describe -Dcmd=package.


D:\workspace\react-fe-spring-boot>mvn help:describe -Dcmd=package

[INFO] 'package' is a phase corresponding to this plugin:
org.apache.maven.plugins:maven-jar-plugin:2.4:jar

It is a part of the lifecycle for the POM packaging 'jar'. This lifecycle includes the following phases:
* validate: Not defined
* initialize: Not defined
* generate-sources: Not defined
* process-sources: Not defined
* generate-resources: Not defined
* process-resources: org.apache.maven.plugins:maven-resources-plugin:2.6:resources
* compile: org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
* process-classes: Not defined
* generate-test-sources: Not defined
* process-test-sources: Not defined
* generate-test-resources: Not defined
* process-test-resources: org.apache.maven.plugins:maven-resources-plugin:2.6:testResources
* test-compile: org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile
* process-test-classes: Not defined
* test: org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
* prepare-package: Not defined
* package: org.apache.maven.plugins:maven-jar-plugin:2.4:jar
* pre-integration-test: Not defined
* integration-test: Not defined
* post-integration-test: Not defined
* verify: Not defined
* install: org.apache.maven.plugins:maven-install-plugin:2.4:install
* deploy: org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.947 s
[INFO] Finished at: 2023-08-06T23:04:08+01:00
[INFO] ------------------------------------------------------------------------

First change is to make maven-resources-plugin execute on the generate-sources phase. Change validate like so.


<plugin> 
	<artifactId>maven-resources-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-resources</id>
			<!-- <phase>validate</phase> -->
			<phase>generate-sources</phase>
			...snipped...

Next is add the frontend-maven-plugin like so.


<plugin>
	<groupId>com.github.eirslett</groupId>
	<artifactId>frontend-maven-plugin</artifactId>
	<version>1.11.3</version>
	<configuration>
		<workingDirectory>dev/my-app</workingDirectory>
		<nodeVersion>v18.10.0</nodeVersion>
		<yarnVersion>v1.22.17</yarnVersion>
	</configuration>
	<executions>
		<execution>
			<id>install-frontend-tools</id>
			<goals>
				<goal>install-node-and-yarn</goal>
			</goals>
			<phase>validate</phase>
		</execution>
		<execution>
			<id>build-frontend</id>
			<goals>
				<goal>yarn</goal>
			</goals>
			<phase>initialize</phase>
			<configuration>
				<arguments>build</arguments>
			</configuration>
		</execution>
	</executions>
</plugin>

Now, do you know why I had to show you the Maven package phases? During the package phase, the first thing that will be done by the frontend plugin will be to install node and yarn (i.e. validate phase). After that is building the React app, yarn build (i.e. initialize phase). And lastly, resources will copy the files over (i.e. generate-sources phase). Viola! We're ready to run the build again. Remove the built files (e.g. mvn clean or delete target directory, delete the React app build directory) and then do a mvn clean package. You should see something like below. After the build you should be able to run the jar like before.


D:\workspace\react-fe-spring-boot>mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.blogspot.jpllosa:react-fe-spring-boot >--------------
[INFO] Building react-fe-spring-boot 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:3.2.0:clean (default-clean) @ react-fe-spring-boot ---
[INFO]
[INFO] --- frontend-maven-plugin:1.11.3:install-node-and-yarn (install-frontend-tools) @ react-fe-spring-boot ---
[INFO] Installing node version v18.10.0
[INFO] Copying node binary from C:\Users\jpllosa\.m2\repository\com\github\eirslett\node\18.10.0\node-18.10.0-win-x64.exe to D:\workspace\react-fe-spring-boot\dev\my-app\node\node.exe
[INFO] Installed node locally.
[INFO] Installing Yarn version v1.22.17
[INFO] Unpacking C:\Users\jpllosa\.m2\repository\com\github\eirslett\yarn\1.22.17\yarn-1.22.17.tar.gz into D:\workspace\react-fe-spring-boot\dev\my-app\node\yarn
[INFO] Installed Yarn locally.
[INFO]
[INFO] --- frontend-maven-plugin:1.11.3:yarn (build-frontend) @ react-fe-spring-boot ---
[INFO] Running 'yarn build' in D:\workspace\react-fe-spring-boot\dev\my-app
[INFO] yarn run v1.22.17
[INFO] $ react-scripts build
[INFO] Creating an optimized production build...
[INFO] Compiled successfully.
[INFO]
[INFO] File sizes after gzip:
[INFO]
[INFO]   46.61 kB  build\static\js\main.46f5c8f5.js
[INFO]   1.78 kB   build\static\js\787.28cb0dcd.chunk.js
[INFO]   541 B     build\static\css\main.073c9b0a.css
[INFO]
[INFO] The project was built assuming it is hosted at /.
[INFO] You can control this with the homepage field in your package.json.
[INFO]
[INFO] The build folder is ready to be deployed.
[INFO] You may serve it with a static server:
[INFO]
[INFO]   npm install -g serve
[INFO]   serve -s build
[INFO]
[INFO] Find out more about deployment here:
[INFO]
[INFO]   https://cra.link/deployment
[INFO]
[INFO] Done in 8.49s.
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:copy-resources (copy-resources) @ react-fe-spring-boot ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 15 resources
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ react-fe-spring-boot ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 0 resource

There you have it. Serving a Rect app through Spring Boot's fat jar and embedded Tomcat. Plus building it in one command! Grab the full repo here, github.com/jpllosa/react-fe-spring-boot

Friday, July 28, 2023

Spring ResourceTransformer Example

Here's an example of how to handle static web resources in Spring. Let us pretend that one of the business requirement of our Angular FE Spring Boot app is A/B testing. A/B testing is a method of comparing two versions of an app or webpage against each other to determine which one performs better. In short, we'll need a control webpage and a variant webpage. For the control webpage, there will be no changes. For the variant webpage, we'll need to inject "ab-testing.js" in the head of our webpage.

We will not be talking about how to do A/B testing. Instead, we will focus on how to inject the script tag in the head of our webpage. Requirements clear enough? Let's begin. Below is our control webpage.

Spring ResourceTransformer Example
Control Page

We will utilize the code from my previous blog, Angular FE Spring Boot Example and you can clone the code here, github.com/jpllosa/angular-fe-spring-boot.

Two Strategies

As far as I know, there are two ways to fulfill this business requirement.

First one is to insert the script tag when ngOnInit is called in a Component class.

Second is to use Spring's ResourceTransformer. We will use this method because the script tag is injected server side. Prior to serving the HTML file, it is transformed. The first strategy happens on the client side which is why I think this is a better solution. This also seems better because the "ab-testing.js" is already in the head prior to DOM manipulation by Angular. So let's get to it.

POM

Update the POM file. We need the dependency below for IOUtils.


<dependency>
	<groupId>commons-io</groupId>
	<artifactId>commons-io</artifactId>
	<version>2.11.0</version>
</dependency>

index.html

Add a marker text in dev/my-app/src/index.html, like so:


<head>
  ... snipped ...
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- INJECT_HERE -->
</head>

Java Code

Create a com.blogspot.jpllosa.transformer package and add the following files below:


package com.blogspot.jpllosa.transformer;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;


@Configuration
public class IndexTransformationConfigurer extends WebMvcConfigurerAdapter {
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("index.html")
			.addResourceLocations("classpath:/static/")
			.resourceChain(false)
			.addTransformer(new IndexTransformer());
	}
}

We create the class above so that we can add a handler when serving the index.html file. Here it specifies that the index.html file from the classpath:/static/ location will have to pass through a Transformer. Other static resources will not be affected. We are not caching the result of the resource resolution (i.e. resourceChain(false)).


package com.blogspot.jpllosa.transformer;

import org.apache.commons.io.IOUtils;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.ResourceTransformerChain;
import org.springframework.web.servlet.resource.TransformedResource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Service
public class IndexTransformer implements ResourceTransformer {
	@Override
	public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain chain) throws IOException {
		String html = IOUtils.toString(resource.getInputStream(), "UTF-8");
		html = html.replace("<!-- INJECT_HERE -->", 
			"<script src=\"//third.party.com/ab-testing.js\"> </script>");
		return new TransformedResource(resource, html.getBytes());
	}
}

We've kept our transformer simple for this example. The code is readable enough as to what it is doing, isn't it? We are replacing the placeholder text with the script tag. Job done.

Demonstration

If you have read the Angular FE Spring Boot Example then you should know what to do next. In case you have not. I'll reiterate.

Build the Angular project, ng build on dev/my-app. This will create files in the dist directory.

Run mvn clean package on the project root directory. After that, run java -jar ./target/angular-fe-spring-boot-0.0.1-SNAPSHOT.jar

and we should be able to see something like below running on localhost:8080. Is "ab-testing.js" there?
Spring ResourceTransformer Example
Variant Page

There you have it. A quick example of how to handle or transform static web resources in Spring. Grab the full repo here, github.com/jpllosa/angular-fe-spring-boot

Monday, June 26, 2023

Angular FE Spring Boot Example

In this article we will demonstrate an Angular Front-End app served through Spring Boot's embedded Tomcat. Why do it this way? We can serve an Angular app in different ways (e.g. Apache Web Server, Nginx, etc.). What is so special about serving an Angular app through Spring Boot's Tomcat? There is nothing special. A reason could be that the development team is already well versed in Spring Boot with Tomcat. They have mastered configuring Tomcat and investing in a new web server (e.g. Nginx) is of little value. Another could be the deployment pipeline is already set up to do Java/Maven deployment (e.g. Jenkins). Lastly, it could be that you are a new hire and the development team already do it that way. That is, we don't have a choice but to support an existing Angular FE app served through Spring Boot.

Our focus will be on building the Angular app so that it can be deployed through a Spring Boot fat jar. We will lightly touch on starting a Spring Boot and Angular apps. It is assumed that the reader has knowledge regarding Node, Angular CLI, Java, Maven, Spring Boot, etc. The reader should know how to set up the mentioned technology stack.

Two Ways to Build

As far as I know, there are two ways to build an Angular app so that it can be deployed as single Spring Boot fat jar.

First one is to use the exec-maven-plugin. This plugin runs the ng build and the outputPath of the Angular app must point to src/main/resources/static so that Angular dist files are bundled in the jar during packaging.

Second is to use the maven-resources-plugin. This plugin copies the Angular dist files to the classes/static directory of the Spring Boot app. This is the method we will be demonstrating. This way seems to be cleaner because it doesn't populate any files under the Java section of the code. The Angular build code goes straight into the fat jar. This way, all Angular development code and work remain in a separate directory from the Java side of things. And only become one when we build the fat jar. So let's get to it.

Spring Initializr

Head over to Spring Initializr and it should look like below:

Spring Initialzr

Angular Bit

Under the root directory, create a dev folder. This is where all the Angular stuff goes. Assuming you have Node, Node Version Manager and Node Package Manager ready, run npm install -g @angular/cli to install the Angular CLI. Check your installation, ng -v. We should have something like below:

Angular Version

Create an Angualr starter app under the dev folder, ng new my-app. This will create the scaffolding. Change directory to my-app, cd my-app, then run g serve. Open localhost:4200, we should have something like below:

My-App on 4200

Build the Angular project, ng build. This will create files in the dist directory. Take note of this folder as we will need it in our POM file. We should see something like below after the build:

Built Files

These are the built files found in the dist folder.

dist Contents

Maven Bit

We don't need to change any Java code. The key is in the POM. We will copy the dist files over to the classes/static folder and then Maven (mvn clean package) will take care of making the fat jar. Add the maven-resources-plugin like below (make sure you get the diretories right ;) ):


<plugin> 
	<artifactId>maven-resources-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-resources</id>
			<phase>validate</phase>
			<goals>
				<goal>copy-resources</goal>
			</goals> 
			<configuration>
				<outputDirectory>${build.directory}/classes/static/</outputDirectory>
				<resources>
					<resource>
						<directory>dev/my-app/dist/my-app</directory>
					</resource>
				</resources>
			</configuration>
		</execution>
	</executions>
</plugin>

Below is the key bit when running mvn clean package. This tells us the files copied from Angular to Java classes. The number of files should match!

mvn clean package

Demonstration

Now, run java -jar ./target/angular-fe-spring-boot-0.0.1-SNAPSHOT.jar

and we should be able to see the it running on localhost:8080. Did you notice that it's on a new port this time? It's now being served by Tomcat.
My-App Served Through Spring Boot's Tomcat

There you have it. A nice way of serving an Angular app through Spring Boot's fat jar and embedded Tomcat. Grab the full repo here, github.com/jpllosa/angular-fe-spring-boot