When adding a webview to extend the capabilities of Associate App, external services or APIs used in this webview may require authentication. For this scenario, two different integrations are necessary.
Important
Do not hard-code credentials in the code of a webview.
Retrieving external authentication information
The Associate App offers a mechanism to retrieve an external authentication token after the user authenticated in the app. For that, use the Associate App configuration api to set the configuration for post_login_callback_urls.
For an example, see how you can use this property to set up the callback URL for loyalty rewards integration and the associated identity provider here.
{
"external_integration": {
"post_login_callback_urls": [
{
"identity_provider": "external-integration",
"url": "https://integration.site/auth/callback"
},
{
"identity_provider": "customer-loyalty",
"url": "https://customer.loyalty.callback.url.com"
}
]
}
}The API configured here will be invoked after every successful login in Associate App with a payload containing a NewStore identity auth token and device identifier.
{
"device_id": "",
"identity": "Bearer {NEWSTORE-JWT-TOKEN}"
}In this API, first verify the NewStore token, then create a valid token for your third-party service. The endpoint is expected to return authentication identifier which will be handled as string and stored in the local storage of the app. NewStore does not care about the format, it can even be serialized data. An example implementation can be found here.
{
"external_identity": "{TOKEN-VALID-FOR-EXTERNAL-PROVIDER}"
}Note
It is recommended to set the token expiration time to 2 to 3 hours.
Using data in a webview component
After setting up a webview customization (Managing Associate App customizations ) for one of the offered locations, the given url will be loaded with (context-aware) data attached as hash. The data can be accessed using Javascript and we recommend having a fallback for local development:
function getContext() {
const hash = window.location.hash.slice(1)
if (!hash) {
// fallback for local development
return { contextProps: { formData: { email_optin: 'true' } } }
}
return JSON.parse(atob(decodeURIComponent(hash)))
}
const context = getContext()Every data schema passed into a webview contain three default properties:
{
associateId}: The ID of the associate currently using Associate App,{
storeId}: The ID of the store where the associate is currently operating in,{
cartId}: The ID of the current cart, if one was created.
The remaining schema of the data depends on the location. For customer profile extended attributes, the schema contains all extended attributes of the customer profile in contextProps.formData as key-value pairs.
{
"contextProps": {
"formData": {
"emailOptin": "true",
"favoriteColor": "Blue",
"associateId": "1db2a42d-e872-432a-a87f-ed0964efc8d2",
"storeId": "d4acdd7f-b6ef-4c4a-b229-250d60e745fb",
"cartId": "d816a750-bcdf-4858-bca4-adca283be8b3",
}
},
"auth": {
"accessToken": "newstore-platform-access-token"
},
"externalIdentities": [{
"identityProvider": "external-integration",
"identity": "secret-token-for-external-integration",
},
{
"identityProvider": "customer-loyalty",
"identity": "secret-token-for-customer-loyalty",
}],
}The More menu webview customization contains the following context properties.
{
contextProps: {
formData: {
storeInfo: {
label: string
physicalAddress: {
addressLine1: string
addressLine2?: string
province?: string
state?: string
zipCode?: string
city?: string
countryCode: string
latitude?: number
longitude?: number
}
shippingAddress?: {
addressLine1: string
addressLine2?: string
province?: string
state?: string
zipCode?: string
city?: string
countryCode: string
latitude?: number
longitude?: number
}
divisionName?: string
managerId?: string
imageUrl?: string
phoneNumber?: string
activeStatus: boolean,
supportedShippingMethods: Array<
| 'traditional_carrier'
| 'same_day_delivery'
| 'in_store_pick_up'
| 'in_store_handover'
>
giftWrapping: boolean
pricebook?: string
deliveryZipCodes: string[]
shippingProviderInfo: {
[key: string]: any
}
businessHours: Array<{
fromTime: string
toTime: string
weekday: number
earliestPickUp?: string
latestPickUp?: string
}>
timezone: string
taxId?: string
taxIncluded: boolean
queuePrioritization: Array<{
priority: number
shippingType:
| 'traditional_carrier'
| 'same_day_delivery'
| 'in_store_pick_up'
| 'DHL_PAKET'
displayPriorityType?: 'priority' | 'normal'
}>
catalog?: string
locale?: string
storeId: string
displayPriceUnitType: 'net' | 'gross'
}
userInfo: {
id: string
email: string
firstName: string
lastName: string
telephoneNumber: string
storeId: string
imageUrl: string
createdAt: Date
updatedAt: Date
isActive: boolean
printerLocationId: string | null
printerLocation?: {
uuid: string
name: string
}
}
associateId: string
storeId: string
cartId: string
}
}
}Webview customization examples
Webview: VanillaJS
<html lang="en">
<head>
<title>Test App</title>
<meta name="viewport" content="user-scalable=no, width=device-width">
<script type="application/javascript">
function getContext() {
const hash = window.location.hash.slice(1)
if (!hash) {
// fallback for local development
return { contextProps: { formData: { email_optin: 'true' } } }
}
return JSON.parse(atob(decodeURIComponent(hash)))
}
const context = getContext()
</script>
<style>
label,
input {
display: block;
}
</style>
</head>
<body>
<div id="extended-attributes"></div>
<script type="application/javascript">
if (context && context.contextProps) {
const translation = {
email_optin: 'Wants to receive emails from us'
}
Object.entries(context.contextProps.formData).map(function (entry) {
const key = entry[0];
const value = entry[1];
const label = document.createElement("label");
const input = document.createElement("input");
label.innerText = translation[key] || key
input.type = 'text'
input.name = key
input.value = value
label.append(input)
document.getElementById("extended-attributes").append(label);
});
}
</script>
</body>
</html>Webview: ReactJS
<html lang="en">
<head>
<title>React Example</title>
<meta name="viewport" content="user-scalable=no, width=device-width">
<script type="application/javascript">
function getContext() {
const hash = window.location.hash.slice(1)
if (!hash) {
// fallback for local development
return { contextProps: { formData: { loyalty_id: '1234567890' } } }
}
return JSON.parse(atob(decodeURIComponent(hash)))
}
const context = getContext()
</script>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<style>
label,
input {
display: block;
}
</style>
</head>
<body>
<div id="extended-attributes"></div>
<script type="text/babel">
if (context && context.contextProps) {
const domContainer = document.querySelector('#extended-attributes');
const root = ReactDOM.createRoot(domContainer);
const translation = {
loyalty_id: 'Loyalty ID'
}
const LabelledInput = function (props) {
return (
<label>
{props.name}
<input type="text" name={props.name} value={props.value} onChange={() => null} />
</label>
)
}
const Form = () => {
return Object.entries(context.contextProps.formData).map(([name, value]) => (
<LabelledInput key={name} name={translation[name] || name} value={value} />
))
}
root.render(<Form />);
}
</script>
</body>
</html>Webview: Using external APIs
<html lang="en">
<head>
<title>React Example</title>
<meta name="viewport" content="user-scalable=no, width=device-width">
<script type="application/javascript">
function getContext() {
const hash = window.location.hash.slice(1)
if (!hash) {
// fallback for local development
return { contextProps: { formData: { 'External Customer ID': '2' } } }
}
return JSON.parse(atob(decodeURIComponent(hash)))
}
const context = getContext()
</script>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@tanstack/react-query@4/build/umd/index.production.js"></script>
<script src="https://unpkg.com/axios@0.27.2/dist/axios.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<style>
label,
input {
display: block;
font-family: Tahoma, serif;
width: 100%;
}
label {
color: #aaaaaa;
margin-bottom: 1rem;
font-size: 0.8rem;
}
input {
font-size: 1rem;
border: 1px solid #aaa;
border-radius: 6px;
padding: 8px;
}
</style>
</head>
<body>
<div id="extended-attributes"></div>
<script type="text/babel">
if (context && context.contextProps) {
const domContainer = document.querySelector('#extended-attributes');
const root = ReactDOM.createRoot(domContainer);
const queryClient = new window.ReactQuery.QueryClient();
const QueryClientProvider = window.ReactQuery.QueryClientProvider;
const useQuery = window.ReactQuery.useQuery;
const external_user_id = context.contextProps.formData["External Customer ID"];