add order details page

This commit is contained in:
tedraykov
2024-06-20 13:23:02 +03:00
parent 3694fef9a6
commit 8749b8aaec
52 changed files with 2000 additions and 724 deletions

View File

@@ -1,12 +1,59 @@
//you need to remain this as type so as not to confuse with the actual function
import type { NextRequest, NextResponse as NextResponseType } from 'next/server';
import { cookies } from 'next/headers';
import { getNonce } from 'lib/shopify/customer/auth-utils';
import {
SHOPIFY_CUSTOMER_ACCOUNT_API_URL,
SHOPIFY_USER_AGENT,
SHOPIFY_CLIENT_ID
} from './constants';
import { NextRequest, NextResponse } from 'next/server';
export const CUSTOMER_API_URL = process.env.SHOPIFY_CUSTOMER_ACCOUNT_API_URL!;
export const CUSTOMER_API_CLIENT_ID = process.env.SHOPIFY_CUSTOMER_ACCOUNT_API_CLIENT_ID || '';
export const ORIGIN_URL = process.env.SHOPIFY_ORIGIN_URL || '';
export const USER_AGENT = '*';
export async function generateCodeVerifier() {
const randomCode = generateRandomCode();
return base64UrlEncode(randomCode);
}
export async function generateCodeChallenge(codeVerifier: string) {
const digestOp = await crypto.subtle.digest(
{ name: 'SHA-256' },
new TextEncoder().encode(codeVerifier)
);
const hash = convertBufferToString(digestOp);
return base64UrlEncode(hash);
}
function generateRandomCode() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return String.fromCharCode.apply(null, Array.from(array));
}
function base64UrlEncode(str: string) {
const base64 = btoa(str);
// This is to ensure that the encoding does not have +, /, or = characters in it.
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function convertBufferToString(hash: ArrayBuffer) {
const uintArray = new Uint8Array(hash);
const numberArray = Array.from(uintArray);
return String.fromCharCode(...numberArray);
}
export async function generateRandomString() {
const timestamp = Date.now().toString();
const randomString = Math.random().toString(36).substring(2);
return timestamp + randomString;
}
export async function getNonce(token: string) {
const [header, payload, signature] = token.split('.');
const decodedHeader = JSON.parse(atob(header || ''));
const decodedPayload = JSON.parse(atob(payload || ''));
return {
header: decodedHeader,
payload: decodedPayload,
signature
};
}
export async function initialAccessToken(
request: NextRequest,
@@ -58,29 +105,30 @@ export async function initialAccessToken(
headersNew.append('User-Agent', userAgent);
headersNew.append('Origin', newOrigin || '');
const tokenRequestUrl = `${customerAccountApiUrl}/auth/oauth/token`;
console.log('sending request to', tokenRequestUrl);
const response = await fetch(tokenRequestUrl, {
method: 'POST',
headers: headersNew,
body
});
const data = await response.json();
console.log('data initial access token', data);
console.log('ok', response.ok);
if (!response.ok) {
console.log('data response error auth', data.error);
const error = await response.text();
console.log('data response error auth', error);
console.log('response auth', response.status);
return { success: false, message: `Response error auth` };
}
const data = await response.json();
if (data?.errors) {
const errorMessage = data?.errors?.[0]?.message ?? 'Unknown error auth';
return { success: false, message: `${errorMessage}` };
}
const nonce = await getNonce(data?.id_token || '');
const nonceValue = nonce.payload.nonce;
const shopNonce = request.cookies.get('shop_nonce');
const shopNonceValue = shopNonce?.value;
console.log('sent nonce', nonce);
console.log('original nonce', shopNonceValue);
if (nonce !== shopNonceValue) {
//make equal === to force error for testing
if (nonceValue !== shopNonceValue) {
console.log('Error nonce match');
return { success: false, message: `Error: Nonce mismatch` };
}
@@ -134,18 +182,16 @@ export async function refreshToken({ request, origin }: { request: NextRequest;
console.log('Error: No Refresh Token');
return { success: false, message: `no_refresh_token` };
}
const customerAccountApiUrl = SHOPIFY_CUSTOMER_ACCOUNT_API_URL;
const clientId = SHOPIFY_CLIENT_ID;
const userAgent = SHOPIFY_USER_AGENT;
const clientId = CUSTOMER_API_CLIENT_ID;
newBody.append('grant_type', 'refresh_token');
newBody.append('refresh_token', refreshTokenValue);
newBody.append('client_id', clientId);
const headers = {
'content-type': 'application/x-www-form-urlencoded',
'User-Agent': userAgent,
'User-Agent': USER_AGENT,
Origin: origin
};
const tokenRequestUrl = `${customerAccountApiUrl}/auth/oauth/token`;
const tokenRequestUrl = `${CUSTOMER_API_URL}/auth/oauth/token`;
const response = await fetch(tokenRequestUrl, {
method: 'POST',
headers,
@@ -164,7 +210,7 @@ export async function refreshToken({ request, origin }: { request: NextRequest;
const customerAccessToken = await exchangeAccessToken(
access_token,
clientId,
customerAccountApiUrl,
CUSTOMER_API_URL,
origin
);
// console.log("Customer Access Token in refresh request", customerAccessToken)
@@ -203,7 +249,7 @@ export async function checkExpires({
return { ranRefresh: isExpired, success: true };
}
export function removeAllCookies(response: NextResponseType) {
export function removeAllCookies(response: NextResponse) {
//response.cookies.delete('shop_auth_token') //never set. We don't use it anywhere.
response.cookies.delete('shop_customer_token');
response.cookies.delete('shop_refresh_token');
@@ -234,7 +280,7 @@ export async function createAllCookies({
expiresAt,
id_token
}: {
response: NextResponseType;
response: NextResponse;
customerAccessToken: string;
expires_in: number;
refresh_token: string;
@@ -284,3 +330,181 @@ export async function createAllCookies({
return response;
}
export async function isLoggedIn(request: NextRequest, origin: string) {
const customerToken = request.cookies.get('shop_customer_token');
const customerTokenValue = customerToken?.value;
const refreshToken = request.cookies.get('shop_refresh_token');
const refreshTokenValue = refreshToken?.value;
console.log('customer token', customerTokenValue);
const newHeaders = new Headers(request.headers);
if (!customerTokenValue && !refreshTokenValue) {
const redirectUrl = new URL(`${origin}`);
const response = NextResponse.redirect(`${redirectUrl}`);
return removeAllCookies(response);
}
const expiresToken = request.cookies.get('shop_expires_at');
const expiresTokenValue = expiresToken?.value;
if (!expiresTokenValue) {
const redirectUrl = new URL(`${origin}`);
const response = NextResponse.redirect(`${redirectUrl}`);
return removeAllCookies(response);
//return { success: false, message: `no_expires_at` }
}
const isExpired = await checkExpires({
request: request,
expiresAt: expiresTokenValue,
origin: origin
});
console.log('is Expired?', isExpired);
//only execute the code below to reset the cookies if it was expired!
if (isExpired.ranRefresh) {
const isSuccess = isExpired?.refresh?.success;
if (!isSuccess) {
const redirectUrl = new URL(`${origin}`);
const response = NextResponse.redirect(`${redirectUrl}`);
return removeAllCookies(response);
//return { success: false, message: `no_refresh_token` }
} else {
const refreshData = isExpired?.refresh?.data;
//console.log ("refresh data", refreshData)
console.log('We used the refresh token, so now going to reset the token and cookies');
const newCustomerAccessToken = refreshData?.customerAccessToken;
const expires_in = refreshData?.expires_in;
//const test_expires_in = 180 //to test to see if it expires in 60 seconds!
const expiresAt = new Date(new Date().getTime() + (expires_in! - 120) * 1000).getTime() + '';
newHeaders.set('x-shop-customer-token', `${newCustomerAccessToken}`);
const resetCookieResponse = NextResponse.next({
request: {
// New request headers
headers: newHeaders
}
});
return await createAllCookies({
response: resetCookieResponse,
customerAccessToken: newCustomerAccessToken,
expires_in,
refresh_token: refreshData?.refresh_token,
expiresAt
});
}
}
newHeaders.set('x-shop-customer-token', `${customerTokenValue}`);
return NextResponse.next({
request: {
// New request headers
headers: newHeaders
}
});
}
//when we are running on the production website we just get the origin from the request.nextUrl
export function getOrigin(request: NextRequest) {
const nextOrigin = request.nextUrl.origin;
console.log('Current Origin', nextOrigin);
//when running localhost, we want to use fake origin otherwise we use the real origin
let newOrigin = nextOrigin;
if (nextOrigin === 'https://localhost:3000' || nextOrigin === 'http://localhost:3000') {
newOrigin = ORIGIN_URL;
} else {
newOrigin = nextOrigin;
}
console.log('New Origin', newOrigin);
return newOrigin;
}
export async function authorize(request: NextRequest, origin: string) {
const clientId = CUSTOMER_API_CLIENT_ID;
const newHeaders = new Headers(request.headers);
/***
STEP 1: Get the initial access token or deny access
****/
const dataInitialToken = await initialAccessToken(request, origin, CUSTOMER_API_URL, clientId);
console.log('data initial token', dataInitialToken);
if (!dataInitialToken.success) {
console.log('Error: Access Denied. Check logs', dataInitialToken.message);
newHeaders.set('x-shop-access', 'denied');
return NextResponse.next({
request: {
// New request headers
headers: newHeaders
}
});
}
const { access_token, expires_in, id_token, refresh_token } = dataInitialToken.data;
/***
STEP 2: Get a Customer Access Token
****/
const customerAccessToken = await exchangeAccessToken(
access_token,
clientId,
CUSTOMER_API_URL,
origin || ''
);
console.log('customer access token', customerAccessToken);
if (!customerAccessToken.success) {
console.log('Error: Customer Access Token');
newHeaders.set('x-shop-access', 'denied');
return NextResponse.next({
request: {
// New request headers
headers: newHeaders
}
});
}
//console.log("customer access Token", customerAccessToken.data.access_token)
/**STEP 3: Set Customer Access Token cookies
We are setting the cookies here b/c if we set it on the request, and then redirect
it doesn't see to set sometimes
**/
newHeaders.set('x-shop-access', 'allowed');
/*
const authResponse = NextResponse.next({
request: {
// New request headers
headers: newHeaders,
},
})
*/
const accountUrl = new URL(`${origin}/account`);
const authResponse = NextResponse.redirect(`${accountUrl}`);
//sets an expires time 2 minutes before expiration which we can use in refresh strategy
//const test_expires_in = 180 //to test to see if it expires in 60 seconds!
const expiresAt = new Date(new Date().getTime() + (expires_in! - 120) * 1000).getTime() + '';
console.log('expires at', expiresAt);
return await createAllCookies({
response: authResponse,
customerAccessToken: customerAccessToken?.data?.access_token,
expires_in,
refresh_token,
expiresAt,
id_token
});
}
export async function logout(request: NextRequest, origin: string) {
//console.log("New Origin", newOrigin)
const idToken = request.cookies.get('shop_id_token');
const idTokenValue = idToken?.value;
//revalidateTag(TAGS.customer); //this causes some strange error in Nextjs about invariant, so removing for now
//if there is no idToken, then sending to logout url will redirect shopify, so just
//redirect to login here and delete cookies (presumably they don't even exist)
if (!idTokenValue) {
const logoutUrl = new URL(`${origin}/login`);
const response = NextResponse.redirect(`${logoutUrl}`);
return removeAllCookies(response);
}
//console.log ("id toke value", idTokenValue)
const logoutUrl = new URL(
`${CUSTOMER_API_URL}/auth/logout?id_token_hint=${idTokenValue}&post_logout_redirect_uri=${origin}`
);
//console.log ("logout url", logoutUrl)
const logoutResponse = NextResponse.redirect(logoutUrl);
return removeAllCookies(logoutResponse);
}

View File

@@ -1,48 +0,0 @@
// @ts-nocheck
export async function generateCodeVerifier() {
const randomCode = generateRandomCode();
return base64UrlEncode(randomCode);
}
export async function generateCodeChallenge(codeVerifier: string) {
const digestOp = await crypto.subtle.digest(
{ name: 'SHA-256' },
new TextEncoder().encode(codeVerifier)
);
const hash = convertBufferToString(digestOp);
return base64UrlEncode(hash);
}
function generateRandomCode() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return String.fromCharCode.apply(null, Array.from(array));
}
function base64UrlEncode(str: string) {
const base64 = btoa(str);
// This is to ensure that the encoding does not have +, /, or = characters in it.
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function convertBufferToString(hash: ArrayBuffer) {
const uintArray = new Uint8Array(hash);
const numberArray = Array.from(uintArray);
return String.fromCharCode(...numberArray);
}
export async function generateRandomString() {
const timestamp = Date.now().toString();
const randomString = Math.random().toString(36).substring(2);
return timestamp + randomString;
}
export async function getNonce(token: string) {
return decodeJwt(token).payload.nonce;
}
function decodeJwt(token: string) {
const [header, payload, signature] = token.split('.');
const decodedHeader = JSON.parse(atob(header || ''));
const decodedPayload = JSON.parse(atob(payload || ''));
return {
header: decodedHeader,
payload: decodedPayload,
signature
};
}

View File

@@ -1,10 +0,0 @@
export const TAGS = {
customer: 'customer'
};
//ENVs
export const SHOPIFY_CUSTOMER_ACCOUNT_API_URL = process.env.SHOPIFY_CUSTOMER_ACCOUNT_API_URL || '';
export const SHOPIFY_CLIENT_ID = process.env.SHOPIFY_CUSTOMER_ACCOUNT_API_CLIENT_ID || '';
export const SHOPIFY_CUSTOMER_API_VERSION = process.env.SHOPIFY_CUSTOMER_API_VERSION || '';
export const SHOPIFY_USER_AGENT = '*';
export const SHOPIFY_ORIGIN = process.env.SHOPIFY_ORIGIN_URL || '';

View File

@@ -1,285 +0,0 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import {
checkExpires,
removeAllCookies,
initialAccessToken,
exchangeAccessToken,
createAllCookies
} from './auth-helpers';
import { isShopifyError } from 'lib/type-guards';
import { parseJSON } from 'lib/shopify/customer/utils/parse-json';
import {
SHOPIFY_CUSTOMER_ACCOUNT_API_URL,
SHOPIFY_USER_AGENT,
SHOPIFY_CUSTOMER_API_VERSION,
SHOPIFY_CLIENT_ID,
SHOPIFY_ORIGIN
} from './constants';
type ExtractVariables<T> = T extends { variables: object } ? T['variables'] : never;
const customerAccountApiUrl = SHOPIFY_CUSTOMER_ACCOUNT_API_URL;
const apiVersion = SHOPIFY_CUSTOMER_API_VERSION;
const userAgent = SHOPIFY_USER_AGENT;
const customerEndpoint = `${customerAccountApiUrl}/account/customer/api/${apiVersion}/graphql`;
//NEVER CACHE THIS! Doesn't see to be cached anyway b/c
//https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#opting-out-of-data-caching
//The fetch request comes after the usage of headers or cookies.
//and we always send this anyway after getting a cookie for the customer
export async function shopifyCustomerFetch<T>({
customerToken,
query,
tags,
variables
}: {
cache?: RequestCache;
customerToken: string;
query: string;
tags?: string[];
variables?: ExtractVariables<T>;
}): Promise<{ status: number; body: T } | never> {
try {
const customerOrigin = SHOPIFY_ORIGIN;
const result = await fetch(customerEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': userAgent,
Origin: customerOrigin,
Authorization: customerToken
},
body: JSON.stringify({
...(query && { query }),
...(variables && { variables })
}),
cache: 'no-store',
...(tags && { next: { tags } })
});
const body = await result.json();
if (!result.ok) {
//the statuses here could be different, a 401 means
//https://shopify.dev/docs/api/customer#endpoints
//401 means the token is bad
console.log('Error in Customer Fetch Status', body.errors);
if (result.status === 401) {
// clear session because current access token is invalid
const errorMessage = 'unauthorized';
throw errorMessage; //this should throw in the catch below in the non-shopify catch
}
let errors;
try {
errors = parseJSON(body);
} catch (_e) {
errors = [{ message: body }];
}
throw errors;
}
//this just throws an error and the error boundary is called
if (body.errors) {
//throw 'Error'
console.log('Error in Customer Fetch', body.errors[0]);
throw body.errors[0];
}
return {
status: result.status,
body
};
} catch (e) {
if (isShopifyError(e)) {
throw {
cause: e.cause?.toString() || 'unknown',
status: e.status || 500,
message: e.message,
query
};
}
throw {
error: e,
query
};
}
}
export async function isLoggedIn(request: NextRequest, origin: string) {
const customerToken = request.cookies.get('shop_customer_token');
const customerTokenValue = customerToken?.value;
const refreshToken = request.cookies.get('shop_refresh_token');
const refreshTokenValue = refreshToken?.value;
const newHeaders = new Headers(request.headers);
if (!customerTokenValue && !refreshTokenValue) {
const redirectUrl = new URL(`${origin}`);
const response = NextResponse.redirect(`${redirectUrl}`);
return removeAllCookies(response);
}
const expiresToken = request.cookies.get('shop_expires_at');
const expiresTokenValue = expiresToken?.value;
if (!expiresTokenValue) {
const redirectUrl = new URL(`${origin}`);
const response = NextResponse.redirect(`${redirectUrl}`);
return removeAllCookies(response);
//return { success: false, message: `no_expires_at` }
}
const isExpired = await checkExpires({
request: request,
expiresAt: expiresTokenValue,
origin: origin
});
console.log('is Expired?', isExpired);
//only execute the code below to reset the cookies if it was expired!
if (isExpired.ranRefresh) {
const isSuccess = isExpired?.refresh?.success;
if (!isSuccess) {
const redirectUrl = new URL(`${origin}`);
const response = NextResponse.redirect(`${redirectUrl}`);
return removeAllCookies(response);
//return { success: false, message: `no_refresh_token` }
} else {
const refreshData = isExpired?.refresh?.data;
//console.log ("refresh data", refreshData)
console.log('We used the refresh token, so now going to reset the token and cookies');
const newCustomerAccessToken = refreshData?.customerAccessToken;
const expires_in = refreshData?.expires_in;
//const test_expires_in = 180 //to test to see if it expires in 60 seconds!
const expiresAt = new Date(new Date().getTime() + (expires_in! - 120) * 1000).getTime() + '';
newHeaders.set('x-shop-customer-token', `${newCustomerAccessToken}`);
const resetCookieResponse = NextResponse.next({
request: {
// New request headers
headers: newHeaders
}
});
return await createAllCookies({
response: resetCookieResponse,
customerAccessToken: newCustomerAccessToken,
expires_in,
refresh_token: refreshData?.refresh_token,
expiresAt
});
}
}
newHeaders.set('x-shop-customer-token', `${customerTokenValue}`);
return NextResponse.next({
request: {
// New request headers
headers: newHeaders
}
});
}
//when we are running on the production website we just get the origin from the request.nextUrl
export function getOrigin(request: NextRequest) {
const nextOrigin = request.nextUrl.origin;
//console.log("Current Origin", nextOrigin)
//when running localhost, we want to use fake origin otherwise we use the real origin
let newOrigin = nextOrigin;
if (nextOrigin === 'https://localhost:3000' || nextOrigin === 'http://localhost:3000') {
newOrigin = SHOPIFY_ORIGIN;
} else {
newOrigin = nextOrigin;
}
return newOrigin;
}
export async function authorizeFn(request: NextRequest, origin: string) {
const clientId = SHOPIFY_CLIENT_ID;
const newHeaders = new Headers(request.headers);
/***
STEP 1: Get the initial access token or deny access
****/
const dataInitialToken = await initialAccessToken(
request,
origin,
customerAccountApiUrl,
clientId
);
if (!dataInitialToken.success) {
console.log('Error: Access Denied. Check logs', dataInitialToken.message);
newHeaders.set('x-shop-access', 'denied');
return NextResponse.next({
request: {
// New request headers
headers: newHeaders
}
});
}
const { access_token, expires_in, id_token, refresh_token } = dataInitialToken.data;
/***
STEP 2: Get a Customer Access Token
****/
const customerAccessToken = await exchangeAccessToken(
access_token,
clientId,
customerAccountApiUrl,
origin || ''
);
if (!customerAccessToken.success) {
console.log('Error: Customer Access Token');
newHeaders.set('x-shop-access', 'denied');
return NextResponse.next({
request: {
// New request headers
headers: newHeaders
}
});
}
//console.log("customer access Token", customerAccessToken.data.access_token)
/**STEP 3: Set Customer Access Token cookies
We are setting the cookies here b/c if we set it on the request, and then redirect
it doesn't see to set sometimes
**/
newHeaders.set('x-shop-access', 'allowed');
/*
const authResponse = NextResponse.next({
request: {
// New request headers
headers: newHeaders,
},
})
*/
const accountUrl = new URL(`${origin}/account`);
const authResponse = NextResponse.redirect(`${accountUrl}`);
//sets an expires time 2 minutes before expiration which we can use in refresh strategy
//const test_expires_in = 180 //to test to see if it expires in 60 seconds!
const expiresAt = new Date(new Date().getTime() + (expires_in! - 120) * 1000).getTime() + '';
return await createAllCookies({
response: authResponse,
customerAccessToken: customerAccessToken?.data?.access_token,
expires_in,
refresh_token,
expiresAt,
id_token
});
}
export async function logoutFn(request: NextRequest, origin: string) {
//console.log("New Origin", newOrigin)
const idToken = request.cookies.get('shop_id_token');
const idTokenValue = idToken?.value;
//revalidateTag(TAGS.customer); //this causes some strange error in Nextjs about invariant, so removing for now
//if there is no idToken, then sending to logout url will redirect shopify, so just
//redirect to login here and delete cookies (presumably they don't even exist)
if (!idTokenValue) {
const logoutUrl = new URL(`${origin}/login`);
const response = NextResponse.redirect(`${logoutUrl}`);
return removeAllCookies(response);
}
//console.log ("id toke value", idTokenValue)
const logoutUrl = new URL(
`${customerAccountApiUrl}/auth/logout?id_token_hint=${idTokenValue}&post_logout_redirect_uri=${origin}`
);
//console.log ("logout url", logoutUrl)
const logoutResponse = NextResponse.redirect(logoutUrl);
return removeAllCookies(logoutResponse);
}

View File

@@ -1,97 +0,0 @@
//https://shopify.dev/docs/api/customer/2024-01/queries/customer
export const CUSTOMER_ME_QUERY = /* GraphQL */ `
query customer {
customer {
emailAddress {
emailAddress
}
firstName
lastName
tags
}
}
`;
const CUSTOMER_FRAGMENT = `#graphql
fragment OrderCard on Order {
id
number
processedAt
financialStatus
fulfillments(first: 1) {
nodes {
status
}
}
totalPrice {
amount
currencyCode
}
lineItems(first: 2) {
edges {
node {
title
image {
altText
height
url
width
}
}
}
}
}
fragment AddressPartial on CustomerAddress {
id
formatted
firstName
lastName
company
address1
address2
territoryCode
zoneCode
city
zip
phoneNumber
}
fragment CustomerDetails on Customer {
firstName
lastName
phoneNumber {
phoneNumber
}
emailAddress {
emailAddress
}
defaultAddress {
...AddressPartial
}
addresses(first: 6) {
edges {
node {
...AddressPartial
}
}
}
orders(first: 250, sortKey: PROCESSED_AT, reverse: true) {
edges {
node {
...OrderCard
}
}
}
}
` as const;
// NOTE: https://shopify.dev/docs/api/customer/latest/queries/customer
export const CUSTOMER_DETAILS_QUERY = `#graphql
query CustomerDetails {
customer {
...CustomerDetails
}
}
${CUSTOMER_FRAGMENT}
` as const;

View File

@@ -1,36 +0,0 @@
export type Maybe<T> = T | null;
export type Connection<T> = {
edges: Array<Edge<T>>;
};
export type Edge<T> = {
node: T;
};
export type CustomerData = {
data: {
customer: {
emailAddress: {
emailAddress: string;
};
firstName: string;
lastName: string;
tags: any[];
};
};
};
export type GenericObject = { [key: string]: any };
export type CustomerDetailsData = {
data: {
customer: {
emailAddress: {
emailAddress: string;
};
// Using GenericObject to type 'orders' since the fields are not known in advance
orders: Connection<GenericObject>;
};
};
};

View File

@@ -1,7 +0,0 @@
export function parseJSON(json: any) {
if (String(json).includes('__proto__')) return JSON.parse(json, noproto);
return JSON.parse(json);
}
function noproto(k: string, v: string) {
if (k !== '__proto__') return v;
}

View File

@@ -0,0 +1,18 @@
const customerAddress = /* GraphQL */ `
fragment CustomerAddress on CustomerAddress {
id
formatted
firstName
lastName
company
address1
address2
territoryCode
zoneCode
city
zip
phoneNumber
}
`;
export default customerAddress;

View File

@@ -0,0 +1,36 @@
import customerAddress from './customer-address';
import orderCard from './order-card';
const customerDetailsFragment = /* GraphQL */ `
${customerAddress}
${orderCard}
fragment CustomerDetails on Customer {
firstName
lastName
phoneNumber {
phoneNumber
}
emailAddress {
emailAddress
}
defaultAddress {
...CustomerAddress
}
addresses(first: 6) {
edges {
node {
...CustomerAddress
}
}
}
orders(first: 20, sortKey: PROCESSED_AT, reverse: true) {
edges {
node {
...OrderCard
}
}
}
}
`;
export default customerDetailsFragment;

View File

@@ -0,0 +1,35 @@
const orderCard = /* GraphQL */ `
fragment OrderCard on Order {
id
number
name
processedAt
financialStatus
fulfillments(first: 1) {
edges {
node {
status
}
}
}
totalPrice {
amount
currencyCode
}
lineItems(first: 20) {
edges {
node {
title
image {
altText
height
url
width
}
}
}
}
}
`;
export default orderCard;

View File

@@ -11,7 +11,7 @@ import {
YEAR_FILTER_ID
} from 'lib/constants';
import { isShopifyError } from 'lib/type-guards';
import { ensureStartsWith, normalizeUrl, parseMetaFieldValue } from 'lib/utils';
import { ensureStartsWith, normalizeUrl, parseJSON, parseMetaFieldValue } from 'lib/shopify/utils';
import { revalidatePath, revalidateTag } from 'next/cache';
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
@@ -38,16 +38,21 @@ import {
getProductsQuery
} from './queries/product';
import {
Address,
Cart,
CartAttributeInput,
CartItem,
Collection,
Connection,
Customer,
Filter,
Image,
Menu,
Metaobject,
Money,
Order,
Fulfillment,
Transaction,
Page,
PageInfo,
Product,
@@ -60,6 +65,9 @@ import {
ShopifyCollectionProductsOperation,
ShopifyCollectionsOperation,
ShopifyCreateCartOperation,
ShopifyCustomerOperation,
ShopifyCustomerOrderOperation,
ShopifyCustomerOrdersOperation,
ShopifyFilter,
ShopifyImageOperation,
ShopifyMenuOperation,
@@ -75,13 +83,31 @@ import {
ShopifyProductsOperation,
ShopifyRemoveFromCartOperation,
ShopifySetCartAttributesOperation,
ShopifyUpdateCartOperation
ShopifyUpdateCartOperation,
ShopifyCustomer,
ShopifyOrder,
ShopifyAddress,
ShopifyMoneyV2,
LineItem
} from './types';
import { getCustomerQuery } from './queries/customer';
import { getCustomerOrdersQuery } from './queries/orders';
import { getCustomerOrderQuery } from './queries/order';
const domain = process.env.SHOPIFY_STORE_DOMAIN
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
: '';
const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
const customerApiUrl = process.env.SHOPIFY_CUSTOMER_ACCOUNT_API_URL;
const customerApiVersion = process.env.SHOPIFY_CUSTOMER_API_VERSION;
const storefrontEndpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
const customerEndpoint = `${customerApiUrl}/account/customer/api/${customerApiVersion}/graphql`;
const userAgent = '*';
const placeholderProductImage =
'https://cdn.shopify.com/shopifycloud/customer-account-web/production/assets/8bc6556601c510713d76.svg';
const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
type ExtractVariables<T> = T extends { variables: object } ? T['variables'] : never;
@@ -100,7 +126,7 @@ export async function shopifyFetch<T>({
variables?: ExtractVariables<T>;
}): Promise<{ status: number; body: T } | never> {
try {
const result = await fetch(endpoint, {
const result = await fetch(storefrontEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -142,6 +168,80 @@ export async function shopifyFetch<T>({
}
}
export async function shopifyCustomerFetch<T>({
query,
variables
}: {
query: string;
variables?: ExtractVariables<T>;
}): Promise<{ status: number; body: T } | never> {
const headersList = headers();
const customerToken = headersList.get('x-shop-customer-token') || '';
try {
const result = await fetch(customerEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': userAgent,
Origin: domain,
Authorization: customerToken
},
body: JSON.stringify({
...(query && { query }),
...(variables && { variables })
}),
cache: 'no-store'
});
const body = await result.json();
if (!result.ok) {
//the statuses here could be different, a 401 means
//https://shopify.dev/docs/api/customer#endpoints
//401 means the token is bad
console.log('Error in Customer Fetch Status', body.errors);
if (result.status === 401) {
// clear session because current access token is invalid
const errorMessage = 'unauthorized';
throw errorMessage; //this should throw in the catch below in the non-shopify catch
}
let errors;
try {
errors = parseJSON(body);
} catch (_e) {
errors = [{ message: body }];
}
throw errors;
}
//this just throws an error and the error boundary is called
if (body.errors) {
//throw 'Error'
console.log('Error in Customer Fetch', body.errors[0]);
throw body.errors[0];
}
return {
status: result.status,
body
};
} catch (e) {
if (isShopifyError(e)) {
throw {
cause: e.cause?.toString() || 'unknown',
status: e.status || 500,
message: e.message,
query
};
}
throw {
error: e,
query
};
}
}
const removeEdgesAndNodes = (array: Connection<any>) => {
return array.edges.map((edge) => edge?.node);
};
@@ -316,6 +416,143 @@ const reshapeProducts = (products: ShopifyProduct[]) => {
return reshapedProducts;
};
function reshapeCustomer(customer: ShopifyCustomer): Customer {
return {
firstName: customer.firstName,
lastName: customer.lastName,
displayName: customer.displayName,
emailAddress: customer.emailAddress.emailAddress
};
}
function reshapeOrders(orders: ShopifyOrder[]): any[] | Promise<Order[]> {
const reshapedOrders: Order[] = [];
for (const order of orders) {
const reshapedOrder = reshapeOrder(order);
if (!reshapedOrder) continue;
reshapedOrders.push(reshapedOrder);
}
return reshapedOrders;
}
function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
const reshapeAddress = (address?: ShopifyAddress): Address | undefined => {
if (!address) return undefined;
return {
address1: address.address1,
address2: address.address2,
firstName: address.firstName,
lastName: address.lastName,
provinceCode: address.provinceCode,
city: address.city,
zip: address.zip,
country: address.countryCodeV2,
company: address.company,
phone: address.phone
};
};
const reshapeMoney = (money?: ShopifyMoneyV2): Money | undefined => {
if (!money) return undefined;
return {
amount: money.amount || '0.00',
currencyCode: money.currencyCode || 'USD'
};
};
const orderFulfillments: Fulfillment[] =
shopifyOrder.fulfillments?.edges?.map((edge) => ({
status: edge.node.status,
createdAt: edge.node.createdAt,
trackingInformation:
edge.node.trackingInformation?.map((tracking) => ({
number: tracking.number,
company: tracking.company,
url: tracking.url
})) || [],
events:
edge.node.events?.edges.map((event) => ({
status: event.node.status,
happenedAt: event.node.happenedAt
})) || [],
fulfilledLineItems:
edge.node.fulfillmentLineItems?.nodes.map((lineItem) => ({
id: lineItem.lineItem.id,
quantity: lineItem.quantity,
image: {
url: lineItem.lineItem.image?.url || placeholderProductImage,
altText: lineItem.lineItem.image?.altText || lineItem.lineItem.title,
width: 100,
height: 100
}
})) || []
})) || [];
const orderTransactions: Transaction[] = shopifyOrder.transactions?.map((transaction) => ({
processedAt: transaction.processedAt,
paymentIcon: {
url: transaction.paymentIcon.url,
altText: transaction.paymentIcon.altText,
width: 100,
height: 100
},
paymentDetails: {
last4: transaction.paymentDetails.last4,
cardBrand: transaction.paymentDetails.cardBrand
},
transactionAmount: reshapeMoney(transaction.transactionAmount.presentmentMoney)!
}));
const orderLineItems: LineItem[] =
shopifyOrder.lineItems?.edges.map((edge) => ({
id: edge.node.id,
title: edge.node.title,
quantity: edge.node.quantity,
image: {
url: edge.node.image?.url || placeholderProductImage,
altText: edge.node.image?.altText || edge.node.title,
width: edge.node.image?.width || 62,
height: edge.node.image?.height || 62
},
price: reshapeMoney(edge.node.price),
totalPrice: reshapeMoney(edge.node.totalPrice),
variantTitle: edge.node.variantTitle,
sku: edge.node.sku
})) || [];
const order: Order = {
id: shopifyOrder.id.replace('gid://shopify/Order/', ''),
name: shopifyOrder.name,
processedAt: shopifyOrder.processedAt,
fulfillments: orderFulfillments,
transactions: orderTransactions,
lineItems: orderLineItems,
shippingAddress: reshapeAddress(shopifyOrder.shippingAddress),
billingAddress: reshapeAddress(shopifyOrder.billingAddress),
subtotal: reshapeMoney(shopifyOrder.subtotal),
totalShipping: reshapeMoney(shopifyOrder.totalShipping),
totalTax: reshapeMoney(shopifyOrder.totalTax),
totalPrice: reshapeMoney(shopifyOrder.totalPrice)
};
if (shopifyOrder.customer) {
order.customer = reshapeCustomer(shopifyOrder.customer);
}
if (shopifyOrder.shippingLine) {
console.log('Shipping Line', shopifyOrder.shippingLine);
order.shippingMethod = {
name: shopifyOrder.shippingLine?.title,
price: reshapeMoney(shopifyOrder.shippingLine.originalPrice)!
};
}
return order;
}
export async function createCart(): Promise<Cart> {
const res = await shopifyFetch<ShopifyCreateCartOperation>({
query: createCartMutation,
@@ -650,6 +887,33 @@ export async function getProducts({
pageInfo
};
}
export async function getCustomer(): Promise<Customer> {
const res = await shopifyCustomerFetch<ShopifyCustomerOperation>({
query: getCustomerQuery
});
const customer = res.body.data.customer;
return reshapeCustomer(customer);
}
export async function getCustomerOrders(): Promise<Order[]> {
const res = await shopifyCustomerFetch<ShopifyCustomerOrdersOperation>({
query: getCustomerOrdersQuery
});
return reshapeOrders(removeEdgesAndNodes(res.body.data.customer.orders));
}
export async function getCustomerOrder(orderId: string): Promise<Order> {
const res = await shopifyCustomerFetch<ShopifyCustomerOrderOperation>({
query: getCustomerOrderQuery,
variables: { orderId: `gid://shopify/Order/${orderId}` }
});
return reshapeOrder(res.body.data.order);
}
// This is called from `app/api/revalidate.ts` so providers can control revalidation logic.
export async function revalidate(req: NextRequest): Promise<NextResponse> {
console.log(`Receiving revalidation request from Shopify.`);

View File

@@ -0,0 +1,13 @@
//https://shopify.dev/docs/api/customer/2024-01/queries/customer
export const getCustomerQuery = /* GraphQL */ `
query customer {
customer {
emailAddress {
emailAddress
}
firstName
lastName
tags
}
}
`;

View File

@@ -0,0 +1,240 @@
// NOTE: https://shopify.dev/docs/api/customer/latest/queries/customer
export const getCustomerOrderQuery = /* GraphQL */ `
query getCustomerOrderQuery($orderId: ID!) {
customer {
emailAddress {
emailAddress
}
displayName
}
order(id: $orderId) {
... on Order {
id
...Order
customer {
id
emailAddress {
emailAddress
marketingState
}
firstName
lastName
phoneNumber {
phoneNumber
marketingState
}
imageUrl
displayName
}
}
}
}
fragment Order on Order {
id
name
confirmationNumber
processedAt
cancelledAt
currencyCode
transactions {
...OrderTransaction
}
billingAddress {
...Address
}
shippingAddress {
...Address
}
fulfillments(first: 20, sortKey: CREATED_AT, reverse: true, query: "NOT status:CANCELLED") {
edges {
node {
id
...Fulfillment
}
}
}
lineItems(first: 50) {
edges {
node {
id
...LineItem
}
}
}
totalPrice {
...Price
}
subtotal {
...Price
}
totalShipping {
...Price
}
totalTax {
...Price
}
financialStatus
totalRefunded {
...Price
}
refunds {
id
createdAt
}
paymentInformation {
paymentCollectionUrl
...OrderPaymentInformation
}
requiresShipping
note
shippingLine {
title
originalPrice {
...Price
}
}
}
fragment OrderTransaction on OrderTransaction {
id
processedAt
paymentIcon {
id
url
altText
}
paymentDetails {
... on CardPaymentDetails {
last4
cardBrand
}
}
transactionAmount {
presentmentMoney {
...Price
}
}
giftCardDetails {
last4
balance {
...Price
}
}
status
kind
transactionParentId
type
typeDetails {
name
message
}
}
fragment Price on MoneyV2 {
amount
currencyCode
}
fragment Address on CustomerAddress {
id
address1
address2
firstName
lastName
provinceCode: zoneCode
city
zip
countryCodeV2: territoryCode
company
phone: phoneNumber
}
fragment Fulfillment on Fulfillment {
id
status
createdAt
estimatedDeliveryAt
trackingInformation {
number
company
url
}
requiresShipping
fulfillmentLineItems(first: 20) {
nodes {
id
quantity
lineItem {
id
name
title
presentmentTitle
sku
image {
id
url
altText
}
}
}
}
events(first: 20, sortKey: HAPPENED_AT, reverse: true) {
edges {
node {
id
...FulfillmentEvent
}
}
}
}
fragment FulfillmentEvent on FulfillmentEvent {
status
happenedAt
}
fragment LineItem on LineItem {
title
image {
altText
height
url
width
}
price {
...Price
}
quantity
sku
totalPrice {
...Price
}
variantTitle
}
fragment OrderPaymentInformation on OrderPaymentInformation {
paymentStatus
totalPaidAmount {
...Price
}
totalOutstandingAmount {
...Price
}
paymentTerms {
id
overdue
nextDueAt
paymentSchedules(first: 2) {
nodes {
id
dueAt
completed
amount {
...Price
}
}
}
}
}
`;

View File

@@ -0,0 +1,15 @@
import customerDetailsFragment from '../fragments/customer-details';
const customerFragment = `#graphql
`;
// NOTE: https://shopify.dev/docs/api/customer/latest/queries/customer
export const getCustomerOrdersQuery = `#graphql
query getCustomerOrdersQuery {
customer {
...CustomerDetails
}
}
${customerFragment}
${customerDetailsFragment}
`;

View File

@@ -35,6 +35,13 @@ export type Collection = ShopifyCollection & {
path: string;
};
export type Customer = {
emailAddress: string;
firstName?: string;
lastName?: string;
displayName?: string;
};
export type Image = {
url: string;
altText: string;
@@ -58,6 +65,278 @@ export type PageMetafield = {
value: string;
};
export type Fulfillment = {
status: string;
createdAt: string;
fulfilledLineItems: {
id: string;
quantity: number;
image: Image;
}[];
trackingInformation: {
number: string;
company: string;
url: string;
}[];
events: {
status: string;
happenedAt: string;
}[];
};
export type Transaction = {
processedAt: string;
paymentIcon: Image;
paymentDetails: {
last4: string;
cardBrand: string;
};
transactionAmount: Money;
};
export type Address = {
address1: string;
address2: string | null;
firstName: string;
lastName: string;
provinceCode: string;
city: string;
zip: string;
country: string;
company: string | null;
phone: string;
};
export type LineItem = {
id: string;
title: string;
image: Image;
price?: Money;
quantity?: number;
sku?: string;
totalPrice?: Money;
variantTitle?: string;
};
export type Order = {
id: string;
name: string;
customer?: Customer;
processedAt: string;
fulfillments: Fulfillment[];
transactions: Transaction[];
lineItems: LineItem[];
shippingAddress: Address;
billingAddress: Address;
/** the price of all line items, excluding taxes and surcharges */
subtotal: Money;
totalShipping: Money;
totalTax: Money;
totalPrice: Money;
shippingMethod?: {
name: string;
price: Money;
};
};
export type ShopifyOrder = {
id: string;
name: string;
confirmationNumber: string;
customer: ShopifyCustomer;
processedAt: string;
cancelledAt: string | null;
currencyCode: string;
transactions: ShopifyOrderTransaction[];
billingAddress: ShopifyAddress;
shippingAddress: ShopifyAddress;
fulfillments: Connection<ShopifyFulfillment>;
lineItems: Connection<ShopifyLineItem>;
totalPrice: ShopifyMoneyV2;
subtotal: ShopifyMoneyV2;
totalShipping: ShopifyMoneyV2;
totalTax: ShopifyMoneyV2;
financialStatus: string;
totalRefunded: ShopifyMoneyV2;
refunds: ShopifyRefund[];
paymentInformation: ShopifyOrderPaymentInformation;
requiresShipping: boolean;
shippingLine: ShopifyShippingLine;
note: string | null;
};
type ShopifyShippingLine = {
title: string;
originalPrice: ShopifyMoneyV2;
};
type ShopifyOrderTransaction = {
id: string;
processedAt: string;
paymentIcon: ShopifyPaymentIconImage;
paymentDetails: ShopifyCardPaymentDetails;
transactionAmount: ShopifyMoneyBag;
giftCardDetails: ShopifyGiftCardDetails | null;
status: string;
kind: string;
transactionParentId: string | null;
type: string;
typeDetails: ShopifyTransactionTypeDetails;
};
type ShopifyPaymentIconImage = {
id: string;
url: string;
altText: string;
};
type ShopifyCardPaymentDetails = {
last4: string;
cardBrand: string;
};
type ShopifyGiftCardDetails = {
last4: string;
balance: ShopifyMoneyV2;
};
type ShopifyMoneyBag = {
presentmentMoney: ShopifyMoneyV2;
};
export type ShopifyMoneyV2 = {
amount: string;
currencyCode: string;
};
type ShopifyTransactionTypeDetails = {
name: string;
message: string | null;
};
export type ShopifyAddress = {
id: string;
address1: string;
address2: string | null;
firstName: string;
lastName: string;
provinceCode: string;
city: string;
zip: string;
countryCodeV2: string;
company: string | null;
phone: string;
};
type ShopifyFulfillment = {
id: string;
status: string;
createdAt: string;
estimatedDeliveryAt: string | null;
trackingInformation: ShopifyTrackingInformation[];
requiresShipping: boolean;
fulfillmentLineItems: ShopifyFulfillmentLineItemConnection;
events: Connection<ShopifyFulfillmentEvent>;
};
type ShopifyTrackingInformation = {
number: string;
company: string;
url: string;
};
type ShopifyFulfillmentLineItemConnection = {
nodes: ShopifyFulfillmentLineItem[];
};
type ShopifyFulfillmentLineItem = {
id: string;
quantity: number;
lineItem: ShopifyLineItem;
};
type ShopifyLineItem = {
id: string;
title: string;
image: ShopifyImage;
price: ShopifyMoneyV2;
quantity: number;
sku: string;
totalPrice: ShopifyMoneyV2;
variantTitle: string;
};
type ShopifyImage = {
altText: string;
height: number;
url: string;
width: number;
};
type ShopifyFulfillmentEventConnection = {
edges: ShopifyFulfillmentEventEdge[];
};
type ShopifyFulfillmentEventEdge = {
node: ShopifyFulfillmentEvent;
};
type ShopifyFulfillmentEvent = {
status: string;
happenedAt: string;
};
type ShopifyRefund = {
id: string;
createdAt: string;
};
type ShopifyOrderPaymentInformation = {
paymentCollectionUrl: string;
paymentStatus: string;
totalPaidAmount: ShopifyMoneyV2;
totalOutstandingAmount: ShopifyMoneyV2;
paymentTerms: ShopifyPaymentTerms | null;
};
type ShopifyPaymentTerms = {
id: string;
overdue: boolean;
nextDueAt: string;
paymentSchedules: ShopifyPaymentScheduleConnection;
};
type ShopifyPaymentScheduleConnection = {
nodes: ShopifyPaymentSchedule[];
};
type ShopifyPaymentSchedule = {
id: string;
dueAt: string;
completed: boolean;
amount: ShopifyMoneyV2;
};
export type ShopifyCustomer = {
id: string;
emailAddress: ShopifyCustomerEmailAddress;
firstName: string;
lastName: string;
phoneNumber: ShopifyCustomerPhoneNumber | null;
imageUrl: string;
displayName: string;
};
type ShopifyCustomerEmailAddress = {
emailAddress: string;
marketingState: string;
};
type ShopifyCustomerPhoneNumber = {
phoneNumber: string;
marketingState: string;
};
export const PAGE_TYPES = [
'image',
'icon_content_section',
@@ -399,6 +678,29 @@ export type ShopifyProductsOperation = {
};
};
export type ShopifyCustomerOperation = {
data: {
customer: ShopifyCustomer;
};
};
export type ShopifyCustomerOrdersOperation = {
data: {
customer: {
orders: Connection<ShopifyOrder>;
};
};
};
export type ShopifyCustomerOrderOperation = {
data: {
order: ShopifyOrder;
};
variables: {
orderId: string;
};
};
export type CoreChargeOption = {
label: string;
value: string;

89
lib/shopify/utils.ts Normal file
View File

@@ -0,0 +1,89 @@
import clsx, { ClassValue } from 'clsx';
import { ReadonlyURLSearchParams } from 'next/navigation';
import { twMerge } from 'tailwind-merge';
import { Menu } from './types';
export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyURLSearchParams) => {
const paramsString = params.toString();
const queryString = `${paramsString.length ? '?' : ''}${paramsString}`;
return `${pathname}${queryString}`;
};
export const ensureStartsWith = (stringToCheck: string, startsWith: string) =>
stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`;
export const validateEnvironmentVariables = () => {
const requiredEnvironmentVariables = [
'SHOPIFY_STORE_DOMAIN',
'SHOPIFY_STOREFRONT_ACCESS_TOKEN',
'SHOPIFY_CUSTOMER_ACCOUNT_API_CLIENT_ID',
'SHOPIFY_CUSTOMER_ACCOUNT_API_URL',
'SHOPIFY_CUSTOMER_API_VERSION',
'SHOPIFY_ORIGIN_URL'
];
const missingEnvironmentVariables = [] as string[];
requiredEnvironmentVariables.forEach((envVar) => {
if (!process.env[envVar]) {
missingEnvironmentVariables.push(envVar);
}
});
if (missingEnvironmentVariables.length) {
throw new Error(
`The following environment variables are missing. Your site will not work without them. Read more: https://vercel.com/docs/integrations/shopify#configure-environment-variables\n\n${missingEnvironmentVariables.join(
'\n'
)}\n`
);
}
if (
process.env.SHOPIFY_STORE_DOMAIN?.includes('[') ||
process.env.SHOPIFY_STORE_DOMAIN?.includes(']')
) {
throw new Error(
'Your `SHOPIFY_STORE_DOMAIN` environment variable includes brackets (ie. `[` and / or `]`). Your site will not work with them there. Please remove them.'
);
}
};
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function normalizeUrl(domain: string, url: string) {
return url.replace(domain, '').replace('/collections', '/search').replace('/pages', '');
}
export const parseMetaFieldValue = <T>(field: { value: string } | null): T | null => {
try {
return field?.value ? JSON.parse(field.value) : null;
} catch (error) {
return null;
}
};
export const findParentCollection = (menu: Menu[], collection: string): Menu | null => {
let parentCollection: Menu | null = null;
for (const item of menu) {
if (item.items.length) {
const hasParent = item.items.some((subItem) => subItem.path.includes(collection));
if (hasParent) {
return item;
} else {
parentCollection = findParentCollection(item.items, collection);
}
}
}
return parentCollection;
};
export function parseJSON(json: any) {
if (String(json).includes('__proto__')) return JSON.parse(json, noproto);
return JSON.parse(json);
}
function noproto(k: string, v: string) {
if (k !== '__proto__') return v;
}