Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reset password using Accounts and Integration Hub #256

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions examples/subscriptions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# password-reset Elastic Path storefront starter

This project was generated with [Composable CLI](https://www.npmjs.com/package/composable-cli).

This storefront accelerates the development of a direct-to-consumer ecommerce experience using Elastic Path's modular products.

## Tech Stack

- [Elastic Path](https://www.elasticpath.com/products): A family of composable products for businesses that need to quickly & easily create unique experiences and next-level customer engagements that drive revenue.

- [Next.js](https://nextjs.org/): a React framework for building static and server-side rendered applications

- [Tailwind CSS](https://tailwindcss.com/): enabling you to get started with a range of out the box components that are
easy to customize

- [Headless UI](https://headlessui.com/): completely unstyled, fully accessible UI components, designed to integrate
beautifully with Tailwind CSS.

- [Radix UI Primitives](https://www.radix-ui.com/primitives): Unstyled, accessible, open source React primitives for high-quality web apps and design systems.

- [Typescript](https://www.typescriptlang.org/): a typed superset of JavaScript that compiles to plain JavaScript

## Getting Started

Run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page will hot reload as you edit the file.

## Deployment

Deployment is typical for a Next.js site. We recommend using a provider
like [Netlify](https://www.netlify.com/blog/2020/11/30/how-to-deploy-next.js-sites-to-netlify/)
or [Vercel](https://vercel.com/docs/frameworks/nextjs) to get full Next.js feature support.

## Current feature set reference

| **Feature** | **Notes** |
|------------------------------------------|-----------------------------------------------------------------------------------------------|
| PDP | Product Display Pages |
| PLP | Product Listing Pages. |
| EPCC PXM product variations | [Learn more](https://elasticpath.dev/docs/pxm/products/pxm-product-variations/pxm-variations) |
| EPCC PXM bundles | [Learn more](https://elasticpath.dev/docs/pxm/products/pxm-bundles/pxm-bundles) |
| EPCC PXM hierarchy-based navigation menu | Main site nav driven directly from your store's hierarchy and node structure |
| Prebuilt helper components | Some basic building blocks for typical ecommerce store features |
| Checkout | [Learn more](https://elasticpath.dev/docs/commerce-cloud/checkout/checkout-workflow) |
| Cart | [Learn more](https://elasticpath.dev/docs/commerce-cloud/carts/carts) |

## Notes on this starter

This starter is using the simple store implementation and add the password reset flow.
You'll need to ensure your email service is configured to send the password reset emails.

Here is how to do it using Composer and Postmark.
- Make sure you have a Postmark account and API key
- In Postmark, create a new template for the password reset email and note the template ID
- Go to Composer in Commerce Manager and select Postmark Email
- Add your Postmark API key
- In Event Mapping, add key `one-time-password-token-request.created` and value:

```json
{
"messagingProvider": {
"from": "noreply@<your-domain>.com",
"templateId": 38364654,
"to": "$.payload.user_authentication_info.email"
},
"dynamicFieldMapping": {
"username": "$.payload.user_authentication_info.email",
"token": "$.payload.one_time_password_token",
"password_profile_id": "$.payload.password_profile_id",
"user_authentication_info_id": "$.payload.user_authentication_info.id",
"user_authentication_password_profile_info_id": "$.payload.user_authentication_password_profile_info.id"
},
"metadata": {
"user_id": "$.payload.user_authentication_info.id"
}
}
```
- To learn more about One Time Password tokens, see [here](https://elasticpath.dev/guides/How-To/Authentication/how-to-utilize-one-time-password-tokens)
41 changes: 41 additions & 0 deletions examples/subscriptions/e2e/checkout-flow.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test } from "@playwright/test";
import { createD2CProductDetailPage } from "./models/d2c-product-detail-page";
import { client } from "./util/epcc-client";
import { createD2CCartPage } from "./models/d2c-cart-page";
import { createD2CCheckoutPage } from "./models/d2c-checkout-page";

test.describe("Checkout flow", async () => {
test("should perform product checkout", async ({ page }) => {
const productDetailPage = createD2CProductDetailPage(page, client);
const cartPage = createD2CCartPage(page);
const checkoutPage = createD2CCheckoutPage(page);

/* Go to simple product page */
await productDetailPage.gotoSimpleProduct();

/* Add the product to cart */
await productDetailPage.addProductToCart();

/* Go to cart page and checkout */
await cartPage.goto();
await cartPage.checkoutCart();

/* Enter information */
await checkoutPage.enterInformation({
"Email Address": { value: "[email protected]", fieldType: "input" },
"First Name": { value: "Jim", fieldType: "input" },
"Last Name": { value: "Brown", fieldType: "input" },
Address: { value: "Main Street", fieldType: "input" },
City: { value: "Brownsville", fieldType: "input" },
Region: { value: "Browns", fieldType: "input" },
Postcode: { value: "ABC 123", fieldType: "input" },
Country: { value: "Algeria", fieldType: "combobox" },
"Phone Number": { value: "01234567891", fieldType: "input" },
});

await checkoutPage.checkout();

/* Continue Shopping */
await checkoutPage.continueShopping();
});
});
10 changes: 10 additions & 0 deletions examples/subscriptions/e2e/home-page.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { test } from "@playwright/test";
import { createD2CHomePage } from "./models/d2c-home-page";
import { skipIfMissingCatalog } from "./util/missing-published-catalog";

test.describe("Home Page", async () => {
test("should load home page", async ({ page }) => {
const d2cHomePage = createD2CHomePage(page);
await d2cHomePage.goto();
});
});
23 changes: 23 additions & 0 deletions examples/subscriptions/e2e/models/d2c-cart-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Locator, Page } from "@playwright/test";

export interface D2CCartPage {
readonly page: Page;
readonly checkoutBtn: Locator;
readonly goto: () => Promise<void>;
readonly checkoutCart: () => Promise<void>;
}

export function createD2CCartPage(page: Page): D2CCartPage {
const checkoutBtn = page.getByRole("link", { name: "Checkout" });

return {
page,
checkoutBtn,
async goto() {
await page.goto(`/cart`);
},
async checkoutCart() {
await checkoutBtn.click();
},
};
}
48 changes: 48 additions & 0 deletions examples/subscriptions/e2e/models/d2c-checkout-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Locator, Page } from "@playwright/test";
import { fillAllFormFields, FormInput } from "../util/fill-form-field";
import { enterPaymentInformation as _enterPaymentInformation } from "../util/enter-payment-information";

export interface D2CCheckoutPage {
readonly page: Page;
readonly payNowBtn: Locator;
readonly checkoutBtn: Locator;
readonly goto: () => Promise<void>;
readonly enterInformation: (values: FormInput) => Promise<void>;
readonly checkout: () => Promise<void>;
readonly enterPaymentInformation: (values: FormInput) => Promise<void>;
readonly submitPayment: () => Promise<void>;
readonly continueShopping: () => Promise<void>;
}

export function createD2CCheckoutPage(page: Page): D2CCheckoutPage {
const payNowBtn = page.getByRole("button", { name: "Pay $" });
const checkoutBtn = page.getByRole("button", { name: "Pay $" });
const continueShoppingBtn = page.getByRole("link", {
name: "Continue shopping",
});

return {
page,
payNowBtn,
checkoutBtn,
async goto() {
await page.goto(`/cart`);
},
async enterPaymentInformation(values: FormInput) {
await _enterPaymentInformation(page, values);
},
async enterInformation(values: FormInput) {
await fillAllFormFields(page, values);
},
async submitPayment() {
await payNowBtn.click();
},
async checkout() {
await checkoutBtn.click();
},
async continueShopping() {
await continueShoppingBtn.click();
await page.waitForURL("/");
},
};
}
15 changes: 15 additions & 0 deletions examples/subscriptions/e2e/models/d2c-home-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Page } from "@playwright/test";

export interface D2CHomePage {
readonly page: Page;
readonly goto: () => Promise<void>;
}

export function createD2CHomePage(page: Page): D2CHomePage {
return {
page,
async goto() {
await page.goto("/");
},
};
}
132 changes: 132 additions & 0 deletions examples/subscriptions/e2e/models/d2c-product-detail-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type { Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import {
getProductById,
getSimpleProduct,
getVariationsProduct,
} from "../util/resolver-product-from-store";
import type {ElasticPath, ProductResponse } from "@elasticpath/js-sdk";
import { getCartId } from "../util/get-cart-id";
import { getSkuIdFromOptions } from "../../src/lib/product-helper";

const host = process.env.NEXT_PUBLIC_EPCC_ENDPOINT_URL;

export interface D2CProductDetailPage {
readonly page: Page;
readonly gotoSimpleProduct: () => Promise<void>;
readonly gotoVariationsProduct: () => Promise<void>;
readonly getCartId: () => Promise<string>;
readonly addProductToCart: () => Promise<void>;
readonly gotoProductVariation: () => Promise<void>;
}

export function createD2CProductDetailPage(
page: Page,
client: ElasticPath,
): D2CProductDetailPage {
let activeProduct: ProductResponse | undefined;
const addToCartBtn = page.getByRole("button", { name: "Add to Cart" });

return {
page,
async gotoSimpleProduct() {
activeProduct = await getSimpleProduct(client);
await skipOrGotoProduct(
page,
"Can't run test because there is no simple product published in the store.",
activeProduct,
);
},
async gotoVariationsProduct() {
activeProduct = await getVariationsProduct(client);
await skipOrGotoProduct(
page,
"Can't run test because there is no variation product published in the store.",
activeProduct,
);
},
async gotoProductVariation() {
expect(
activeProduct,
"Make sure you call one of the gotoVariationsProduct function first before calling gotoProductVariation",
).toBeDefined();
expect(activeProduct?.attributes.base_product).toEqual(true);

const expectedProductId = await selectOptions(activeProduct!, page);
const product = await getProductById(client, expectedProductId);

expect(product.data?.id).toBeDefined();
activeProduct = product.data;

/* Check to make sure the page has navigated to the selected product */
await expect(page).toHaveURL(`/products/${expectedProductId}`);
},
getCartId: getCartId(page),
async addProductToCart() {
expect(
activeProduct,
"Make sure you call one of the gotoProduct function first before calling addProductToCart",
).toBeDefined();
/* Get the cart id */
const cartId = await getCartId(page)();

/* Add the product to cart */
await addToCartBtn.click();
/* Wait for the cart POST request to complete */
const reqUrl = `https://${host}/v2/carts/${cartId}/items`;
await page.waitForResponse(reqUrl);

/* Check to make sure the product has been added to cart */
const result = await client.Cart(cartId).With("items").Get();
await expect(
activeProduct?.attributes.price,
"Missing price on active product - make sure the product has a price set can't add to cart without one.",
).toBeDefined();
await expect(
result.included?.items.find(
(item) => item.product_id === activeProduct!.id,
),
).toHaveProperty("product_id", activeProduct!.id);
},
};
}

async function skipOrGotoProduct(
page: Page,
msg: string,
product?: ProductResponse,
) {
if (!product) {
test.skip(!product, msg);
} else {
await page.goto(`/products/${product.id}`);
}
}

async function selectOptions(
baseProduct: ProductResponse,
page: Page,
): Promise<string> {
/* select one of each variation option */
const options = baseProduct.meta.variations?.reduce((acc, variation) => {
return [...acc, ...([variation.options?.[0]] ?? [])];
}, []);

if (options && baseProduct.meta.variation_matrix) {
for (const option of options) {
await page.click(`text=${option.name}`);
}

const variationId = getSkuIdFromOptions(
options.map((x) => x.id),
baseProduct.meta.variation_matrix,
);

if (!variationId) {
throw new Error("Unable to resolve variation id.");
}
return variationId;
}

throw Error("Unable to select options they were not defined.");
}
29 changes: 29 additions & 0 deletions examples/subscriptions/e2e/product-details-page.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test } from "@playwright/test";
import { createD2CProductDetailPage } from "./models/d2c-product-detail-page";
import { client } from "./util/epcc-client";
import { skipIfMissingCatalog } from "./util/missing-published-catalog";

test.describe("Product Details Page", async () => {
test("should add a simple product to cart", async ({ page }) => {
const productDetailPage = createD2CProductDetailPage(page, client);

/* Go to base product page */
await productDetailPage.gotoSimpleProduct();

/* Add the product to cart */
await productDetailPage.addProductToCart();
});

test("should add variation product to cart", async ({ page }) => {
const productDetailPage = createD2CProductDetailPage(page, client);

/* Go to base product page */
await productDetailPage.gotoVariationsProduct();

/* Select the product variations */
await productDetailPage.gotoProductVariation();

/* Add the product to cart */
await productDetailPage.addProductToCart();
});
});
Loading