Add products feed app (#161)
* Add products feed app from another repo * Improve codebase * Create sweet-ears-tease.md * Apply cr changes
This commit is contained in:
parent
61aa7f00ac
commit
ca3a0305f2
70 changed files with 39623 additions and 15 deletions
5
.changeset/sweet-ears-tease.md
Normal file
5
.changeset/sweet-ears-tease.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-app-products-feed": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add products feed app
|
4
apps/products-feed/.eslintrc
Normal file
4
apps/products-feed/.eslintrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["saleor"]
|
||||||
|
}
|
20
apps/products-feed/.graphqlrc.yml
Normal file
20
apps/products-feed/.graphqlrc.yml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
schema: graphql/schema.graphql
|
||||||
|
documents: [graphql/**/*.graphql, src/**/*.ts, src/**/*.tsx]
|
||||||
|
extensions:
|
||||||
|
codegen:
|
||||||
|
overwrite: true
|
||||||
|
generates:
|
||||||
|
generated/graphql.ts:
|
||||||
|
config:
|
||||||
|
dedupeFragments: true
|
||||||
|
plugins:
|
||||||
|
- typescript
|
||||||
|
- typescript-operations
|
||||||
|
- urql-introspection
|
||||||
|
- typescript-urql:
|
||||||
|
documentVariablePrefix: "Untyped"
|
||||||
|
fragmentVariablePrefix: "Untyped"
|
||||||
|
- typed-document-node
|
||||||
|
generated/schema.graphql:
|
||||||
|
plugins:
|
||||||
|
- schema-ast
|
111
apps/products-feed/README.md
Normal file
111
apps/products-feed/README.md
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<div align="center">
|
||||||
|
<h1>Products feed</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<p>Share products data with the feed aggregators</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## About Saleor Invoices app
|
||||||
|
|
||||||
|
- Create Google Merchant Feed XML
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
Before you start, make sure you have installed:
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/en/)
|
||||||
|
- [pnpm](https://pnpm.io/)
|
||||||
|
- [Saleor CLI](https://docs.saleor.io/docs/3.x/cli) - optional, but recommended
|
||||||
|
|
||||||
|
### With CLI
|
||||||
|
|
||||||
|
The easiest way to set up a Saleor app is by using the Saleor CLI.
|
||||||
|
|
||||||
|
[Saleor CLI](https://github.com/saleor/saleor-cli) is designed to save you from the repetitive chores around Saleor development, including creating Apps. It will take the burden of spawning new apps locally, connecting them with Saleor environments, and establishing a tunnel for local development in seconds.
|
||||||
|
|
||||||
|
[Full Saleor CLI reference](https://docs.saleor.io/docs/3.x/developer/cli)
|
||||||
|
|
||||||
|
If you don't have a (free developer) Saleor Cloud account, create one with the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
saleor register
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you're ready to create your first App:
|
||||||
|
|
||||||
|
```
|
||||||
|
saleor app create [your-app-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
In this step, Saleor CLI will:
|
||||||
|
|
||||||
|
- clone this repository to the specified folder
|
||||||
|
- install dependencies
|
||||||
|
- ask you whether you'd like to install the app in the selected Saleor environment
|
||||||
|
- create `.env` file
|
||||||
|
- start the app in development mode
|
||||||
|
|
||||||
|
Having your app ready, the final thing you want to establish is a tunnel with your Saleor environment. Go to your app's directory first and run:
|
||||||
|
|
||||||
|
```
|
||||||
|
saleor app tunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
Your local application should be available now to the outside world (Saleor instance) for accepting all the events via webhooks.
|
||||||
|
|
||||||
|
A quick note: the next time you come back to your project, it is enough to launch your app in a standard way (and then launch your tunnel as described earlier):
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without CLI
|
||||||
|
|
||||||
|
1. Install the dependencies by running:
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the local server with:
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Expose local environment using tunnel:
|
||||||
|
Use tunneling tools like [localtunnel](https://github.com/localtunnel/localtunnel) or [ngrok](https://ngrok.com/).
|
||||||
|
|
||||||
|
4. Install aplication at your dashboard:
|
||||||
|
|
||||||
|
If you use Saleor Cloud or your local server is exposed, you can install your app by following this link:
|
||||||
|
|
||||||
|
```
|
||||||
|
[YOUR_SALEOR_DASHBOARD_URL]/apps/install?manifestUrl=[YOUR_APP_TUNNEL_MANIFEST_URL]
|
||||||
|
```
|
||||||
|
|
||||||
|
This template host manifest at `/api/manifest`
|
||||||
|
|
||||||
|
You can also install application using GQL or command line. Follow the guide [how to install your app](https://docs.saleor.io/docs/3.x/developer/extending/apps/installing-apps#installation-using-graphql-api) to learn more.
|
||||||
|
|
||||||
|
### Generated schema and typings
|
||||||
|
|
||||||
|
Commands `build` and `dev` would generate schema and typed functions using Saleor's GraphQL endpoint. Commit the `generated` folder to your repo as they are necessary for queries and keeping track of the schema changes.
|
||||||
|
|
||||||
|
[Learn more](https://www.graphql-code-generator.com/) about GraphQL code generation.
|
||||||
|
|
||||||
|
### Storing registration data - APL
|
||||||
|
|
||||||
|
During registration process Saleor API pass the auth token to the app. With this token App can query Saleor API with privileged access (depending on requested permissions during the installation).
|
||||||
|
To store this data, app-template use a different [APL interfaces](https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md).
|
||||||
|
|
||||||
|
The choice of the APL is done using `APL` environment variable. If value is not set, FileAPL is used. Available choices:
|
||||||
|
|
||||||
|
- `file`: no additional setup is required. Good choice for local development. Can't be used for multi tenant-apps or be deployed (not intended for production)
|
||||||
|
- `upstash`: use [Upstash](https://upstash.com/) Redis as storage method. Free account required. Can be used for development and production and supports multi-tenancy. Requires `UPSTASH_URL` and `UPSTASH_TOKEN` environment variables to be set
|
||||||
|
- `vercel`: used by deployments from the Marketplace. It's single-tenant only and only supported by Vercel deployments done with Saleor CLI. Requires `SALEOR_REGISTER_APP_URL` and `SALEOR_DEPLOYMENT_TOKEN` environment variables to be set (handled automatically by the Saleor CLI)
|
||||||
|
|
||||||
|
If you want to use your own database, you can implement your own APL. [Check the documentation to read more.](https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md)
|
0
apps/products-feed/graphql/fragments/.gitkeep
Normal file
0
apps/products-feed/graphql/fragments/.gitkeep
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
fragment GoogleFeedProductVariant on ProductVariant {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
sku
|
||||||
|
pricing {
|
||||||
|
price {
|
||||||
|
gross {
|
||||||
|
currency
|
||||||
|
amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quantityAvailable
|
||||||
|
product {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
description
|
||||||
|
seoDescription
|
||||||
|
thumbnail {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
category {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
apps/products-feed/graphql/mutations/.gitkeep
Normal file
0
apps/products-feed/graphql/mutations/.gitkeep
Normal file
0
apps/products-feed/graphql/queries/.gitkeep
Normal file
0
apps/products-feed/graphql/queries/.gitkeep
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
query FetchAppDetails {
|
||||||
|
app {
|
||||||
|
id
|
||||||
|
privateMetadata {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
query FetchProductDataForFeed($first:Int!, $after: String, $channel: String!){
|
||||||
|
productVariants(first:$first, after: $after, channel: $channel){
|
||||||
|
pageInfo{
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges{
|
||||||
|
node{
|
||||||
|
...GoogleFeedProductVariant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
apps/products-feed/graphql/queries/ShopDetails.graphql
Normal file
6
apps/products-feed/graphql/queries/ShopDetails.graphql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
query ShopDetails {
|
||||||
|
shop{
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
29625
apps/products-feed/graphql/schema.graphql
Normal file
29625
apps/products-feed/graphql/schema.graphql
Normal file
File diff suppressed because it is too large
Load diff
0
apps/products-feed/graphql/subscriptions/.gitkeep
Normal file
0
apps/products-feed/graphql/subscriptions/.gitkeep
Normal file
5
apps/products-feed/next-env.d.ts
vendored
Normal file
5
apps/products-feed/next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
4
apps/products-feed/next.config.js
Normal file
4
apps/products-feed/next.config.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
module.exports = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
};
|
75
apps/products-feed/package.json
Normal file
75
apps/products-feed/package.json
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"name": "saleor-app-products-feed",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||||
|
"build": "pnpm generate && next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"fetch-schema": "curl https://raw.githubusercontent.com/saleor/saleor/${npm_package_saleor_schemaVersion}/saleor/graphql/schema.graphql > graphql/schema.graphql",
|
||||||
|
"generate": "graphql-codegen",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"saleor": {
|
||||||
|
"schemaVersion": "3.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@material-ui/core": "^4.12.4",
|
||||||
|
"@material-ui/icons": "^4.11.3",
|
||||||
|
"@material-ui/lab": "4.0.0-alpha.61",
|
||||||
|
"@saleor/app-sdk": "0.29.0",
|
||||||
|
"@saleor/macaw-ui": "^0.7.2",
|
||||||
|
"@tanstack/react-query": "^4.24.2",
|
||||||
|
"@trpc/client": "^10.9.0",
|
||||||
|
"@trpc/next": "^10.9.0",
|
||||||
|
"@trpc/react-query": "^10.9.0",
|
||||||
|
"@trpc/server": "^10.9.0",
|
||||||
|
"@urql/exchange-auth": "^1.0.0",
|
||||||
|
"@vitejs/plugin-react": "^3.0.1",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
|
"fast-xml-parser": "^4.0.15",
|
||||||
|
"graphql": "^16.6.0",
|
||||||
|
"graphql-tag": "^2.12.6",
|
||||||
|
"jsdom": "^20.0.3",
|
||||||
|
"next": "13.1.2",
|
||||||
|
"next-urql": "^4.0.2",
|
||||||
|
"pino": "^8.8.0",
|
||||||
|
"pino-pretty": "^9.1.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.43.0",
|
||||||
|
"react-is": "^18.2.0",
|
||||||
|
"urql": "^3.0.3",
|
||||||
|
"usehooks-ts": "^2.9.1",
|
||||||
|
"vite": "^4.0.4",
|
||||||
|
"vitest": "^0.27.1",
|
||||||
|
"zod": "^3.20.2",
|
||||||
|
"@saleor/apps-shared": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint-config-saleor": "workspace:*",
|
||||||
|
"@graphql-codegen/cli": "2.13.3",
|
||||||
|
"@graphql-codegen/introspection": "2.2.1",
|
||||||
|
"@graphql-codegen/typed-document-node": "^2.3.3",
|
||||||
|
"@graphql-codegen/typescript": "2.7.3",
|
||||||
|
"@graphql-codegen/typescript-operations": "2.5.3",
|
||||||
|
"@graphql-codegen/typescript-urql": "^3.7.0",
|
||||||
|
"@graphql-codegen/urql-introspection": "2.2.1",
|
||||||
|
"@graphql-typed-document-node/core": "^3.1.1",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/react-dom": "^18.0.10",
|
||||||
|
"eslint": "8.31.0",
|
||||||
|
"eslint-config-next": "13.1.2",
|
||||||
|
"eslint-config-prettier": "^8.6.0",
|
||||||
|
"prettier": "^2.8.2",
|
||||||
|
"typescript": "4.9.4"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,ts,tsx}": "eslint --cache --fix",
|
||||||
|
"*.{js,ts,tsx,css,md,json}": "prettier --write"
|
||||||
|
}
|
||||||
|
}
|
7105
apps/products-feed/pnpm-lock.yaml
Normal file
7105
apps/products-feed/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
48
apps/products-feed/src/lib/create-graphq-client.ts
Normal file
48
apps/products-feed/src/lib/create-graphq-client.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { AuthConfig, authExchange } from "@urql/exchange-auth";
|
||||||
|
import {
|
||||||
|
cacheExchange,
|
||||||
|
createClient as urqlCreateClient,
|
||||||
|
dedupExchange,
|
||||||
|
fetchExchange,
|
||||||
|
} from "urql";
|
||||||
|
|
||||||
|
interface IAuthState {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createClient = (url: string, getAuth: AuthConfig<IAuthState>["getAuth"]) =>
|
||||||
|
urqlCreateClient({
|
||||||
|
url,
|
||||||
|
exchanges: [
|
||||||
|
dedupExchange,
|
||||||
|
cacheExchange,
|
||||||
|
authExchange<IAuthState>({
|
||||||
|
addAuthToOperation: ({ authState, operation }) => {
|
||||||
|
if (!authState || !authState?.token) {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions =
|
||||||
|
typeof operation.context.fetchOptions === "function"
|
||||||
|
? operation.context.fetchOptions()
|
||||||
|
: operation.context.fetchOptions || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...operation,
|
||||||
|
context: {
|
||||||
|
...operation.context,
|
||||||
|
fetchOptions: {
|
||||||
|
...fetchOptions,
|
||||||
|
headers: {
|
||||||
|
...fetchOptions.headers,
|
||||||
|
"Authorization-Bearer": authState.token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getAuth,
|
||||||
|
}),
|
||||||
|
fetchExchange,
|
||||||
|
],
|
||||||
|
});
|
17
apps/products-feed/src/lib/fill-url-template.ts
Normal file
17
apps/products-feed/src/lib/fill-url-template.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
interface FillUrlTemplateArgs {
|
||||||
|
urlTemplate: string;
|
||||||
|
productId: string;
|
||||||
|
productSlug: string;
|
||||||
|
variantId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fillUrlTemplate = ({
|
||||||
|
urlTemplate,
|
||||||
|
productId,
|
||||||
|
productSlug,
|
||||||
|
variantId,
|
||||||
|
}: FillUrlTemplateArgs) =>
|
||||||
|
urlTemplate
|
||||||
|
.replace("{productId}", productId)
|
||||||
|
.replace("{productSlug}", productSlug)
|
||||||
|
.replace("{variantId}", variantId);
|
53
apps/products-feed/src/lib/google-feed/fetch-product-data.ts
Normal file
53
apps/products-feed/src/lib/google-feed/fetch-product-data.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { url } from "inspector";
|
||||||
|
import { Client } from "urql";
|
||||||
|
import { logger as pinoLogger } from "../../lib/logger";
|
||||||
|
import {
|
||||||
|
FetchProductDataForFeedDocument,
|
||||||
|
GoogleFeedProductVariantFragment,
|
||||||
|
} from "../../../generated/graphql";
|
||||||
|
|
||||||
|
interface FetchProductDataArgs {
|
||||||
|
client: Client;
|
||||||
|
channel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchProductData = async ({ client, channel }: FetchProductDataArgs) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: url, channel, route: "Google Product Feed" });
|
||||||
|
|
||||||
|
let result = await client
|
||||||
|
.query(FetchProductDataForFeedDocument, { channel: channel as string, first: 100 })
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error(`Error during the GraphqlAPI call: ${result.error.message}`);
|
||||||
|
throw new Error("Error during the GraphQL API call");
|
||||||
|
}
|
||||||
|
|
||||||
|
let variants: GoogleFeedProductVariantFragment[] =
|
||||||
|
result.data?.productVariants?.edges.map((e) => e.node) || [];
|
||||||
|
|
||||||
|
while (result.data?.productVariants?.pageInfo.hasNextPage) {
|
||||||
|
logger.debug("Fetching the next page of products");
|
||||||
|
|
||||||
|
result = await client
|
||||||
|
.query(FetchProductDataForFeedDocument, {
|
||||||
|
channel: channel as string,
|
||||||
|
first: 100,
|
||||||
|
after: result.data.productVariants.pageInfo.endCursor,
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error(`Error during the GraphqlAPI call: ${result.error.message}`);
|
||||||
|
throw new Error("Error during the GraphQL API call");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.data?.productVariants?.edges.length) {
|
||||||
|
logger.warn("Fetching the second page of results resulted in no entries");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
variants = variants?.concat(result.data?.productVariants?.edges.map((e) => e.node));
|
||||||
|
}
|
||||||
|
|
||||||
|
return variants;
|
||||||
|
};
|
31
apps/products-feed/src/lib/google-feed/fetch-shop-data.ts
Normal file
31
apps/products-feed/src/lib/google-feed/fetch-shop-data.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { url } from "inspector";
|
||||||
|
import { Client } from "urql";
|
||||||
|
import { logger as pinoLogger } from "../logger";
|
||||||
|
import { ShopDetailsDocument } from "../../../generated/graphql";
|
||||||
|
|
||||||
|
interface FetchShopDataArgs {
|
||||||
|
client: Client;
|
||||||
|
channel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchShopData = async ({ client, channel }: FetchShopDataArgs) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: url, channel, route: "Google Product Feed" });
|
||||||
|
|
||||||
|
const result = await client.query(ShopDetailsDocument, {}).toPromise();
|
||||||
|
const shopDetails = result.data?.shop;
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error(`Error during the GraphqlAPI call: ${result.error.message}`);
|
||||||
|
throw new Error("Error during the GraphQL API call");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shopDetails) {
|
||||||
|
logger.error("Shop details query returned no data");
|
||||||
|
throw new Error("Shop details query returned no data");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shopName: shopDetails?.name,
|
||||||
|
shopDescription: shopDetails?.description || undefined,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { XMLBuilder } from "fast-xml-parser";
|
||||||
|
import { GoogleFeedProductVariantFragment } from "../../../generated/graphql";
|
||||||
|
import { productToProxy } from "./product-to-proxy";
|
||||||
|
import { shopDetailsToProxy } from "./shop-details-to-proxy";
|
||||||
|
interface GenerateGoogleXmlFeedArgs {
|
||||||
|
productVariants: GoogleFeedProductVariantFragment[];
|
||||||
|
storefrontUrl: string;
|
||||||
|
productStorefrontUrl: string;
|
||||||
|
shopName: string;
|
||||||
|
shopDescription?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateGoogleXmlFeed = ({
|
||||||
|
productVariants,
|
||||||
|
storefrontUrl,
|
||||||
|
productStorefrontUrl,
|
||||||
|
shopName,
|
||||||
|
shopDescription,
|
||||||
|
}: GenerateGoogleXmlFeedArgs) => {
|
||||||
|
const items = productVariants.map((v) => {
|
||||||
|
// Price format has to be altered from the en format to the one expected by Google
|
||||||
|
const price = v.pricing?.price?.gross.currency
|
||||||
|
? new Intl.NumberFormat("en-EN", {
|
||||||
|
useGrouping: false,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
style: "decimal",
|
||||||
|
currencyDisplay: "code",
|
||||||
|
currency: v.pricing?.price?.gross.currency,
|
||||||
|
}).format(v.pricing?.price?.gross.amount) + ` ${v.pricing?.price?.gross.currency}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return productToProxy({
|
||||||
|
storefrontUrlTemplate: productStorefrontUrl,
|
||||||
|
id: v.product.id,
|
||||||
|
name: `${v.product.name} - ${v.name}`,
|
||||||
|
slug: v.product.slug,
|
||||||
|
variantId: v.id,
|
||||||
|
sku: v.sku || undefined,
|
||||||
|
description: v.product.seoDescription || v.product.description,
|
||||||
|
availability: v.quantityAvailable && v.quantityAvailable > 0 ? "in_stock" : "out_of_stock",
|
||||||
|
category: v.product.category?.name || "unknown",
|
||||||
|
// TODO: prepare category mapping for google categories
|
||||||
|
googleProductCategory: "",
|
||||||
|
price: price,
|
||||||
|
imageUrl: v.product.thumbnail?.url || "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const builder = new XMLBuilder({
|
||||||
|
attributeNamePrefix: "@_",
|
||||||
|
attributesGroupName: "@",
|
||||||
|
textNodeName: "#text",
|
||||||
|
ignoreAttributes: false,
|
||||||
|
format: true,
|
||||||
|
indentBy: " ",
|
||||||
|
suppressEmptyNode: false,
|
||||||
|
preserveOrder: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const channelData = shopDetailsToProxy({
|
||||||
|
title: shopName,
|
||||||
|
description: shopDescription,
|
||||||
|
storefrontUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
"?xml": [
|
||||||
|
{
|
||||||
|
"#text": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
":@": {
|
||||||
|
"@_version": "1.0",
|
||||||
|
"@_encoding": "utf-8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
rss: [
|
||||||
|
{
|
||||||
|
channel: channelData.concat(items),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
":@": {
|
||||||
|
"@_xmlns:g": "http://base.google.com/ns/1.0",
|
||||||
|
"@_version": "2.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return builder.build(data);
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
|
import { appRouter } from "../../modules/trpc/trpc-app-router";
|
||||||
|
|
||||||
|
interface GetGoogleFeedSettingsArgs {
|
||||||
|
authData: AuthData;
|
||||||
|
channel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGoogleFeedSettings = async ({ authData, channel }: GetGoogleFeedSettingsArgs) => {
|
||||||
|
const caller = appRouter.createCaller({
|
||||||
|
appId: authData.appId,
|
||||||
|
saleorApiUrl: authData.saleorApiUrl,
|
||||||
|
token: authData.token,
|
||||||
|
ssr: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const configurations = await caller.appConfiguration.fetch();
|
||||||
|
|
||||||
|
const configuration = configurations.shopConfigPerChannel[channel];
|
||||||
|
|
||||||
|
const storefrontUrl = configuration.urlConfiguration.storefrontUrl;
|
||||||
|
|
||||||
|
const productStorefrontUrl = configuration.urlConfiguration.productStorefrontUrl;
|
||||||
|
|
||||||
|
if (!storefrontUrl.length || !productStorefrontUrl.length) {
|
||||||
|
throw new Error("The application has not been configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
storefrontUrl,
|
||||||
|
productStorefrontUrl,
|
||||||
|
};
|
||||||
|
};
|
103
apps/products-feed/src/lib/google-feed/product-to-proxy.ts
Normal file
103
apps/products-feed/src/lib/google-feed/product-to-proxy.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import { fillUrlTemplate } from "../fill-url-template";
|
||||||
|
import { ProductEntry } from "./types";
|
||||||
|
|
||||||
|
export const productToProxy = (p: ProductEntry) => {
|
||||||
|
const item: any[] = [
|
||||||
|
{
|
||||||
|
"g:id": [
|
||||||
|
{
|
||||||
|
"#text": p.sku || p.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: [
|
||||||
|
{
|
||||||
|
"#text": p.name,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"g:condition": [
|
||||||
|
{
|
||||||
|
"#text": p.condition || "new",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Consider implementing categories
|
||||||
|
* https://support.google.com/merchants/answer/6324436?hl=en
|
||||||
|
*
|
||||||
|
* However, this field is optional and google seems to automatically match category
|
||||||
|
*/
|
||||||
|
// {
|
||||||
|
// "g:google_product_category": [
|
||||||
|
// {
|
||||||
|
// "#text": p.googleProductCategory,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
"g:availability": [
|
||||||
|
{
|
||||||
|
"#text": p.availability,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"g:product_type": [
|
||||||
|
{
|
||||||
|
"#text": p.category,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (p.description?.length) {
|
||||||
|
item.push({
|
||||||
|
"g:description": [
|
||||||
|
{
|
||||||
|
"#text": p.description,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.storefrontUrlTemplate?.length) {
|
||||||
|
item.push({
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
"#text": fillUrlTemplate({
|
||||||
|
urlTemplate: p.storefrontUrlTemplate,
|
||||||
|
productId: p.id,
|
||||||
|
productSlug: p.slug,
|
||||||
|
variantId: p.variantId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.imageUrl?.length) {
|
||||||
|
item.push({
|
||||||
|
"g:image_link": [
|
||||||
|
{
|
||||||
|
"#text": p.imageUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.price?.length) {
|
||||||
|
item.push({
|
||||||
|
"g:price": [
|
||||||
|
{
|
||||||
|
"#text": p.price,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { ShopDetailsEntry } from "./types";
|
||||||
|
|
||||||
|
export const shopDetailsToProxy = ({ title, storefrontUrl, description }: ShopDetailsEntry) => {
|
||||||
|
const data: any[] = [
|
||||||
|
{
|
||||||
|
title: [
|
||||||
|
{
|
||||||
|
"#text": title,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
"#text": storefrontUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (description) {
|
||||||
|
data.push({
|
||||||
|
description: [
|
||||||
|
{
|
||||||
|
"#text": description,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
21
apps/products-feed/src/lib/google-feed/types.ts
Normal file
21
apps/products-feed/src/lib/google-feed/types.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export type ProductEntry = {
|
||||||
|
id: string;
|
||||||
|
sku?: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
variantId: string;
|
||||||
|
description?: string;
|
||||||
|
storefrontUrlTemplate?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
condition?: "new" | "refurbished" | "used";
|
||||||
|
price?: string;
|
||||||
|
googleProductCategory: string;
|
||||||
|
availability: "in_stock" | "out_of_stock" | "preorder" | "backorder";
|
||||||
|
category: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShopDetailsEntry = {
|
||||||
|
title: string;
|
||||||
|
storefrontUrl: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
7
apps/products-feed/src/lib/is-in-iframe.ts
Normal file
7
apps/products-feed/src/lib/is-in-iframe.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export function isInIframe() {
|
||||||
|
try {
|
||||||
|
return window.self !== window.top;
|
||||||
|
} catch (e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
16
apps/products-feed/src/lib/logger.ts
Normal file
16
apps/products-feed/src/lib/logger.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import pino from "pino";
|
||||||
|
|
||||||
|
export const logger = pino({
|
||||||
|
level: process.env.APP_DEBUG ?? "silent",
|
||||||
|
transport:
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? {
|
||||||
|
target: "pino-pretty",
|
||||||
|
options: {
|
||||||
|
colorize: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createLogger = logger.child.bind(logger);
|
19
apps/products-feed/src/lib/no-ssr-wrapper.tsx
Normal file
19
apps/products-feed/src/lib/no-ssr-wrapper.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React, { PropsWithChildren } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
const Wrapper = (props: PropsWithChildren<{}>) => <React.Fragment>{props.children}</React.Fragment>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saleor App can be rendered only as a Saleor Dashboard iframe.
|
||||||
|
* All content is rendered after Dashboard exchanges auth with the app.
|
||||||
|
* Hence, there is no reason to render app server side.
|
||||||
|
*
|
||||||
|
* This component forces app to work in SPA-mode. It simplifies browser-only code and reduces need
|
||||||
|
* of using dynamic() calls
|
||||||
|
*
|
||||||
|
* You can use this wrapper selectively for some pages or remove it completely.
|
||||||
|
* It doesn't affect Saleor communication, but may cause problems with some client-only code.
|
||||||
|
*/
|
||||||
|
export const NoSSRWrapper = dynamic(() => Promise.resolve(Wrapper), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
48
apps/products-feed/src/lib/theme-synchronizer.test.tsx
Normal file
48
apps/products-feed/src/lib/theme-synchronizer.test.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { AppBridgeState } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { render, waitFor } from "@testing-library/react";
|
||||||
|
import { ThemeSynchronizer } from "./theme-synchronizer";
|
||||||
|
|
||||||
|
const appBridgeState: AppBridgeState = {
|
||||||
|
ready: true,
|
||||||
|
token: "token",
|
||||||
|
domain: "some-domain.saleor.cloud",
|
||||||
|
theme: "dark",
|
||||||
|
path: "/",
|
||||||
|
locale: "en",
|
||||||
|
id: "app-id",
|
||||||
|
saleorApiUrl: "https://some-domain.saleor.cloud/graphql/",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockThemeChange = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("@saleor/app-sdk/app-bridge", () => {
|
||||||
|
return {
|
||||||
|
useAppBridge() {
|
||||||
|
return {
|
||||||
|
appBridgeState: appBridgeState,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@saleor/macaw-ui", () => {
|
||||||
|
return {
|
||||||
|
useTheme() {
|
||||||
|
return {
|
||||||
|
setTheme: mockThemeChange,
|
||||||
|
themeType: "light",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ThemeSynchronizer", () => {
|
||||||
|
it("Updates MacawUI theme when AppBridgeState theme changes", () => {
|
||||||
|
render(<ThemeSynchronizer />);
|
||||||
|
|
||||||
|
return waitFor(() => {
|
||||||
|
expect(mockThemeChange).toHaveBeenCalledWith("dark");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
33
apps/products-feed/src/lib/theme-synchronizer.tsx
Normal file
33
apps/products-feed/src/lib/theme-synchronizer.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { useTheme } from "@saleor/macaw-ui";
|
||||||
|
import { memo, useEffect } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Macaw-ui stores its theme mode in memory and local storage. To synchronize App with Dashboard,
|
||||||
|
* Macaw must be informed about this change from AppBridge.
|
||||||
|
*
|
||||||
|
* If you are not using Macaw, you can remove this.
|
||||||
|
*/
|
||||||
|
function _ThemeSynchronizer() {
|
||||||
|
const { appBridgeState } = useAppBridge();
|
||||||
|
const { setTheme, themeType } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!setTheme || !appBridgeState?.theme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeType !== appBridgeState?.theme) {
|
||||||
|
setTheme(appBridgeState.theme);
|
||||||
|
/**
|
||||||
|
* Hack to fix macaw, which is going into infinite loop on light mode (probably de-sync local storage with react state)
|
||||||
|
* TODO Fix me when Macaw 2.0 is shipped
|
||||||
|
*/
|
||||||
|
window.localStorage.setItem("macaw-ui-theme", appBridgeState.theme);
|
||||||
|
}
|
||||||
|
}, [appBridgeState?.theme, setTheme, themeType]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeSynchronizer = memo(_ThemeSynchronizer);
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { AppConfig, SellerShopConfig } from "./app-config";
|
||||||
|
|
||||||
|
const getDefaultEmptyUrlConfiguration = (): SellerShopConfig["urlConfiguration"] => ({
|
||||||
|
storefrontUrl: "",
|
||||||
|
productStorefrontUrl: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const getChannelUrlConfiguration =
|
||||||
|
(appConfig: AppConfig | null | undefined) => (channelSlug: string) => {
|
||||||
|
try {
|
||||||
|
return appConfig?.shopConfigPerChannel[channelSlug].urlConfiguration ?? null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setChannelUrlConfiguration =
|
||||||
|
(appConfig: AppConfig | null | undefined) =>
|
||||||
|
(channelSlug: string) =>
|
||||||
|
(urlConfiguration: SellerShopConfig["urlConfiguration"]) => {
|
||||||
|
const appConfigNormalized = structuredClone(appConfig) ?? { shopConfigPerChannel: {} };
|
||||||
|
|
||||||
|
appConfigNormalized.shopConfigPerChannel[channelSlug] ??= {
|
||||||
|
urlConfiguration: getDefaultEmptyUrlConfiguration(),
|
||||||
|
};
|
||||||
|
appConfigNormalized.shopConfigPerChannel[channelSlug].urlConfiguration = urlConfiguration;
|
||||||
|
|
||||||
|
return appConfigNormalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppConfigContainer = {
|
||||||
|
getChannelUrlConfiguration: getChannelUrlConfiguration,
|
||||||
|
setChannelUrlConfiguration: setChannelUrlConfiguration,
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appConfigInputSchema = z.object({
|
||||||
|
shopConfigPerChannel: z.record(
|
||||||
|
z.object({
|
||||||
|
urlConfiguration: z.object({
|
||||||
|
/**
|
||||||
|
* min() to allow empty strings
|
||||||
|
*/
|
||||||
|
storefrontUrl: z.string().min(0),
|
||||||
|
productStorefrontUrl: z.string().min(0),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
|
@ -0,0 +1,12 @@
|
||||||
|
export interface SellerShopConfig {
|
||||||
|
urlConfiguration: {
|
||||||
|
storefrontUrl: string;
|
||||||
|
productStorefrontUrl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShopConfigPerChannelSlug = Record<string, SellerShopConfig>;
|
||||||
|
|
||||||
|
export type AppConfig = {
|
||||||
|
shopConfigPerChannel: ShopConfigPerChannelSlug;
|
||||||
|
};
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { router } from "../trpc/trpc-server";
|
||||||
|
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
|
||||||
|
import { PrivateMetadataAppConfigurator } from "./app-configurator";
|
||||||
|
import { createSettingsManager } from "./metadata-manager";
|
||||||
|
import { logger as pinoLogger } from "../../lib/logger";
|
||||||
|
import { appConfigInputSchema } from "./app-config-input-schema";
|
||||||
|
import { GetAppConfigurationService } from "./get-app-configuration.service";
|
||||||
|
|
||||||
|
export const appConfigurationRouter = router({
|
||||||
|
fetch: protectedClientProcedure.query(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
||||||
|
logger.debug("appConfigurationRouter.fetch called");
|
||||||
|
|
||||||
|
return new GetAppConfigurationService({
|
||||||
|
apiClient: ctx.apiClient,
|
||||||
|
saleorApiUrl: ctx.saleorApiUrl,
|
||||||
|
}).getConfiguration();
|
||||||
|
}),
|
||||||
|
setAndReplace: protectedClientProcedure
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(appConfigInputSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
||||||
|
logger.debug(input, "appConfigurationRouter.setAndReplace called with input");
|
||||||
|
|
||||||
|
const appConfigurator = new PrivateMetadataAppConfigurator(
|
||||||
|
createSettingsManager(ctx.apiClient),
|
||||||
|
ctx.saleorApiUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
await appConfigurator.setConfig(input);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { AppConfig } from "./app-config";
|
||||||
|
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
|
|
||||||
|
export interface AppConfigurator {
|
||||||
|
setConfig(config: AppConfig): Promise<void>;
|
||||||
|
getConfig(): Promise<AppConfig | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrivateMetadataAppConfigurator implements AppConfigurator {
|
||||||
|
private metadataKey = "app-config";
|
||||||
|
|
||||||
|
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
|
||||||
|
|
||||||
|
getConfig(): Promise<AppConfig | undefined> {
|
||||||
|
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {
|
||||||
|
if (!data) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("Invalid metadata value, can't be parsed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config: AppConfig): Promise<void> {
|
||||||
|
return this.metadataManager.set({
|
||||||
|
key: this.metadataKey,
|
||||||
|
value: JSON.stringify(config),
|
||||||
|
domain: this.saleorApiUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { AppConfig } from "./app-config";
|
||||||
|
import { AppConfigContainer } from "./app-config-container";
|
||||||
|
import { ChannelFragment, ShopInfoFragment } from "../../../generated/graphql";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO Test
|
||||||
|
*/
|
||||||
|
export const FallbackAppConfig = {
|
||||||
|
createFallbackConfigFromExistingShopAndChannels(
|
||||||
|
channels: ChannelFragment[],
|
||||||
|
shopUrlConfiguration: ShopInfoFragment | null
|
||||||
|
) {
|
||||||
|
return (channels ?? []).reduce<AppConfig>(
|
||||||
|
(state, channel) => {
|
||||||
|
return AppConfigContainer.setChannelUrlConfiguration(state)(channel.slug)({
|
||||||
|
storefrontUrl: "",
|
||||||
|
productStorefrontUrl: "",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ shopConfigPerChannel: {} }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { PrivateMetadataAppConfigurator } from "./app-configurator";
|
||||||
|
import { createSettingsManager } from "./metadata-manager";
|
||||||
|
import { ChannelsFetcher } from "../channels/channels-fetcher";
|
||||||
|
import { ShopInfoFetcher } from "../shop-info/shop-info-fetcher";
|
||||||
|
import { FallbackAppConfig } from "./fallback-app-config";
|
||||||
|
import { Client } from "urql";
|
||||||
|
import { logger as pinoLogger } from "../../lib/logger";
|
||||||
|
|
||||||
|
export class GetAppConfigurationService {
|
||||||
|
constructor(
|
||||||
|
private settings: {
|
||||||
|
apiClient: Client;
|
||||||
|
saleorApiUrl: string;
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getConfiguration() {
|
||||||
|
const logger = pinoLogger.child({
|
||||||
|
service: "GetAppConfigurationService",
|
||||||
|
saleorApiUrl: this.settings.saleorApiUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { saleorApiUrl, apiClient } = this.settings;
|
||||||
|
|
||||||
|
const appConfigurator = new PrivateMetadataAppConfigurator(
|
||||||
|
createSettingsManager(apiClient),
|
||||||
|
saleorApiUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
const savedAppConfig = (await appConfigurator.getConfig()) ?? null;
|
||||||
|
|
||||||
|
logger.debug(savedAppConfig, "Retrieved app config from Metadata. Will return it");
|
||||||
|
|
||||||
|
if (savedAppConfig) {
|
||||||
|
return savedAppConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("App config not found in metadata. Will create default config now.");
|
||||||
|
|
||||||
|
const channelsFetcher = new ChannelsFetcher(apiClient);
|
||||||
|
const shopInfoFetcher = new ShopInfoFetcher(apiClient);
|
||||||
|
|
||||||
|
const [channels, shopUrlConfiguration] = await Promise.all([
|
||||||
|
channelsFetcher.fetchChannels(),
|
||||||
|
shopInfoFetcher.fetchShopInfo(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
logger.debug(channels, "Fetched channels");
|
||||||
|
logger.debug(shopUrlConfiguration, "Fetched shop url configuration");
|
||||||
|
|
||||||
|
const appConfig = FallbackAppConfig.createFallbackConfigFromExistingShopAndChannels(
|
||||||
|
channels ?? [],
|
||||||
|
shopUrlConfiguration
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(appConfig, "Created a fallback AppConfig. Will save it.");
|
||||||
|
|
||||||
|
await appConfigurator.setConfig(appConfig);
|
||||||
|
|
||||||
|
logger.info("Saved initial AppConfig");
|
||||||
|
|
||||||
|
return appConfig;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { MetadataEntry, EncryptedMetadataManager } from "@saleor/app-sdk/settings-manager";
|
||||||
|
import { Client, gql } from "urql";
|
||||||
|
import {
|
||||||
|
FetchAppDetailsDocument,
|
||||||
|
FetchAppDetailsQuery,
|
||||||
|
UpdateAppMetadataDocument,
|
||||||
|
} from "../../../generated/graphql";
|
||||||
|
|
||||||
|
gql`
|
||||||
|
mutation UpdateAppMetadata($id: ID!, $input: [MetadataInput!]!) {
|
||||||
|
updatePrivateMetadata(id: $id, input: $input) {
|
||||||
|
item {
|
||||||
|
privateMetadata {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
gql`
|
||||||
|
query FetchAppDetails {
|
||||||
|
app {
|
||||||
|
id
|
||||||
|
privateMetadata {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function fetchAllMetadata(client: Client): Promise<MetadataEntry[]> {
|
||||||
|
const { error, data } = await client
|
||||||
|
.query<FetchAppDetailsQuery>(FetchAppDetailsDocument, {})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data?.app?.privateMetadata.map((md) => ({ key: md.key, value: md.value })) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function mutateMetadata(client: Client, metadata: MetadataEntry[]) {
|
||||||
|
// to update the metadata, ID is required
|
||||||
|
const { error: idQueryError, data: idQueryData } = await client
|
||||||
|
.query(FetchAppDetailsDocument, {})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (idQueryError) {
|
||||||
|
throw new Error(
|
||||||
|
"Could not fetch the app id. Please check if auth data for the client are valid."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appId = idQueryData?.app?.id;
|
||||||
|
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("Could not fetch the app ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: mutationError, data: mutationData } = await client
|
||||||
|
.mutation(UpdateAppMetadataDocument, {
|
||||||
|
id: appId,
|
||||||
|
input: metadata,
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (mutationError) {
|
||||||
|
throw new Error(`Mutation error: ${mutationError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
mutationData?.updatePrivateMetadata?.item?.privateMetadata.map((md) => ({
|
||||||
|
key: md.key,
|
||||||
|
value: md.value,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSettingsManager = (client: Client) => {
|
||||||
|
// EncryptedMetadataManager gives you interface to manipulate metadata and cache values in memory.
|
||||||
|
// We recommend it for production, because all values are encrypted.
|
||||||
|
// If your use case require plain text values, you can use MetadataManager.
|
||||||
|
return new EncryptedMetadataManager({
|
||||||
|
// Secret key should be randomly created for production and set as environment variable
|
||||||
|
encryptionKey: process.env.SECRET_KEY!,
|
||||||
|
fetchMetadata: () => fetchAllMetadata(client),
|
||||||
|
mutateMetadata: (metadata) => mutateMetadata(client, metadata),
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
|
import { LinearProgress, Paper } from "@material-ui/core";
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
import { AppConfigContainer } from "../app-config-container";
|
||||||
|
import { UrlConfigurationForm } from "./url-configuration-form";
|
||||||
|
import { ChannelsList } from "./channels-list";
|
||||||
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { AppColumnsLayout } from "../../ui/app-columns-layout";
|
||||||
|
import { FeedPreviewCard } from "./feed-preview-card";
|
||||||
|
import { Instructions } from "./instructions";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => {
|
||||||
|
return {
|
||||||
|
header: { marginBottom: 20 },
|
||||||
|
grid: { display: "grid", gridTemplateColumns: "1fr 1fr", alignItems: "start", gap: 40 },
|
||||||
|
formContainer: {
|
||||||
|
top: 0,
|
||||||
|
position: "sticky",
|
||||||
|
},
|
||||||
|
instructionsContainer: {
|
||||||
|
padding: 15,
|
||||||
|
},
|
||||||
|
configurationColumn: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ChannelsConfiguration = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
|
|
||||||
|
const { data: configurationData, refetch: refetchConfig } =
|
||||||
|
trpcClient.appConfiguration.fetch.useQuery();
|
||||||
|
|
||||||
|
const channels = trpcClient.channels.fetch.useQuery();
|
||||||
|
|
||||||
|
const [activeChannelSlug, setActiveChannelSlug] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { mutate, error: saveError } = trpcClient.appConfiguration.setAndReplace.useMutation({
|
||||||
|
onSuccess() {
|
||||||
|
refetchConfig();
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: "Success",
|
||||||
|
text: "Saved app configuration",
|
||||||
|
status: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (channels.isSuccess) {
|
||||||
|
setActiveChannelSlug(channels.data![0].slug ?? null);
|
||||||
|
}
|
||||||
|
}, [channels.isSuccess, channels.data]);
|
||||||
|
|
||||||
|
const activeChannel = useMemo(() => {
|
||||||
|
try {
|
||||||
|
return channels.data!.find((c) => c.slug === activeChannelSlug)!;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [channels.data, activeChannelSlug]);
|
||||||
|
|
||||||
|
if (channels.isLoading || !channels.data) {
|
||||||
|
return <LinearProgress />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activeChannel) {
|
||||||
|
return <div>Error. No channel available</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppColumnsLayout>
|
||||||
|
<ChannelsList
|
||||||
|
channels={channels.data}
|
||||||
|
activeChannelSlug={activeChannel.slug}
|
||||||
|
onChannelClick={setActiveChannelSlug}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{activeChannel ? (
|
||||||
|
<div className={styles.configurationColumn}>
|
||||||
|
<FeedPreviewCard channelSlug={activeChannel.slug} />
|
||||||
|
<Paper elevation={0} className={styles.formContainer}>
|
||||||
|
<UrlConfigurationForm
|
||||||
|
channelID={activeChannel.id}
|
||||||
|
key={activeChannelSlug}
|
||||||
|
channelSlug={activeChannel.slug}
|
||||||
|
onSubmit={async (data) => {
|
||||||
|
const newConfig = AppConfigContainer.setChannelUrlConfiguration(configurationData)(
|
||||||
|
activeChannel.slug
|
||||||
|
)(data);
|
||||||
|
|
||||||
|
mutate(newConfig);
|
||||||
|
}}
|
||||||
|
initialData={AppConfigContainer.getChannelUrlConfiguration(configurationData)(
|
||||||
|
activeChannel.slug
|
||||||
|
)}
|
||||||
|
channelName={activeChannel?.name ?? activeChannelSlug}
|
||||||
|
/>
|
||||||
|
{saveError && <span>{saveError.message}</span>}
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<Instructions />
|
||||||
|
</AppColumnsLayout>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,68 @@
|
||||||
|
import {
|
||||||
|
makeStyles,
|
||||||
|
OffsettedList,
|
||||||
|
OffsettedListBody,
|
||||||
|
OffsettedListHeader,
|
||||||
|
OffsettedListItem,
|
||||||
|
OffsettedListItemCell,
|
||||||
|
} from "@saleor/macaw-ui";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
import React from "react";
|
||||||
|
import { ChannelFragment } from "../../../../generated/graphql";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => {
|
||||||
|
return {
|
||||||
|
listItem: {
|
||||||
|
cursor: "pointer",
|
||||||
|
height: "auto !important",
|
||||||
|
border: `1px solid transparent`,
|
||||||
|
},
|
||||||
|
listItemActive: {
|
||||||
|
border: `1px solid ${
|
||||||
|
theme.palette.type === "light" ? theme.palette.divider : theme.palette.grey.A200
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
cellSlug: {
|
||||||
|
fontFamily: "monospace",
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
channels: ChannelFragment[];
|
||||||
|
activeChannelSlug: string;
|
||||||
|
onChannelClick(channelSlug: string): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChannelsList = ({ channels, activeChannelSlug, onChannelClick }: Props) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OffsettedList gridTemplate={["1fr"]}>
|
||||||
|
<OffsettedListBody>
|
||||||
|
{channels.map((c) => {
|
||||||
|
return (
|
||||||
|
<OffsettedListItem
|
||||||
|
className={clsx(styles.listItem, {
|
||||||
|
[styles.listItemActive]: c.slug === activeChannelSlug,
|
||||||
|
})}
|
||||||
|
key={c.slug}
|
||||||
|
onClick={() => {
|
||||||
|
onChannelClick(c.slug);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OffsettedListItemCell>
|
||||||
|
{c.name}
|
||||||
|
<Typography variant="caption">
|
||||||
|
<code>{c.slug}</code>
|
||||||
|
</Typography>
|
||||||
|
</OffsettedListItemCell>
|
||||||
|
</OffsettedListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</OffsettedListBody>
|
||||||
|
</OffsettedList>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Paper, TextField, Typography } from "@material-ui/core";
|
||||||
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { Button, makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => {
|
||||||
|
return {
|
||||||
|
header: { marginBottom: 20 },
|
||||||
|
grid: { display: "grid", gridTemplateColumns: "1fr 1fr", alignItems: "start", gap: 40 },
|
||||||
|
formContainer: {
|
||||||
|
top: 0,
|
||||||
|
position: "sticky",
|
||||||
|
},
|
||||||
|
instructionsContainer: {
|
||||||
|
padding: 15,
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
interface FeedPreviewCardProps {
|
||||||
|
channelSlug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeedPreviewCard = ({ channelSlug }: FeedPreviewCardProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
|
const { saleorApiUrl } = appBridge?.getState() || {};
|
||||||
|
|
||||||
|
const googleFeedUrl = `${window.location.origin}/api/feed/${encodeURIComponent(
|
||||||
|
saleorApiUrl as string
|
||||||
|
)}/${channelSlug}/google.xml`;
|
||||||
|
|
||||||
|
const openUrlInNewTab = async (url: string) => {
|
||||||
|
await appBridge?.dispatch(actions.Redirect({ to: url, newContext: true }));
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Paper elevation={0} className={styles.instructionsContainer}>
|
||||||
|
<Typography paragraph variant="h4">
|
||||||
|
Your Google Merchant Feed
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
label="Google feed URL"
|
||||||
|
fullWidth
|
||||||
|
value={googleFeedUrl}
|
||||||
|
disabled={true}
|
||||||
|
className={styles.field}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="secondary"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => openUrlInNewTab(googleFeedUrl)}
|
||||||
|
>
|
||||||
|
Open feed in a new tab
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Link, Paper, Typography } from "@material-ui/core";
|
||||||
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => {
|
||||||
|
return {
|
||||||
|
instructionsContainer: {
|
||||||
|
padding: 15,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Instructions = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={0} className={styles.instructionsContainer}>
|
||||||
|
<Typography paragraph variant="h4">
|
||||||
|
Using the feed
|
||||||
|
</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
Configure your Google Merchant account to schedule fetches of the feed. Instructions can be
|
||||||
|
found{" "}
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
onClick={() => {
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Redirect({
|
||||||
|
to: "https://support.google.com/merchants/answer/1219255",
|
||||||
|
newContext: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography paragraph variant="h4">
|
||||||
|
URL templates
|
||||||
|
</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
URLs to products in your storefront are generated dynamically, based on the product data.
|
||||||
|
For example, the template
|
||||||
|
</Typography>
|
||||||
|
<code>{"https://example.com/product/{productSlug}"}</code>
|
||||||
|
<Typography paragraph>Will produce</Typography>
|
||||||
|
<code>{"https://example.com/product/red-t-shirt"}</code>
|
||||||
|
<Typography paragraph>Available fields: productId, productSlug, variantId</Typography>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { SellerShopConfig } from "../app-config";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { TextField, TextFieldProps, Typography } from "@material-ui/core";
|
||||||
|
import { Button, makeStyles } from "@saleor/macaw-ui";
|
||||||
|
import React from "react";
|
||||||
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
field: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
channelName: {
|
||||||
|
fontFamily: "monospace",
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
channelSlug: string;
|
||||||
|
channelName: string;
|
||||||
|
channelID: string;
|
||||||
|
onSubmit(data: SellerShopConfig["urlConfiguration"]): Promise<void>;
|
||||||
|
initialData?: SellerShopConfig["urlConfiguration"] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UrlConfigurationForm = (props: Props) => {
|
||||||
|
const { register, handleSubmit } = useForm<SellerShopConfig["urlConfiguration"]>({
|
||||||
|
defaultValues: props.initialData ?? undefined,
|
||||||
|
});
|
||||||
|
const styles = useStyles();
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
|
|
||||||
|
const CommonFieldProps: TextFieldProps = {
|
||||||
|
className: styles.field,
|
||||||
|
fullWidth: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChannelNameClick = () => {
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Redirect({
|
||||||
|
to: `/channels/${props.channelID}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((data, event) => {
|
||||||
|
props.onSubmit(data);
|
||||||
|
})}
|
||||||
|
className={styles.form}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" paragraph>
|
||||||
|
Configure
|
||||||
|
<strong onClick={handleChannelNameClick} className={styles.channelName}>
|
||||||
|
{` ${props.channelName} `}
|
||||||
|
</strong>
|
||||||
|
channel:
|
||||||
|
</Typography>
|
||||||
|
<TextField label="Storefront home URL" {...CommonFieldProps} {...register("storefrontUrl")} />
|
||||||
|
<TextField
|
||||||
|
label="Storefront product URL template"
|
||||||
|
{...CommonFieldProps}
|
||||||
|
{...register("productStorefrontUrl")}
|
||||||
|
/>
|
||||||
|
<Button type="submit" fullWidth variant="primary">
|
||||||
|
Save channel configuration
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { SellerShopConfig } from "./app-config";
|
||||||
|
|
||||||
|
export const UrlConfiguration = {
|
||||||
|
createEmpty(): SellerShopConfig["urlConfiguration"] {
|
||||||
|
return {
|
||||||
|
storefrontUrl: "",
|
||||||
|
productStorefrontUrl: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
27
apps/products-feed/src/modules/channels/channels-fetcher.ts
Normal file
27
apps/products-feed/src/modules/channels/channels-fetcher.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Client, gql } from "urql";
|
||||||
|
import { FetchChannelsDocument } from "../../../generated/graphql";
|
||||||
|
|
||||||
|
gql`
|
||||||
|
fragment Channel on Channel {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
|
||||||
|
query FetchChannels {
|
||||||
|
channels {
|
||||||
|
...Channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export class ChannelsFetcher {
|
||||||
|
constructor(private client: Client) {}
|
||||||
|
|
||||||
|
fetchChannels() {
|
||||||
|
return this.client
|
||||||
|
.query(FetchChannelsDocument, {})
|
||||||
|
.toPromise()
|
||||||
|
.then((r) => r.data?.channels ?? null);
|
||||||
|
}
|
||||||
|
}
|
17
apps/products-feed/src/modules/channels/channels.router.ts
Normal file
17
apps/products-feed/src/modules/channels/channels.router.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { router } from "../trpc/trpc-server";
|
||||||
|
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
|
||||||
|
import { ChannelsFetcher } from "./channels-fetcher";
|
||||||
|
import { ChannelFragment } from "../../../generated/graphql";
|
||||||
|
import { createClient } from "../../lib/create-graphq-client";
|
||||||
|
|
||||||
|
export const channelsRouter = router({
|
||||||
|
fetch: protectedClientProcedure.query(async ({ ctx, input }): Promise<ChannelFragment[]> => {
|
||||||
|
const client = createClient(ctx.saleorApiUrl, async () =>
|
||||||
|
Promise.resolve({ token: ctx.appToken })
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetcher = new ChannelsFetcher(client);
|
||||||
|
|
||||||
|
return fetcher.fetchChannels().then((channels) => channels ?? []);
|
||||||
|
}),
|
||||||
|
});
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Client, gql } from "urql";
|
||||||
|
import { ShopInfoDocument, ShopInfoFragment } from "../../../generated/graphql";
|
||||||
|
|
||||||
|
gql`
|
||||||
|
fragment ShopInfo on Shop {
|
||||||
|
companyAddress {
|
||||||
|
country {
|
||||||
|
country
|
||||||
|
code
|
||||||
|
}
|
||||||
|
city
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
streetAddress1
|
||||||
|
streetAddress2
|
||||||
|
companyName
|
||||||
|
phone
|
||||||
|
postalCode
|
||||||
|
countryArea
|
||||||
|
cityArea
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query ShopInfo {
|
||||||
|
shop {
|
||||||
|
...ShopInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface IShopInfoFetcher {
|
||||||
|
fetchShopInfo(): Promise<ShopInfoFragment | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShopInfoFetcher implements IShopInfoFetcher {
|
||||||
|
constructor(private client: Client) {}
|
||||||
|
|
||||||
|
fetchShopInfo(): Promise<ShopInfoFragment | null> {
|
||||||
|
return this.client
|
||||||
|
.query(ShopInfoDocument, {})
|
||||||
|
.toPromise()
|
||||||
|
.then((resp) => resp.data?.shop ?? null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { verifyJWT } from "@saleor/app-sdk/verify-jwt";
|
||||||
|
import { middleware, procedure } from "./trpc-server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { ProtectedHandlerError } from "@saleor/app-sdk/handlers/next";
|
||||||
|
import { saleorApp } from "../../saleor-app";
|
||||||
|
import { logger } from "../../lib/logger";
|
||||||
|
import { createClient } from "../../lib/create-graphq-client";
|
||||||
|
|
||||||
|
const attachAppToken = middleware(async ({ ctx, next }) => {
|
||||||
|
logger.debug("attachAppToken middleware");
|
||||||
|
|
||||||
|
if (!ctx.saleorApiUrl) {
|
||||||
|
logger.debug("ctx.saleorApiUrl not found, throwing");
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Missing saleorApiUrl in request",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const authData = await saleorApp.apl.get(ctx.saleorApiUrl);
|
||||||
|
|
||||||
|
if (!authData) {
|
||||||
|
logger.debug("authData not found, throwing 401");
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "Missing auth data",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
appToken: authData.token,
|
||||||
|
saleorApiUrl: authData.saleorApiUrl,
|
||||||
|
appId: authData.appId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateClientToken = middleware(async ({ ctx, next, meta }) => {
|
||||||
|
logger.debug(
|
||||||
|
{
|
||||||
|
permissions: meta?.requiredClientPermissions,
|
||||||
|
},
|
||||||
|
"Calling validateClientToken middleware with permissions required"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ctx.token) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Missing token in request. This middleware can be used only in frontend",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.appId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Missing appId in request. This middleware can be used after auth is attached",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.saleorApiUrl) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message:
|
||||||
|
"Missing saleorApiUrl in request. This middleware can be used after auth is attached",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.ssr) {
|
||||||
|
try {
|
||||||
|
logger.debug("trying to verify JWT token from frontend");
|
||||||
|
logger.debug({ token: ctx.token ? `${ctx.token[0]}...` : undefined });
|
||||||
|
|
||||||
|
await verifyJWT({
|
||||||
|
appId: ctx.appId,
|
||||||
|
token: ctx.token,
|
||||||
|
saleorApiUrl: ctx.saleorApiUrl,
|
||||||
|
requiredPermissions: meta?.requiredClientPermissions ?? [],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug("JWT verification failed, throwing");
|
||||||
|
throw new ProtectedHandlerError("JWT verification failed: ", "JWT_VERIFICATION_FAILED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
...ctx,
|
||||||
|
saleorApiUrl: ctx.saleorApiUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct common graphQL client and attach it to the context
|
||||||
|
*
|
||||||
|
* Can be used only if called from the frontend (react-query),
|
||||||
|
* otherwise jwks validation will fail (if createCaller used)
|
||||||
|
*
|
||||||
|
* TODO Rethink middleware composition to enable safe server-side router calls
|
||||||
|
*/
|
||||||
|
export const protectedClientProcedure = procedure
|
||||||
|
.use(attachAppToken)
|
||||||
|
.use(validateClientToken)
|
||||||
|
.use(async ({ ctx, next }) => {
|
||||||
|
const client = createClient(ctx.saleorApiUrl, async () =>
|
||||||
|
Promise.resolve({ token: ctx.appToken })
|
||||||
|
);
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
apiClient: client,
|
||||||
|
appToken: ctx.appToken,
|
||||||
|
saleorApiUrl: ctx.saleorApiUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
10
apps/products-feed/src/modules/trpc/trpc-app-router.ts
Normal file
10
apps/products-feed/src/modules/trpc/trpc-app-router.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { channelsRouter } from "../channels/channels.router";
|
||||||
|
import { router } from "./trpc-server";
|
||||||
|
import { appConfigurationRouter } from "../app-configuration/app-configuration.router";
|
||||||
|
|
||||||
|
export const appRouter = router({
|
||||||
|
channels: channelsRouter,
|
||||||
|
appConfiguration: appConfigurationRouter,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AppRouter = typeof appRouter;
|
36
apps/products-feed/src/modules/trpc/trpc-client.ts
Normal file
36
apps/products-feed/src/modules/trpc/trpc-client.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { httpBatchLink } from "@trpc/client";
|
||||||
|
import { createTRPCNext } from "@trpc/next";
|
||||||
|
|
||||||
|
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
|
||||||
|
import { appBridgeInstance } from "../../pages/_app";
|
||||||
|
import { AppRouter } from "./trpc-app-router";
|
||||||
|
|
||||||
|
function getBaseUrl() {
|
||||||
|
if (typeof window !== "undefined") return "";
|
||||||
|
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
||||||
|
|
||||||
|
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const trpcClient = createTRPCNext<AppRouter>({
|
||||||
|
config({ ctx }) {
|
||||||
|
return {
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
|
headers() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Attach headers from app to client requests, so tRPC can add them to context
|
||||||
|
*/
|
||||||
|
[SALEOR_AUTHORIZATION_BEARER_HEADER]: appBridgeInstance?.getState().token,
|
||||||
|
[SALEOR_API_URL_HEADER]: appBridgeInstance?.getState().saleorApiUrl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
ssr: false,
|
||||||
|
});
|
14
apps/products-feed/src/modules/trpc/trpc-context.ts
Normal file
14
apps/products-feed/src/modules/trpc/trpc-context.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import * as trpcNext from "@trpc/server/adapters/next";
|
||||||
|
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
|
||||||
|
import { inferAsyncReturnType } from "@trpc/server";
|
||||||
|
|
||||||
|
export const createTrpcContext = async ({ res, req }: trpcNext.CreateNextContextOptions) => {
|
||||||
|
return {
|
||||||
|
token: req.headers[SALEOR_AUTHORIZATION_BEARER_HEADER] as string | undefined,
|
||||||
|
saleorApiUrl: req.headers[SALEOR_API_URL_HEADER] as string | undefined,
|
||||||
|
appId: undefined as undefined | string,
|
||||||
|
ssr: undefined as undefined | boolean,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TrpcContext = inferAsyncReturnType<typeof createTrpcContext>;
|
13
apps/products-feed/src/modules/trpc/trpc-server.ts
Normal file
13
apps/products-feed/src/modules/trpc/trpc-server.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { initTRPC } from "@trpc/server";
|
||||||
|
import { TrpcContext } from "./trpc-context";
|
||||||
|
import { AppPermission } from "@saleor/app-sdk/types";
|
||||||
|
|
||||||
|
interface Meta {
|
||||||
|
requiredClientPermissions?: AppPermission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = initTRPC.context<TrpcContext>().meta<Meta>().create();
|
||||||
|
|
||||||
|
export const router = t.router;
|
||||||
|
export const procedure = t.procedure;
|
||||||
|
export const middleware = t.middleware;
|
21
apps/products-feed/src/modules/ui/app-columns-layout.tsx
Normal file
21
apps/products-feed/src/modules/ui/app-columns-layout.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "280px auto 400px",
|
||||||
|
alignItems: "start",
|
||||||
|
gap: 32,
|
||||||
|
maxWidth: 1180,
|
||||||
|
margin: "0 auto",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = PropsWithChildren<{}>;
|
||||||
|
|
||||||
|
export const AppColumnsLayout = ({ children }: Props) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return <div className={styles.root}>{children}</div>;
|
||||||
|
};
|
51
apps/products-feed/src/modules/ui/main-bar.tsx
Normal file
51
apps/products-feed/src/modules/ui/main-bar.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { Paper, PaperProps } from "@material-ui/core";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
height: 96,
|
||||||
|
padding: "0 32px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
leftColumn: {
|
||||||
|
marginRight: "auto",
|
||||||
|
},
|
||||||
|
rightColumn: {},
|
||||||
|
iconColumn: {
|
||||||
|
marginRight: 24,
|
||||||
|
},
|
||||||
|
appName: { fontSize: 24, margin: 0 },
|
||||||
|
appAuthor: {
|
||||||
|
fontSize: 12,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontWeight: 500,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string;
|
||||||
|
author: string;
|
||||||
|
rightColumnContent?: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
} & PaperProps;
|
||||||
|
|
||||||
|
export const MainBar = ({ name, author, rightColumnContent, className, icon }: Props) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={0} className={clsx(styles.root, className)}>
|
||||||
|
{icon && <div className={styles.iconColumn}>{icon}</div>}
|
||||||
|
<div className={styles.leftColumn}>
|
||||||
|
<h1 className={styles.appName}>{name}</h1>
|
||||||
|
<h1 className={styles.appAuthor}>{author}</h1>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rightColumn}>{rightColumnContent}</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
57
apps/products-feed/src/pages/_app.tsx
Normal file
57
apps/products-feed/src/pages/_app.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import "../styles/globals.css";
|
||||||
|
|
||||||
|
import { Theme } from "@material-ui/core/styles";
|
||||||
|
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
||||||
|
import { ThemeProvider as MacawUIThemeProvider } from "@saleor/macaw-ui";
|
||||||
|
import React, { PropsWithChildren, useEffect } from "react";
|
||||||
|
import { AppProps } from "next/app";
|
||||||
|
|
||||||
|
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
||||||
|
import { NoSSRWrapper } from "../lib/no-ssr-wrapper";
|
||||||
|
import { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
|
|
||||||
|
const themeOverrides: Partial<Theme> = {
|
||||||
|
/**
|
||||||
|
* You can override MacawUI theme here
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure instance is a singleton.
|
||||||
|
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
|
||||||
|
*/
|
||||||
|
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* That's a hack required by Macaw-UI incompatibility with React@18
|
||||||
|
*/
|
||||||
|
const ThemeProvider = MacawUIThemeProvider as React.FC<
|
||||||
|
PropsWithChildren<{ overrides?: Partial<Theme>; ssr: boolean }>
|
||||||
|
>;
|
||||||
|
|
||||||
|
function NextApp({ Component, pageProps }: AppProps) {
|
||||||
|
/**
|
||||||
|
* Configure JSS (used by MacawUI) for SSR. If Macaw is not used, can be removed.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const jssStyles = document.querySelector("#jss-server-side");
|
||||||
|
if (jssStyles) {
|
||||||
|
jssStyles?.parentElement?.removeChild(jssStyles);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NoSSRWrapper>
|
||||||
|
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
||||||
|
<ThemeProvider overrides={themeOverrides} ssr={false}>
|
||||||
|
<ThemeSynchronizer />
|
||||||
|
<RoutePropagator />
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</ThemeProvider>
|
||||||
|
</AppBridgeProvider>
|
||||||
|
</NoSSRWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default trpcClient.withTRPC(NextApp);
|
|
@ -0,0 +1,107 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { initUrqlClient } from "next-urql";
|
||||||
|
import { GoogleFeedProductVariantFragment } from "../../../../../../generated/graphql";
|
||||||
|
import { apl } from "../../../../../saleor-app";
|
||||||
|
import { logger as pinoLogger } from "../../../../../lib/logger";
|
||||||
|
import { fetchProductData } from "../../../../../lib/google-feed/fetch-product-data";
|
||||||
|
import { getGoogleFeedSettings } from "../../../../../lib/google-feed/get-google-feed-settings";
|
||||||
|
import { generateGoogleXmlFeed } from "../../../../../lib/google-feed/generate-google-xml-feed";
|
||||||
|
import { fetchShopData } from "../../../../../lib/google-feed/fetch-shop-data";
|
||||||
|
|
||||||
|
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
const url = req.query.url as string;
|
||||||
|
const channel = req.query.channel as string;
|
||||||
|
|
||||||
|
const logger = pinoLogger.child({
|
||||||
|
saleorApiUrl: url,
|
||||||
|
channel,
|
||||||
|
route: "api/feed/{url}/{channel}/google.xml",
|
||||||
|
});
|
||||||
|
logger.debug("Feed route visited");
|
||||||
|
|
||||||
|
if (!url.length) {
|
||||||
|
logger.error("Missing URL param");
|
||||||
|
return res.status(400).json({ error: "No url parameter" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channel.length) {
|
||||||
|
logger.error("Missing channel param");
|
||||||
|
return res.status(400).json({ error: "No channel parameter" });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Checking if app is installed in the given env");
|
||||||
|
const authData = await apl.get(url as string);
|
||||||
|
|
||||||
|
if (!authData) {
|
||||||
|
logger.error(`The app has not been configured with the ${url}`);
|
||||||
|
return res.status(400).json({ error: "The given instance has not been registered" });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("The app is registered for the given URL, checking the configuration");
|
||||||
|
|
||||||
|
// use unauthorized client to eliminate possibility of spilling the non-public data
|
||||||
|
const client = initUrqlClient(
|
||||||
|
{
|
||||||
|
url: authData.saleorApiUrl,
|
||||||
|
},
|
||||||
|
false /* set to false to disable suspense */
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
logger.error("Can't create the gql client");
|
||||||
|
return res.status(500).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
let storefrontUrl: string;
|
||||||
|
let productStorefrontUrl: string;
|
||||||
|
try {
|
||||||
|
const settings = await getGoogleFeedSettings({ authData, channel });
|
||||||
|
storefrontUrl = settings.storefrontUrl;
|
||||||
|
productStorefrontUrl = settings.productStorefrontUrl;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("The application has not been configured");
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Please configure the Google Feed settings at the dashboard" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let shopName: string;
|
||||||
|
let shopDescription: string | undefined;
|
||||||
|
try {
|
||||||
|
const shopDetails = await fetchShopData({ client, channel });
|
||||||
|
shopName = shopDetails.shopName;
|
||||||
|
shopDescription = shopDetails.shopDescription;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Could not fetch the shop details");
|
||||||
|
return res.status(500).json({ error: "Could not fetch the shop details" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: instead of separate variants, use group id https://support.google.com/merchants/answer/6324507?hl=en
|
||||||
|
let productVariants: GoogleFeedProductVariantFragment[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
productVariants = await fetchProductData({ client, channel });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return res.status(400).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Product data fetched. Generating the output");
|
||||||
|
|
||||||
|
const xmlContent = generateGoogleXmlFeed({
|
||||||
|
shopDescription,
|
||||||
|
shopName,
|
||||||
|
storefrontUrl,
|
||||||
|
productStorefrontUrl,
|
||||||
|
productVariants,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug("Feed generated. Returning formatted XML");
|
||||||
|
|
||||||
|
// TODO: add cache headers
|
||||||
|
res.setHeader("Content-Type", "text/xml");
|
||||||
|
res.write(xmlContent);
|
||||||
|
res.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
21
apps/products-feed/src/pages/api/manifest.ts
Normal file
21
apps/products-feed/src/pages/api/manifest.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
|
import { AppManifest } from "@saleor/app-sdk/types";
|
||||||
|
|
||||||
|
import packageJson from "../../../package.json";
|
||||||
|
|
||||||
|
export default createManifestHandler({
|
||||||
|
async manifestFactory(context) {
|
||||||
|
const manifest: AppManifest = {
|
||||||
|
name: packageJson.name,
|
||||||
|
tokenTargetUrl: `${context.appBaseUrl}/api/register`,
|
||||||
|
appUrl: context.appBaseUrl,
|
||||||
|
permissions: [],
|
||||||
|
id: "saleor.app.products-feed",
|
||||||
|
version: packageJson.version,
|
||||||
|
webhooks: [],
|
||||||
|
extensions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return manifest;
|
||||||
|
},
|
||||||
|
});
|
24
apps/products-feed/src/pages/api/register.ts
Normal file
24
apps/products-feed/src/pages/api/register.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
|
|
||||||
|
import { saleorApp } from "../../saleor-app";
|
||||||
|
|
||||||
|
const allowedUrlsPattern = process.env.ALLOWED_DOMAIN_PATTERN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required endpoint, called by Saleor to install app.
|
||||||
|
* It will exchange tokens with app, so saleorApp.apl will contain token
|
||||||
|
*/
|
||||||
|
export default createAppRegisterHandler({
|
||||||
|
apl: saleorApp.apl,
|
||||||
|
allowedSaleorUrls: [
|
||||||
|
(url) => {
|
||||||
|
if (allowedUrlsPattern) {
|
||||||
|
const regex = new RegExp(allowedUrlsPattern);
|
||||||
|
|
||||||
|
return regex.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
8
apps/products-feed/src/pages/api/trpc/[trpc].ts
Normal file
8
apps/products-feed/src/pages/api/trpc/[trpc].ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import * as trpcNext from "@trpc/server/adapters/next";
|
||||||
|
import { createTrpcContext } from "../../../modules/trpc/trpc-context";
|
||||||
|
import { appRouter } from "../../../modules/trpc/trpc-app-router";
|
||||||
|
|
||||||
|
export default trpcNext.createNextApiHandler({
|
||||||
|
router: appRouter,
|
||||||
|
createContext: createTrpcContext,
|
||||||
|
});
|
98
apps/products-feed/src/pages/configuration.tsx
Normal file
98
apps/products-feed/src/pages/configuration.tsx
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { ChannelsConfiguration } from "../modules/app-configuration/ui/channels-configuration";
|
||||||
|
import { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { MainBar } from "../modules/ui/main-bar";
|
||||||
|
import { Button, makeStyles } from "@saleor/macaw-ui";
|
||||||
|
import { GitHub, OfflineBoltOutlined } from "@material-ui/icons";
|
||||||
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
buttonsGrid: { display: "flex", gap: 10 },
|
||||||
|
topBar: {
|
||||||
|
marginBottom: 32,
|
||||||
|
},
|
||||||
|
appIconContainer: {
|
||||||
|
background: `rgb(12, 197, 155)`,
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: "50%",
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const AppIcon = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.appIconContainer}>
|
||||||
|
<Typography variant="h2">PF</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfigurationPage: NextPage = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const channels = trpcClient.channels.fetch.useQuery();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
|
|
||||||
|
const openInNewTab = (url: string) => {
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Redirect({
|
||||||
|
to: url,
|
||||||
|
newContext: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (channels.isSuccess && channels.data.length === 0) {
|
||||||
|
router.push("/not-ready");
|
||||||
|
}
|
||||||
|
}, [channels.data, channels.isSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MainBar
|
||||||
|
icon={<AppIcon />}
|
||||||
|
className={styles.topBar}
|
||||||
|
name="Products Feed"
|
||||||
|
author="By Saleor Commerce"
|
||||||
|
rightColumnContent={
|
||||||
|
<div className={styles.buttonsGrid}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
startIcon={<GitHub />}
|
||||||
|
onClick={() => {
|
||||||
|
openInNewTab("https://github.com/saleor/apps");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Repository
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<OfflineBoltOutlined />}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
openInNewTab("https://github.com/saleor/apps/discussions");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Request a feature
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ChannelsConfiguration />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigurationPage;
|
33
apps/products-feed/src/pages/index.tsx
Normal file
33
apps/products-feed/src/pages/index.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useIsMounted } from "usehooks-ts";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { LinearProgress } from "@material-ui/core";
|
||||||
|
import { isInIframe } from "../lib/is-in-iframe";
|
||||||
|
|
||||||
|
const IndexPage: NextPage = () => {
|
||||||
|
const { appBridgeState } = useAppBridge();
|
||||||
|
const isMounted = useIsMounted();
|
||||||
|
const { replace } = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMounted() && appBridgeState?.ready) {
|
||||||
|
replace("/configuration");
|
||||||
|
}
|
||||||
|
}, [isMounted, appBridgeState?.ready]);
|
||||||
|
|
||||||
|
if (isInIframe()) {
|
||||||
|
return <LinearProgress />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Saleor Merchant Feed</h1>
|
||||||
|
<p>This is Saleor App that allows product feed generation</p>
|
||||||
|
<p>Install app in your Saleor instance and open in with Dashboard</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IndexPage;
|
36
apps/products-feed/src/pages/not-ready.tsx
Normal file
36
apps/products-feed/src/pages/not-ready.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { AlertBase, Button } from "@saleor/macaw-ui";
|
||||||
|
import React from "react";
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
|
||||||
|
const NotReadyPage = () => {
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Saleor Invoices App</h1>
|
||||||
|
<AlertBase variant="error">
|
||||||
|
<Typography variant="h3" paragraph>
|
||||||
|
App can not be used
|
||||||
|
</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
To configure Invoices App you need to create at least 1 channel
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Redirect({
|
||||||
|
to: `/channels/add`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Set up channel
|
||||||
|
</Button>
|
||||||
|
</AlertBase>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotReadyPage;
|
35
apps/products-feed/src/saleor-app.ts
Normal file
35
apps/products-feed/src/saleor-app.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { APL, FileAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
|
||||||
|
import { SaleorApp } from "@saleor/app-sdk/saleor-app";
|
||||||
|
|
||||||
|
const aplType = process.env.APL ?? "file";
|
||||||
|
|
||||||
|
export let apl: APL;
|
||||||
|
|
||||||
|
switch (aplType) {
|
||||||
|
case "upstash":
|
||||||
|
apl = new UpstashAPL();
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "file":
|
||||||
|
apl = new FileAPL();
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "saleor-cloud": {
|
||||||
|
if (!process.env.REST_APL_ENDPOINT || !process.env.REST_APL_TOKEN) {
|
||||||
|
throw new Error("Rest APL is not configured - missing env variables. Check saleor-app.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
apl = new SaleorCloudAPL({
|
||||||
|
resourceUrl: process.env.REST_APL_ENDPOINT,
|
||||||
|
token: process.env.REST_APL_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error("Invalid APL config, ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const saleorApp = new SaleorApp({
|
||||||
|
apl,
|
||||||
|
});
|
6
apps/products-feed/src/setup-tests.ts
Normal file
6
apps/products-feed/src/setup-tests.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Add test setup logic here
|
||||||
|
*
|
||||||
|
* https://vitest.dev/config/#setupfiles
|
||||||
|
*/
|
||||||
|
export {};
|
15
apps/products-feed/src/styles/globals.css
Normal file
15
apps/products-feed/src/styles/globals.css
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
body {
|
||||||
|
font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
|
||||||
|
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
|
color: #111;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
border-radius: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||||
|
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||||
|
}
|
20
apps/products-feed/tsconfig.json
Normal file
20
apps/products-feed/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
13
apps/products-feed/vitest.config.ts
Normal file
13
apps/products-feed/vitest.config.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
passWithNoTests: true,
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: "./src/setup-tests.ts",
|
||||||
|
css: false,
|
||||||
|
},
|
||||||
|
});
|
538
pnpm-lock.yaml
538
pnpm-lock.yaml
|
@ -287,6 +287,111 @@ importers:
|
||||||
pretty-quick: 3.1.3_prettier@2.8.3
|
pretty-quick: 3.1.3_prettier@2.8.3
|
||||||
typescript: 4.9.5
|
typescript: 4.9.5
|
||||||
|
|
||||||
|
apps/products-feed:
|
||||||
|
specifiers:
|
||||||
|
'@graphql-codegen/cli': 2.13.3
|
||||||
|
'@graphql-codegen/introspection': 2.2.1
|
||||||
|
'@graphql-codegen/typed-document-node': ^2.3.3
|
||||||
|
'@graphql-codegen/typescript': 2.7.3
|
||||||
|
'@graphql-codegen/typescript-operations': 2.5.3
|
||||||
|
'@graphql-codegen/typescript-urql': ^3.7.0
|
||||||
|
'@graphql-codegen/urql-introspection': 2.2.1
|
||||||
|
'@graphql-typed-document-node/core': ^3.1.1
|
||||||
|
'@material-ui/core': ^4.12.4
|
||||||
|
'@material-ui/icons': ^4.11.3
|
||||||
|
'@material-ui/lab': 4.0.0-alpha.61
|
||||||
|
'@saleor/app-sdk': 0.29.0
|
||||||
|
'@saleor/apps-shared': workspace:*
|
||||||
|
'@saleor/macaw-ui': ^0.7.2
|
||||||
|
'@tanstack/react-query': ^4.24.2
|
||||||
|
'@testing-library/react': ^13.4.0
|
||||||
|
'@testing-library/react-hooks': ^8.0.1
|
||||||
|
'@trpc/client': ^10.9.0
|
||||||
|
'@trpc/next': ^10.9.0
|
||||||
|
'@trpc/react-query': ^10.9.0
|
||||||
|
'@trpc/server': ^10.9.0
|
||||||
|
'@types/node': ^18.11.18
|
||||||
|
'@types/react': ^18.0.26
|
||||||
|
'@types/react-dom': ^18.0.10
|
||||||
|
'@urql/exchange-auth': ^1.0.0
|
||||||
|
'@vitejs/plugin-react': ^3.0.1
|
||||||
|
clsx: ^1.2.1
|
||||||
|
eslint: 8.31.0
|
||||||
|
eslint-config-next: 13.1.2
|
||||||
|
eslint-config-prettier: ^8.6.0
|
||||||
|
eslint-config-saleor: workspace:*
|
||||||
|
fast-xml-parser: ^4.0.15
|
||||||
|
graphql: ^16.6.0
|
||||||
|
graphql-tag: ^2.12.6
|
||||||
|
jsdom: ^20.0.3
|
||||||
|
next: 13.1.2
|
||||||
|
next-urql: ^4.0.2
|
||||||
|
pino: ^8.8.0
|
||||||
|
pino-pretty: ^9.1.1
|
||||||
|
prettier: ^2.8.2
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0
|
||||||
|
react-hook-form: ^7.43.0
|
||||||
|
react-is: ^18.2.0
|
||||||
|
typescript: 4.9.4
|
||||||
|
urql: ^3.0.3
|
||||||
|
usehooks-ts: ^2.9.1
|
||||||
|
vite: ^4.0.4
|
||||||
|
vitest: ^0.27.1
|
||||||
|
zod: ^3.20.2
|
||||||
|
dependencies:
|
||||||
|
'@material-ui/core': 4.12.4_5ndqzdd6t4rivxsukjv3i3ak2q
|
||||||
|
'@material-ui/icons': 4.11.3_x54wk6dsnsxe7g7vvfmytp77te
|
||||||
|
'@material-ui/lab': 4.0.0-alpha.61_x54wk6dsnsxe7g7vvfmytp77te
|
||||||
|
'@saleor/app-sdk': 0.29.0_7jnwqgtpcnwg4nzft4b6xlzlfi
|
||||||
|
'@saleor/apps-shared': link:../../packages/shared
|
||||||
|
'@saleor/macaw-ui': 0.7.2_pmlnlm755hlzzzocw2qhf3a34e
|
||||||
|
'@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y
|
||||||
|
'@trpc/client': 10.10.0_@trpc+server@10.10.0
|
||||||
|
'@trpc/next': 10.10.0_nwsmvl2oykjx4lgvtx3htlfwme
|
||||||
|
'@trpc/react-query': 10.10.0_5mhyv2iryamqlilh5gtjpyz23q
|
||||||
|
'@trpc/server': 10.10.0
|
||||||
|
'@urql/exchange-auth': 1.0.0_graphql@16.6.0
|
||||||
|
'@vitejs/plugin-react': 3.1.0_vite@4.1.1
|
||||||
|
clsx: 1.2.1
|
||||||
|
fast-xml-parser: 4.1.2
|
||||||
|
graphql: 16.6.0
|
||||||
|
graphql-tag: 2.12.6_graphql@16.6.0
|
||||||
|
jsdom: 20.0.3
|
||||||
|
next: 13.1.2_biqbaboplfbrettd7655fr4n2y
|
||||||
|
next-urql: 4.0.3_react@18.2.0+urql@3.0.3
|
||||||
|
pino: 8.9.0
|
||||||
|
pino-pretty: 9.1.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
react-hook-form: 7.43.1_react@18.2.0
|
||||||
|
react-is: 18.2.0
|
||||||
|
urql: 3.0.3_onqnqwb3ubg5opvemcqf7c2qhy
|
||||||
|
usehooks-ts: 2.9.1_biqbaboplfbrettd7655fr4n2y
|
||||||
|
vite: 4.1.1_@types+node@18.13.0
|
||||||
|
vitest: 0.27.3_jsdom@20.0.3
|
||||||
|
zod: 3.20.2
|
||||||
|
devDependencies:
|
||||||
|
'@graphql-codegen/cli': 2.13.3_d3dx4krdt4fsynqrp5lqxelwe4
|
||||||
|
'@graphql-codegen/introspection': 2.2.1_graphql@16.6.0
|
||||||
|
'@graphql-codegen/typed-document-node': 2.3.13_graphql@16.6.0
|
||||||
|
'@graphql-codegen/typescript': 2.7.3_graphql@16.6.0
|
||||||
|
'@graphql-codegen/typescript-operations': 2.5.3_graphql@16.6.0
|
||||||
|
'@graphql-codegen/typescript-urql': 3.7.3_sy4knu3obj4ys7pjcqbyfxmqle
|
||||||
|
'@graphql-codegen/urql-introspection': 2.2.1_graphql@16.6.0
|
||||||
|
'@graphql-typed-document-node/core': 3.1.1_graphql@16.6.0
|
||||||
|
'@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y
|
||||||
|
'@testing-library/react-hooks': 8.0.1_5ndqzdd6t4rivxsukjv3i3ak2q
|
||||||
|
'@types/node': 18.13.0
|
||||||
|
'@types/react': 18.0.27
|
||||||
|
'@types/react-dom': 18.0.10
|
||||||
|
eslint: 8.31.0
|
||||||
|
eslint-config-next: 13.1.2_iukboom6ndih5an6iafl45j2fe
|
||||||
|
eslint-config-prettier: 8.6.0_eslint@8.31.0
|
||||||
|
eslint-config-saleor: link:../../packages/eslint-config-saleor
|
||||||
|
prettier: 2.8.3
|
||||||
|
typescript: 4.9.4
|
||||||
|
|
||||||
apps/search:
|
apps/search:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@graphql-codegen/cli': 2.13.11
|
'@graphql-codegen/cli': 2.13.11
|
||||||
|
@ -2123,7 +2228,7 @@ packages:
|
||||||
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
|
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@graphql-codegen/plugin-helpers': 2.7.2_graphql@16.6.0
|
'@graphql-codegen/plugin-helpers': 2.7.2_graphql@16.6.0
|
||||||
'@graphql-codegen/typescript': 2.7.3_graphql@16.6.0
|
'@graphql-codegen/typescript': 2.8.1_graphql@16.6.0
|
||||||
'@graphql-codegen/visitor-plugin-common': 2.12.1_graphql@16.6.0
|
'@graphql-codegen/visitor-plugin-common': 2.12.1_graphql@16.6.0
|
||||||
auto-bind: 4.0.0
|
auto-bind: 4.0.0
|
||||||
graphql: 16.6.0
|
graphql: 16.6.0
|
||||||
|
@ -3303,6 +3408,12 @@ packages:
|
||||||
glob: 7.1.7
|
glob: 7.1.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@next/eslint-plugin-next/13.1.2:
|
||||||
|
resolution: {integrity: sha512-WGaNVvIYphdriesP6r7jq/8l7u38tzotnVQuxc1RYKLqYYApSsrebti3OCPoT3Gx0pw2smPIFHH98RzcsgW5GQ==}
|
||||||
|
dependencies:
|
||||||
|
glob: 7.1.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@next/eslint-plugin-next/13.1.6:
|
/@next/eslint-plugin-next/13.1.6:
|
||||||
resolution: {integrity: sha512-o7cauUYsXjzSJkay8wKjpKJf2uLzlggCsGUkPu3lP09Pv97jYlekTC20KJrjQKmSv5DXV0R/uks2ZXhqjNkqAw==}
|
resolution: {integrity: sha512-o7cauUYsXjzSJkay8wKjpKJf2uLzlggCsGUkPu3lP09Pv97jYlekTC20KJrjQKmSv5DXV0R/uks2ZXhqjNkqAw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3714,7 +3825,6 @@ packages:
|
||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
tiny-glob: 0.2.9
|
tiny-glob: 0.2.9
|
||||||
tslib: 2.5.0
|
tslib: 2.5.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@popperjs/core/2.11.6:
|
/@popperjs/core/2.11.6:
|
||||||
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
|
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
|
||||||
|
@ -4395,6 +4505,27 @@ packages:
|
||||||
react-ssr-prepass: 1.5.0_react@18.2.0
|
react-ssr-prepass: 1.5.0_react@18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@trpc/next/10.10.0_nwsmvl2oykjx4lgvtx3htlfwme:
|
||||||
|
resolution: {integrity: sha512-7d84L2OoF0RW06drTbNGOOggwMes8JxI3Ln/VOIaYeERzwOFNCtWPmGjWCdq4l1SKbXC6+baS+b9n5cXc+euwA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tanstack/react-query': ^4.3.8
|
||||||
|
'@trpc/client': 10.10.0
|
||||||
|
'@trpc/react-query': ^10.8.0
|
||||||
|
'@trpc/server': 10.10.0
|
||||||
|
next: '*'
|
||||||
|
react: '>=16.8.0'
|
||||||
|
react-dom: '>=16.8.0'
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y
|
||||||
|
'@trpc/client': 10.10.0_@trpc+server@10.10.0
|
||||||
|
'@trpc/react-query': 10.10.0_5mhyv2iryamqlilh5gtjpyz23q
|
||||||
|
'@trpc/server': 10.10.0
|
||||||
|
next: 13.1.2_biqbaboplfbrettd7655fr4n2y
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
react-ssr-prepass: 1.5.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@trpc/react-query/10.10.0_5mhyv2iryamqlilh5gtjpyz23q:
|
/@trpc/react-query/10.10.0_5mhyv2iryamqlilh5gtjpyz23q:
|
||||||
resolution: {integrity: sha512-Jc/uii1MPevf95/z/W3ufYGHvrFvrtkjxQ8UuXhJCzOgv/FGPqhmA5PH124nLHEgGLBA7zQxHumofhdXosEhUQ==}
|
resolution: {integrity: sha512-Jc/uii1MPevf95/z/W3ufYGHvrFvrtkjxQ8UuXhJCzOgv/FGPqhmA5PH124nLHEgGLBA7zQxHumofhdXosEhUQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -4671,6 +4802,26 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@typescript-eslint/parser/5.51.0_iukboom6ndih5an6iafl45j2fe:
|
||||||
|
resolution: {integrity: sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/scope-manager': 5.51.0
|
||||||
|
'@typescript-eslint/types': 5.51.0
|
||||||
|
'@typescript-eslint/typescript-estree': 5.51.0_typescript@4.9.4
|
||||||
|
debug: 4.3.4
|
||||||
|
eslint: 8.31.0
|
||||||
|
typescript: 4.9.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@typescript-eslint/parser/5.51.0_rmayb2veg2btbq6mbmnyivgasy:
|
/@typescript-eslint/parser/5.51.0_rmayb2veg2btbq6mbmnyivgasy:
|
||||||
resolution: {integrity: sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==}
|
resolution: {integrity: sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
@ -4782,6 +4933,27 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@typescript-eslint/typescript-estree/5.51.0_typescript@4.9.4:
|
||||||
|
resolution: {integrity: sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 5.51.0
|
||||||
|
'@typescript-eslint/visitor-keys': 5.51.0
|
||||||
|
debug: 4.3.4
|
||||||
|
globby: 11.1.0
|
||||||
|
is-glob: 4.0.3
|
||||||
|
semver: 7.3.8
|
||||||
|
tsutils: 3.21.0_typescript@4.9.4
|
||||||
|
typescript: 4.9.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@typescript-eslint/typescript-estree/5.51.0_typescript@4.9.5:
|
/@typescript-eslint/typescript-estree/5.51.0_typescript@4.9.5:
|
||||||
resolution: {integrity: sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==}
|
resolution: {integrity: sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
@ -6396,7 +6568,6 @@ packages:
|
||||||
/define-lazy-prop/2.0.0:
|
/define-lazy-prop/2.0.0:
|
||||||
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/define-properties/1.1.4:
|
/define-properties/1.1.4:
|
||||||
resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
|
resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
|
||||||
|
@ -6562,7 +6733,6 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.10
|
graceful-fs: 4.2.10
|
||||||
tapable: 2.2.1
|
tapable: 2.2.1
|
||||||
dev: false
|
|
||||||
|
|
||||||
/enquirer/2.3.6:
|
/enquirer/2.3.6:
|
||||||
resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
|
resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
|
||||||
|
@ -6735,6 +6905,31 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-config-next/13.1.2_iukboom6ndih5an6iafl45j2fe:
|
||||||
|
resolution: {integrity: sha512-zdRAQOr8v69ZwJRtBrGqAqm160ONqKxU/pV1FB1KlgfyqveGsLZmlQ7l31otwtw763901J7xdiTVkj2y3YxXZA==}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^7.23.0 || ^8.0.0
|
||||||
|
typescript: '>=3.3.1'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@next/eslint-plugin-next': 13.1.2
|
||||||
|
'@rushstack/eslint-patch': 1.2.0
|
||||||
|
'@typescript-eslint/parser': 5.51.0_iukboom6ndih5an6iafl45j2fe
|
||||||
|
eslint: 8.31.0
|
||||||
|
eslint-import-resolver-node: 0.3.7
|
||||||
|
eslint-import-resolver-typescript: 3.5.3_vz4tyq5r7fh66imfi352lmrvhq
|
||||||
|
eslint-plugin-import: 2.27.5_wtpxh6i75teqmr6s6crcj3hmim
|
||||||
|
eslint-plugin-jsx-a11y: 6.7.1_eslint@8.31.0
|
||||||
|
eslint-plugin-react: 7.32.2_eslint@8.31.0
|
||||||
|
eslint-plugin-react-hooks: 4.6.0_eslint@8.31.0
|
||||||
|
typescript: 4.9.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- eslint-import-resolver-webpack
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-config-next/13.1.6_4vsywjlpuriuw3tl5oq6zy5a64:
|
/eslint-config-next/13.1.6_4vsywjlpuriuw3tl5oq6zy5a64:
|
||||||
resolution: {integrity: sha512-0cg7h5wztg/SoLAlxljZ0ZPUQ7i6QKqRiP4M2+MgTZtxWwNKb2JSwNc18nJ6/kXBI6xYvPraTbQSIhAuVw6czw==}
|
resolution: {integrity: sha512-0cg7h5wztg/SoLAlxljZ0ZPUQ7i6QKqRiP4M2+MgTZtxWwNKb2JSwNc18nJ6/kXBI6xYvPraTbQSIhAuVw6czw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -6769,6 +6964,15 @@ packages:
|
||||||
eslint: 8.27.0
|
eslint: 8.27.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-config-prettier/8.6.0_eslint@8.31.0:
|
||||||
|
resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '>=7.0.0'
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.31.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-config-prettier/8.6.0_eslint@8.33.0:
|
/eslint-config-prettier/8.6.0_eslint@8.33.0:
|
||||||
resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==}
|
resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -6834,6 +7038,26 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/eslint-import-resolver-typescript/3.5.3_vz4tyq5r7fh66imfi352lmrvhq:
|
||||||
|
resolution: {integrity: sha512-njRcKYBc3isE42LaTcJNVANR3R99H9bAxBDMNDr2W7yq5gYPxbU3MkdhsQukxZ/Xg9C2vcyLlDsbKfRDg0QvCQ==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '*'
|
||||||
|
eslint-plugin-import: '*'
|
||||||
|
dependencies:
|
||||||
|
debug: 4.3.4
|
||||||
|
enhanced-resolve: 5.12.0
|
||||||
|
eslint: 8.31.0
|
||||||
|
eslint-plugin-import: 2.27.5_wtpxh6i75teqmr6s6crcj3hmim
|
||||||
|
get-tsconfig: 4.4.0
|
||||||
|
globby: 13.1.3
|
||||||
|
is-core-module: 2.11.0
|
||||||
|
is-glob: 4.0.3
|
||||||
|
synckit: 0.8.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-module-utils/2.7.4_ayntagxiesqd2engtsqrvzzbfq:
|
/eslint-module-utils/2.7.4_ayntagxiesqd2engtsqrvzzbfq:
|
||||||
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
|
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -6864,6 +7088,36 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-module-utils/2.7.4_hjbla6irz2mheatr6l363y7mca:
|
||||||
|
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
peerDependencies:
|
||||||
|
'@typescript-eslint/parser': '*'
|
||||||
|
eslint: '*'
|
||||||
|
eslint-import-resolver-node: '*'
|
||||||
|
eslint-import-resolver-typescript: '*'
|
||||||
|
eslint-import-resolver-webpack: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@typescript-eslint/parser':
|
||||||
|
optional: true
|
||||||
|
eslint:
|
||||||
|
optional: true
|
||||||
|
eslint-import-resolver-node:
|
||||||
|
optional: true
|
||||||
|
eslint-import-resolver-typescript:
|
||||||
|
optional: true
|
||||||
|
eslint-import-resolver-webpack:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/parser': 5.51.0_iukboom6ndih5an6iafl45j2fe
|
||||||
|
debug: 3.2.7
|
||||||
|
eslint: 8.31.0
|
||||||
|
eslint-import-resolver-node: 0.3.7
|
||||||
|
eslint-import-resolver-typescript: 3.5.3_vz4tyq5r7fh66imfi352lmrvhq
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-module-utils/2.7.4_wj7ubv6viehxm3sdjw6f37lxha:
|
/eslint-module-utils/2.7.4_wj7ubv6viehxm3sdjw6f37lxha:
|
||||||
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
|
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -6960,6 +7214,39 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/eslint-plugin-import/2.27.5_wtpxh6i75teqmr6s6crcj3hmim:
|
||||||
|
resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
peerDependencies:
|
||||||
|
'@typescript-eslint/parser': '*'
|
||||||
|
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@typescript-eslint/parser':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/parser': 5.51.0_iukboom6ndih5an6iafl45j2fe
|
||||||
|
array-includes: 3.1.6
|
||||||
|
array.prototype.flat: 1.3.1
|
||||||
|
array.prototype.flatmap: 1.3.1
|
||||||
|
debug: 3.2.7
|
||||||
|
doctrine: 2.1.0
|
||||||
|
eslint: 8.31.0
|
||||||
|
eslint-import-resolver-node: 0.3.7
|
||||||
|
eslint-module-utils: 2.7.4_hjbla6irz2mheatr6l363y7mca
|
||||||
|
has: 1.0.3
|
||||||
|
is-core-module: 2.11.0
|
||||||
|
is-glob: 4.0.3
|
||||||
|
minimatch: 3.1.2
|
||||||
|
object.values: 1.1.6
|
||||||
|
resolve: 1.22.1
|
||||||
|
semver: 6.3.0
|
||||||
|
tsconfig-paths: 3.14.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- eslint-import-resolver-typescript
|
||||||
|
- eslint-import-resolver-webpack
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-jsx-a11y/6.7.1_eslint@8.27.0:
|
/eslint-plugin-jsx-a11y/6.7.1_eslint@8.27.0:
|
||||||
resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==}
|
resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
@ -6985,6 +7272,31 @@ packages:
|
||||||
semver: 6.3.0
|
semver: 6.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-plugin-jsx-a11y/6.7.1_eslint@8.31.0:
|
||||||
|
resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.20.13
|
||||||
|
aria-query: 5.1.3
|
||||||
|
array-includes: 3.1.6
|
||||||
|
array.prototype.flatmap: 1.3.1
|
||||||
|
ast-types-flow: 0.0.7
|
||||||
|
axe-core: 4.6.3
|
||||||
|
axobject-query: 3.1.1
|
||||||
|
damerau-levenshtein: 1.0.8
|
||||||
|
emoji-regex: 9.2.2
|
||||||
|
eslint: 8.31.0
|
||||||
|
has: 1.0.3
|
||||||
|
jsx-ast-utils: 3.3.3
|
||||||
|
language-tags: 1.0.5
|
||||||
|
minimatch: 3.1.2
|
||||||
|
object.entries: 1.1.6
|
||||||
|
object.fromentries: 2.0.6
|
||||||
|
semver: 6.3.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-jsx-a11y/6.7.1_eslint@8.33.0:
|
/eslint-plugin-jsx-a11y/6.7.1_eslint@8.33.0:
|
||||||
resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==}
|
resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
@ -7019,6 +7331,15 @@ packages:
|
||||||
eslint: 8.27.0
|
eslint: 8.27.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-plugin-react-hooks/4.6.0_eslint@8.31.0:
|
||||||
|
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.31.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-react-hooks/4.6.0_eslint@8.33.0:
|
/eslint-plugin-react-hooks/4.6.0_eslint@8.33.0:
|
||||||
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
|
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -7052,6 +7373,30 @@ packages:
|
||||||
string.prototype.matchall: 4.0.8
|
string.prototype.matchall: 4.0.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-plugin-react/7.32.2_eslint@8.31.0:
|
||||||
|
resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
|
||||||
|
dependencies:
|
||||||
|
array-includes: 3.1.6
|
||||||
|
array.prototype.flatmap: 1.3.1
|
||||||
|
array.prototype.tosorted: 1.1.1
|
||||||
|
doctrine: 2.1.0
|
||||||
|
eslint: 8.31.0
|
||||||
|
estraverse: 5.3.0
|
||||||
|
jsx-ast-utils: 3.3.3
|
||||||
|
minimatch: 3.1.2
|
||||||
|
object.entries: 1.1.6
|
||||||
|
object.fromentries: 2.0.6
|
||||||
|
object.hasown: 1.1.2
|
||||||
|
object.values: 1.1.6
|
||||||
|
prop-types: 15.8.1
|
||||||
|
resolve: 2.0.0-next.4
|
||||||
|
semver: 6.3.0
|
||||||
|
string.prototype.matchall: 4.0.8
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-react/7.32.2_eslint@8.33.0:
|
/eslint-plugin-react/7.32.2_eslint@8.33.0:
|
||||||
resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==}
|
resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -7128,6 +7473,16 @@ packages:
|
||||||
eslint-visitor-keys: 2.1.0
|
eslint-visitor-keys: 2.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-utils/3.0.0_eslint@8.31.0:
|
||||||
|
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
|
||||||
|
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '>=5'
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.31.0
|
||||||
|
eslint-visitor-keys: 2.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-utils/3.0.0_eslint@8.33.0:
|
/eslint-utils/3.0.0_eslint@8.33.0:
|
||||||
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
|
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
|
||||||
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
|
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
|
||||||
|
@ -7237,6 +7592,54 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint/8.31.0:
|
||||||
|
resolution: {integrity: sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
'@eslint/eslintrc': 1.4.1
|
||||||
|
'@humanwhocodes/config-array': 0.11.8
|
||||||
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
|
'@nodelib/fs.walk': 1.2.8
|
||||||
|
ajv: 6.12.6
|
||||||
|
chalk: 4.1.2
|
||||||
|
cross-spawn: 7.0.3
|
||||||
|
debug: 4.3.4
|
||||||
|
doctrine: 3.0.0
|
||||||
|
escape-string-regexp: 4.0.0
|
||||||
|
eslint-scope: 7.1.1
|
||||||
|
eslint-utils: 3.0.0_eslint@8.31.0
|
||||||
|
eslint-visitor-keys: 3.3.0
|
||||||
|
espree: 9.4.1
|
||||||
|
esquery: 1.4.0
|
||||||
|
esutils: 2.0.3
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
file-entry-cache: 6.0.1
|
||||||
|
find-up: 5.0.0
|
||||||
|
glob-parent: 6.0.2
|
||||||
|
globals: 13.20.0
|
||||||
|
grapheme-splitter: 1.0.4
|
||||||
|
ignore: 5.2.4
|
||||||
|
import-fresh: 3.3.0
|
||||||
|
imurmurhash: 0.1.4
|
||||||
|
is-glob: 4.0.3
|
||||||
|
is-path-inside: 3.0.3
|
||||||
|
js-sdsl: 4.3.0
|
||||||
|
js-yaml: 4.1.0
|
||||||
|
json-stable-stringify-without-jsonify: 1.0.1
|
||||||
|
levn: 0.4.1
|
||||||
|
lodash.merge: 4.6.2
|
||||||
|
minimatch: 3.1.2
|
||||||
|
natural-compare: 1.4.0
|
||||||
|
optionator: 0.9.1
|
||||||
|
regexpp: 3.2.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
strip-json-comments: 3.1.1
|
||||||
|
text-table: 0.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint/8.33.0:
|
/eslint/8.33.0:
|
||||||
resolution: {integrity: sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==}
|
resolution: {integrity: sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
@ -7432,6 +7835,13 @@ packages:
|
||||||
strnum: 1.0.5
|
strnum: 1.0.5
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/fast-xml-parser/4.1.2:
|
||||||
|
resolution: {integrity: sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
strnum: 1.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fastest-stable-stringify/2.0.2:
|
/fastest-stable-stringify/2.0.2:
|
||||||
resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==}
|
resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -7761,7 +8171,6 @@ packages:
|
||||||
|
|
||||||
/get-tsconfig/4.4.0:
|
/get-tsconfig/4.4.0:
|
||||||
resolution: {integrity: sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==}
|
resolution: {integrity: sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/glob-parent/5.1.2:
|
/glob-parent/5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
|
@ -7824,7 +8233,6 @@ packages:
|
||||||
|
|
||||||
/globalyzer/0.1.0:
|
/globalyzer/0.1.0:
|
||||||
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
|
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/globby/11.1.0:
|
/globby/11.1.0:
|
||||||
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
||||||
|
@ -7846,11 +8254,9 @@ packages:
|
||||||
ignore: 5.2.4
|
ignore: 5.2.4
|
||||||
merge2: 1.4.1
|
merge2: 1.4.1
|
||||||
slash: 4.0.0
|
slash: 4.0.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/globrex/0.1.2:
|
/globrex/0.1.2:
|
||||||
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/gopd/1.0.1:
|
/gopd/1.0.1:
|
||||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||||
|
@ -8397,7 +8803,6 @@ packages:
|
||||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
|
||||||
|
|
||||||
/is-extglob/2.1.1:
|
/is-extglob/2.1.1:
|
||||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
|
@ -8603,7 +9008,6 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
dev: false
|
|
||||||
|
|
||||||
/isarray/1.0.0:
|
/isarray/1.0.0:
|
||||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
@ -9536,6 +9940,17 @@ packages:
|
||||||
urql: 3.0.3_onqnqwb3ubg5opvemcqf7c2qhy
|
urql: 3.0.3_onqnqwb3ubg5opvemcqf7c2qhy
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/next-urql/4.0.3_react@18.2.0+urql@3.0.3:
|
||||||
|
resolution: {integrity: sha512-pesvwu1ZuGzMla8tPMo0V0yiV3ObDF4dbZyZLB2rZoORy+ebdWtClU/pfz1XDrPEgzyfGC3tqvbR5gH7Kt59XA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
urql: ^3.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-ssr-prepass: 1.5.0_react@18.2.0
|
||||||
|
urql: 3.0.3_onqnqwb3ubg5opvemcqf7c2qhy
|
||||||
|
dev: false
|
||||||
|
|
||||||
/next/13.1.0_biqbaboplfbrettd7655fr4n2y:
|
/next/13.1.0_biqbaboplfbrettd7655fr4n2y:
|
||||||
resolution: {integrity: sha512-lQMZH1V94L5IL/WaihQkTYabSY73aqgrkGPJB5uz+2O3ES4I3losV/maXLY7l7x5e+oNyE9N81upNQ8uRsR5/A==}
|
resolution: {integrity: sha512-lQMZH1V94L5IL/WaihQkTYabSY73aqgrkGPJB5uz+2O3ES4I3losV/maXLY7l7x5e+oNyE9N81upNQ8uRsR5/A==}
|
||||||
engines: {node: '>=14.6.0'}
|
engines: {node: '>=14.6.0'}
|
||||||
|
@ -9939,7 +10354,6 @@ packages:
|
||||||
define-lazy-prop: 2.0.0
|
define-lazy-prop: 2.0.0
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
is-wsl: 2.2.0
|
is-wsl: 2.2.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/optionator/0.8.3:
|
/optionator/0.8.3:
|
||||||
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
|
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
|
||||||
|
@ -10140,6 +10554,10 @@ packages:
|
||||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
/pathe/0.2.0:
|
||||||
|
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/pathe/1.1.0:
|
/pathe/1.1.0:
|
||||||
resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==}
|
resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==}
|
||||||
|
|
||||||
|
@ -10593,6 +11011,10 @@ packages:
|
||||||
/react-is/17.0.2:
|
/react-is/17.0.2:
|
||||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||||
|
|
||||||
|
/react-is/18.2.0:
|
||||||
|
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-lifecycles-compat/3.0.4:
|
/react-lifecycles-compat/3.0.4:
|
||||||
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
|
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -11235,7 +11657,6 @@ packages:
|
||||||
/slash/4.0.0:
|
/slash/4.0.0:
|
||||||
resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
|
resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/slice-ansi/0.0.4:
|
/slice-ansi/0.0.4:
|
||||||
resolution: {integrity: sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw==}
|
resolution: {integrity: sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw==}
|
||||||
|
@ -11616,12 +12037,10 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@pkgr/utils': 2.3.1
|
'@pkgr/utils': 2.3.1
|
||||||
tslib: 2.5.0
|
tslib: 2.5.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tapable/2.2.1:
|
/tapable/2.2.1:
|
||||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tar-stream/2.2.0:
|
/tar-stream/2.2.0:
|
||||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
|
@ -11675,7 +12094,6 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
globalyzer: 0.1.0
|
globalyzer: 0.1.0
|
||||||
globrex: 0.1.2
|
globrex: 0.1.2
|
||||||
dev: false
|
|
||||||
|
|
||||||
/tiny-inflate/1.0.3:
|
/tiny-inflate/1.0.3:
|
||||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||||
|
@ -11856,6 +12274,16 @@ packages:
|
||||||
typescript: 4.8.4
|
typescript: 4.8.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tsutils/3.21.0_typescript@4.9.4:
|
||||||
|
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
|
||||||
|
dependencies:
|
||||||
|
tslib: 1.14.1
|
||||||
|
typescript: 4.9.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tsutils/3.21.0_typescript@4.9.5:
|
/tsutils/3.21.0_typescript@4.9.5:
|
||||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -11996,6 +12424,12 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/typescript/4.9.4:
|
||||||
|
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
|
||||||
|
engines: {node: '>=4.2.0'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/typescript/4.9.5:
|
/typescript/4.9.5:
|
||||||
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
|
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
|
||||||
engines: {node: '>=4.2.0'}
|
engines: {node: '>=4.2.0'}
|
||||||
|
@ -12250,6 +12684,29 @@ packages:
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/vite-node/0.27.3_@types+node@18.13.0:
|
||||||
|
resolution: {integrity: sha512-eyJYOO64o5HIp8poc4bJX+ZNBwMZeI3f6/JdiUmJgW02Mt7LnoCtDMRVmLaY9S05SIsjGe339ZK4uo2wQ+bF9g==}
|
||||||
|
engines: {node: '>=v14.16.0'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
cac: 6.7.14
|
||||||
|
debug: 4.3.4
|
||||||
|
mlly: 1.1.0
|
||||||
|
pathe: 0.2.0
|
||||||
|
picocolors: 1.0.0
|
||||||
|
source-map: 0.6.1
|
||||||
|
source-map-support: 0.5.21
|
||||||
|
vite: 4.1.1_@types+node@18.13.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- less
|
||||||
|
- sass
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
dev: false
|
||||||
|
|
||||||
/vite-node/0.28.4_@types+node@18.13.0:
|
/vite-node/0.28.4_@types+node@18.13.0:
|
||||||
resolution: {integrity: sha512-KM0Q0uSG/xHHKOJvVHc5xDBabgt0l70y7/lWTR7Q0pR5/MrYxadT+y32cJOE65FfjGmJgxpVEEY+69btJgcXOQ==}
|
resolution: {integrity: sha512-KM0Q0uSG/xHHKOJvVHc5xDBabgt0l70y7/lWTR7Q0pR5/MrYxadT+y32cJOE65FfjGmJgxpVEEY+69btJgcXOQ==}
|
||||||
engines: {node: '>=v14.16.0'}
|
engines: {node: '>=v14.16.0'}
|
||||||
|
@ -12305,6 +12762,57 @@ packages:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
|
|
||||||
|
/vitest/0.27.3_jsdom@20.0.3:
|
||||||
|
resolution: {integrity: sha512-Ld3UVgRVhJUtqvQ3dW89GxiApFAgBsWJZBCWzK+gA3w2yG68csXlGZZ4WDJURf+8ecNfgrScga6xY+8YSOpiMg==}
|
||||||
|
engines: {node: '>=v14.16.0'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@vitest/browser': '*'
|
||||||
|
'@vitest/ui': '*'
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/chai': 4.3.4
|
||||||
|
'@types/chai-subset': 1.3.3
|
||||||
|
'@types/node': 18.13.0
|
||||||
|
acorn: 8.8.2
|
||||||
|
acorn-walk: 8.2.0
|
||||||
|
cac: 6.7.14
|
||||||
|
chai: 4.3.7
|
||||||
|
debug: 4.3.4
|
||||||
|
jsdom: 20.0.3
|
||||||
|
local-pkg: 0.4.3
|
||||||
|
picocolors: 1.0.0
|
||||||
|
source-map: 0.6.1
|
||||||
|
std-env: 3.3.2
|
||||||
|
strip-literal: 1.0.1
|
||||||
|
tinybench: 2.3.1
|
||||||
|
tinypool: 0.3.1
|
||||||
|
tinyspy: 1.1.1
|
||||||
|
vite: 4.1.1_@types+node@18.13.0
|
||||||
|
vite-node: 0.27.3_@types+node@18.13.0
|
||||||
|
why-is-node-running: 2.2.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- less
|
||||||
|
- sass
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
dev: false
|
||||||
|
|
||||||
/vitest/0.28.4_jsdom@20.0.3:
|
/vitest/0.28.4_jsdom@20.0.3:
|
||||||
resolution: {integrity: sha512-sfWIy0AdlbyGRhunm+TLQEJrFH9XuRPdApfubsyLcDbCRrUX717BRQKInTgzEfyl2Ipi1HWoHB84Nqtcwxogcg==}
|
resolution: {integrity: sha512-sfWIy0AdlbyGRhunm+TLQEJrFH9XuRPdApfubsyLcDbCRrUX717BRQKInTgzEfyl2Ipi1HWoHB84Nqtcwxogcg==}
|
||||||
engines: {node: '>=v14.16.0'}
|
engines: {node: '>=v14.16.0'}
|
||||||
|
|
18
turbo.json
18
turbo.json
|
@ -72,6 +72,24 @@
|
||||||
"NEXT_PUBLIC_VERCEL_ENV"
|
"NEXT_PUBLIC_VERCEL_ENV"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"build#saleor-app-products-feed": {
|
||||||
|
"env": [
|
||||||
|
"APL",
|
||||||
|
"APP_DEBUG",
|
||||||
|
"NODE_ENV",
|
||||||
|
"SECRET_KEY",
|
||||||
|
"ALLOWED_DOMAIN_PATTERN",
|
||||||
|
"REST_APL_ENDPOINT",
|
||||||
|
"REST_APL_TOKEN",
|
||||||
|
"NEXT_PUBLIC_SENTRY_DSN",
|
||||||
|
"SENTRY_DSN",
|
||||||
|
"NEXT_PUBLIC_SENTRY_DSN",
|
||||||
|
"SENTRY_ORG",
|
||||||
|
"SENTRY_PROJECT",
|
||||||
|
"SENTRY_AUTH_TOKEN",
|
||||||
|
"NEXT_PUBLIC_VERCEL_ENV"
|
||||||
|
]
|
||||||
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue