Skip to content

Commit

Permalink
Introduces CustomerSegmentationTemplate for ui-extensions and ui-exte…
Browse files Browse the repository at this point in the history
…nsions-react

Adds changeset

Updates comment

Fixes typo

Moves CustomerSegmentationTemplateComponent to extension-points file

Moves I18n to shared utility, Renamed extension points

Properly re-export I18n and I18nTranslate

Fixes examples, Renames prop to icon

Uses a type instead of an enum. Adds comments on enabled features

Drops Polaris from comment

Uses single extension point

Returns null

Better examples

Fixes conditional in example

Fixes QL string in examples

Uses render instead of show

Marks enabledFeatures as private and add __ prefix

Moves I18n to API

Uses camelCase for categories, Make categories private

Uses camelCase for enabled features type
  • Loading branch information
loic-d committed Feb 24, 2023
1 parent bbb75b5 commit 4e71987
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 79 deletions.
6 changes: 6 additions & 0 deletions .changeset/silent-ligers-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/ui-extensions': patch
'@shopify/ui-extensions-react': patch
---

Introduces CustomerSegmentationTemplate component
2 changes: 2 additions & 0 deletions packages/ui-extensions-react/src/surfaces/admin/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export {CardSection} from './components/CardSection/CardSection';
export type {CardSectionProps} from './components/CardSection/CardSection';
export {Checkbox} from './components/Checkbox/Checkbox';
export type {CheckboxProps} from './components/Checkbox/Checkbox';
export {CustomerSegmentationTemplate} from './components/CustomerSegmentationTemplate/CustomerSegmentationTemplate';
export type {CustomerSegmentationTemplateProps} from './components/CustomerSegmentationTemplate/CustomerSegmentationTemplate';
export {Heading} from './components/Heading/Heading';
export type {HeadingProps} from './components/Heading/Heading';
export {Icon} from './components/Icon/Icon';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {CustomerSegmentationTemplate as BaseCustomerSegmentationTemplate} from '@shopify/ui-extensions/admin';
import {createRemoteReactComponent} from '@remote-ui/react';

export const CustomerSegmentationTemplate = createRemoteReactComponent(
BaseCustomerSegmentationTemplate,
);
export type {CustomerSegmentationTemplateProps} from '@shopify/ui-extensions/admin';
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import {
reactExtension,
CustomerSegmentationTemplate,
} from '@shopify/ui-extensions-react/admin';

function App({i18n, enabledFeatures, category}) {
if (category == 'reEngageCustomers') {
const productsPurchasedOnTagsEnabled = enabledFeatures.includes('productsPurchasedByTags');
const templateQuery = productsPurchasedOnTagsEnabled
? 'products_purchased(tag: (product_tag)) = true'
: 'products_purchased(id: (product_id)) = true';
const templateQueryToInsert = productsPurchasedOnTagsEnabled
? 'products_purchased(tag:'
: 'products_purchased(id:';

return (
<CustomerSegmentationTemplate
title={i18n.translate('product.title')}
description={i18n.translate('product.description')}
icon='productsMajor'
templateQuery={templateQuery}
templateQueryToInsert={templateQueryToInsert}
/>
);
}

if (category == 'location') {
return (
<CustomerSegmentationTemplate
title={i18n.translate('location.title')}
description={i18n.translate('location.description')}
icon='locationMajor'
templateQuery="customer_cities CONTAINS 'US-NY-NewYorkCity'"
/>
);
}

return null;
}

reactExtension('admin.customers.segmentation-templates.render', ({i18n, __category, __enabledFeatures}) => <App i18n={i18n} enabledFeatures={__enabledFeatures} category={__category} />);
73 changes: 73 additions & 0 deletions packages/ui-extensions/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,76 @@
export interface StandardApi {
readonly extensionPoint: string;
}

/**
* This defines the i18n.translate() signature.
*/
export interface I18nTranslate {
/**
* This returns a translated string matching a key in a locale file.
*
* @example translate("banner.title")
*/
<ReplacementType = string>(
key: string,
options?: {[placeholderKey: string]: ReplacementType | string | number},
): ReplacementType extends string | number
? string
: (string | ReplacementType)[];
}

export interface I18n {
/**
* Returns a localized number.
*
* This function behaves like the standard `Intl.NumberFormat()`
* with a style of `decimal` applied. It uses the buyer's locale by default.
*
* @param options.inExtensionLocale - if true, use the extension's locale
*/
formatNumber: (
number: number | bigint,
options?: {inExtensionLocale?: boolean} & Intl.NumberFormatOptions,
) => string;

/**
* Returns a localized currency value.
*
* This function behaves like the standard `Intl.NumberFormat()`
* with a style of `currency` applied. It uses the buyer's locale by default.
*
* @param options.inExtensionLocale - if true, use the extension's locale
*/
formatCurrency: (
number: number | bigint,
options?: {inExtensionLocale?: boolean} & Intl.NumberFormatOptions,
) => string;

/**
* Returns a localized date value.
*
* This function behaves like the standard `Intl.DateTimeFormatOptions()` and uses
* the buyer's locale by default. Formatting options can be passed in as
* options.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat0
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options
*
* @param options.inExtensionLocale - if true, use the extension's locale
*/
formatDate: (
date: Date,
options?: {inExtensionLocale?: boolean} & Intl.DateTimeFormatOptions,
) => string;

/**
* Returns translated content in the buyer's locale,
* as supported by the extension.
*
* - `options.count` is a special numeric value used in pluralization.
* - The other option keys and values are treated as replacements for interpolation.
* - If the replacements are all primitives, then `translate()` returns a single string.
* - If replacements contain UI components, then `translate()` returns an array of elements.
*/
translate: I18nTranslate;
}
2 changes: 2 additions & 0 deletions packages/ui-extensions/src/surfaces/admin/api.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export type {I18n, I18nTranslate} from '../../api';
export type {StandardApi} from './api/standard/standard';
export type {CustomerSegmentationTemplateApi} from './api/customer-segmentation-template/customer-segmentation-template';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {StandardApi} from '../standard/standard';
import type {I18n} from '../../../../api';
import type {ExtensionPoint as AnyExtensionPoint} from '../../extension-points';

/* List of enabled query language features during a progressive rollout */
type CustomerSegmentationFeature =
/* Allows merchants to segment on products purchased by tags. For example: products_purchased(tag: 'Red hats') = true */
| 'productsPurchasedByTags'
/* Enables count aggregates on functions. For example: shopify_email.opened(count_at_least: 5) = true */
| 'aggregateFilters';

type TemplateCategory =
| 'all'
| 'firstTimeBuyers'
| 'highValueCustomers'
| 'reEngageCustomers'
| 'abandonedCheckout'
| 'purchaseBehaviour'
| 'location';

export interface CustomerSegmentationTemplateApi<
ExtensionPoint extends AnyExtensionPoint,
> extends StandardApi<ExtensionPoint> {
/* Utilities for translating content according to the current `localization` of the admin. */
i18n: I18n;
/** @private */
__category: TemplateCategory;
/** @private */
__enabledFeatures: CustomerSegmentationFeature[];
}
2 changes: 2 additions & 0 deletions packages/ui-extensions/src/surfaces/admin/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export {CardSection} from './components/CardSection/CardSection';
export type {CardSectionProps} from './components/CardSection/CardSection';
export {Checkbox} from './components/Checkbox/Checkbox';
export type {CheckboxProps} from './components/Checkbox/Checkbox';
export {CustomerSegmentationTemplate} from './components/CustomerSegmentationTemplate/CustomerSegmentationTemplate';
export type {CustomerSegmentationTemplateProps} from './components/CustomerSegmentationTemplate/CustomerSegmentationTemplate';
export {Heading} from './components/Heading/Heading';
export type {HeadingProps} from './components/Heading/Heading';
export {Icon} from './components/Icon/Icon';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {createRemoteComponent} from '@remote-ui/core';

type Source =
| 'categoriesMajor'
| 'firstVisitMajor'
| 'heartMajor'
| 'marketingMajor'
| 'checkoutMajor'
| 'ordersMajor'
| 'locationMajor'
| 'emailNewsletterMajor'
| 'firstOrderMajor'
| 'billingStatementDollarMajor'
| 'diamondAlertMajor'
| 'abandonedCartMajor'
| 'calendarMajor'
| 'productsMajor'
| 'globeMajor'
| 'flagMajor'
| 'uploadMajor'
| 'buyButtonMajor'
| 'followUpEmailMajor';

export interface CustomerSegmentationTemplateProps {
/** Localized title of the template. */
title: string;
/** Localized description of the template. */
description: string;
/** Icon identifier for the template. This property is ignored for non-1P Segmentation templates as we fallback to the app icon */
icon?: Source;
/** ShopifyQL code snippet to render in the template with syntax highlighting **/
templateQuery: string;
/** ShopifyQL code snippet to insert in the segment editor. If missing, `templateQuery` will be used. */
templateQueryToInsert?: string;
}

/**
* Customer segmentation templates are used to give merchants a starting point to create segments.
*/
export const CustomerSegmentationTemplate = createRemoteComponent<
'CustomerSegmentationTemplate',
CustomerSegmentationTemplateProps
>('CustomerSegmentationTemplate');
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
extension,
CustomerSegmentationTemplate,
} from '@shopify/ui-extensions/admin';

export default extension(
'admin.customers.segmentation-templates.render',
(root, {i18n, __category, __enabledFeatures}) => {
if (__category === 'reEngageCustomers') {
const productsPurchasedOnTagsEnabled = __enabledFeatures.includes('productsPurchasedByTags');
const productTemplate = root.createComponent(CustomerSegmentationTemplate, {
title: i18n.translate('product.title'),
description: i18n.translate('product.description'),
icon: 'productsMajor',
templateQuery: productsPurchasedOnTagsEnabled
? 'products_purchased(tag: (product_tag)) = true'
: 'products_purchased(id: (product_id)) = true',
templateQueryToInsert: productsPurchasedOnTagsEnabled
? 'products_purchased(tag:'
: 'products_purchased(id:',
});

root.appendChild(productTemplate);
}

if (__category === 'location') {
const locationTemplate = root.createComponent(CustomerSegmentationTemplate, {
title: i18n.translate('location.title'),
description: i18n.translate('location.description'),
icon: 'locationMajor',
templateQuery: "customer_cities CONTAINS 'US-NY-NewYorkCity'",
});

root.appendChild(locationTemplate);
}

root.mount();
},
);
13 changes: 11 additions & 2 deletions packages/ui-extensions/src/surfaces/admin/extension-points.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import type {RenderExtension} from '../../extension';

import type {AnyComponent} from './shared';
import type {StandardApi} from './api';
import type {AnyComponent, Components} from './shared';
import type {StandardApi, CustomerSegmentationTemplateApi} from './api';
import {AnyComponentBuilder} from '../../shared';

type CustomerSegmentationTemplateComponent = AnyComponentBuilder<
Pick<Components, 'CustomerSegmentationTemplate'>
>;

export interface ExtensionPoints {
Playground: RenderExtension<StandardApi<'Playground'>, AnyComponent>;
'admin.customers.segmentation-templates.render': RenderExtension<
CustomerSegmentationTemplateApi<'admin.customers.segmentation-templates.render'>,
CustomerSegmentationTemplateComponent
>;
}

export type ExtensionPoint = keyof ExtensionPoints;
Expand Down
3 changes: 1 addition & 2 deletions packages/ui-extensions/src/surfaces/checkout/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type {I18n, I18nTranslate} from '../../api';
export type {CountryCode, CurrencyCode, Timezone} from './api/shared';
export type {
StandardApi,
Expand Down Expand Up @@ -58,8 +59,6 @@ export type {
DiscountCodeChangeResult,
DiscountCodeChangeResultError,
DiscountCodeChangeResultSuccess,
I18n,
I18nTranslate,
Currency,
Language,
Localization,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {StatefulRemoteSubscribable} from '@remote-ui/async-subscription';

import type {StandardApi as BaseStandardApi} from '../../../../api';
import type {StandardApi as BaseStandardApi, I18n} from '../../../../api';
import type {ExtensionPoint as AnyExtensionPoint} from '../../extension-points';
import type {CurrencyCode, CountryCode, Timezone} from '../shared';

Expand Down Expand Up @@ -350,79 +349,6 @@ export interface AppMetafieldEntry {

export type Version = 'unstable';

/**
* This defines the i18n.translate() signature.
*/
export interface I18nTranslate {
/**
* This returns a translated string matching a key in a locale file.
*
* @example translate("banner.title")
*/
<ReplacementType = string>(
key: string,
options?: {[placeholderKey: string]: ReplacementType | string | number},
): ReplacementType extends string | number
? string
: (string | ReplacementType)[];
}

export interface I18n {
/**
* Returns a localized number.
*
* This function behaves like the standard `Intl.NumberFormat()`
* with a style of `decimal` applied. It uses the buyer's locale by default.
*
* @param options.inExtensionLocale - if true, use the extension's locale
*/
formatNumber: (
number: number | bigint,
options?: {inExtensionLocale?: boolean} & Intl.NumberFormatOptions,
) => string;

/**
* Returns a localized currency value.
*
* This function behaves like the standard `Intl.NumberFormat()`
* with a style of `currency` applied. It uses the buyer's locale by default.
*
* @param options.inExtensionLocale - if true, use the extension's locale
*/
formatCurrency: (
number: number | bigint,
options?: {inExtensionLocale?: boolean} & Intl.NumberFormatOptions,
) => string;

/**
* Returns a localized date value.
*
* This function behaves like the standard `Intl.DateTimeFormatOptions()` and uses
* the buyer's locale by default. Formatting options can be passed in as
* options.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat0
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options
*
* @param options.inExtensionLocale - if true, use the extension's locale
*/
formatDate: (
date: Date,
options?: {inExtensionLocale?: boolean} & Intl.DateTimeFormatOptions,
) => string;

/**
* Returns translated content in the buyer's locale,
* as supported by the extension.
*
* - `options.count` is a special numeric value used in pluralization.
* - The other option keys and values are treated as replacements for interpolation.
* - If the replacements are all primitives, then `translate()` returns a single string.
* - If replacements contain UI components, then `translate()` returns an array of elements.
*/
translate: I18nTranslate;
}

export interface Language {
/**
* The BCP-47 language tag. It may contain a dash followed by an ISO 3166-1 alpha-2 region code.
Expand Down

0 comments on commit 4e71987

Please sign in to comment.