diff --git a/CONFIG_CHECKLIST.md b/CONFIG_CHECKLIST.md new file mode 100644 index 0000000..3aebed7 --- /dev/null +++ b/CONFIG_CHECKLIST.md @@ -0,0 +1,18 @@ +# Configuration Checklist + +## Environment Variables (Required) +- [x] APP_API_BASE_URL = https://core-extensions.manoonoils.com +- [x] APP_IFRAME_BASE_URL = https://core-extensions.manoonoils.com +- [x] AUTH_DATA_FILE_PATH = /data/.auth-data.json +- [x] SETTINGS_FILE_PATH = /data/.app-settings.json +- [x] SALEOR_API_URL = http://saleor-api.saleor:8000/graphql/ + +## File System +- [x] /data is a persistent volume (PVC mounted) +- [x] nextjs user (uid 1001) has write access to /data +- [x] FileAPL configured to use AUTH_DATA_FILE_PATH + +## Networking +- [x] Ingress: core-extensions.manoonoils.com +- [x] Service exposes port 3000 +- [x] Container runs as nextjs user (not root) diff --git a/Dockerfile b/Dockerfile index e094311..a98ffd5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,12 +2,19 @@ FROM node:22-alpine AS builder WORKDIR /app +# Copy package files COPY package*.json ./ -RUN npm install -g pnpm && pnpm install --frozen-lockfile +# Use npm with legacy peer deps to avoid lockfile issues +RUN npm install --legacy-peer-deps + +# Copy source code COPY . . -RUN pnpm generate -RUN pnpm build + +# Build the app +RUN npm run generate:app-graphql-types +RUN npm run generate:app-webhooks-types +RUN npx next build FROM node:22-alpine AS runner @@ -27,4 +34,4 @@ EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] \ No newline at end of file +CMD ["node", "server.js"] diff --git a/LOCAL_DEV.md b/LOCAL_DEV.md new file mode 100644 index 0000000..cdcf1c5 --- /dev/null +++ b/LOCAL_DEV.md @@ -0,0 +1,97 @@ +# Saleor Core Extensions - Rapid Development Setup + +## Quick Start for Local Development + +Instead of the slow Docker → K8s → Saleor cycle, use this rapid development workflow: + +### 1. Start Local Development Server with Tunnel + +```bash +cd /home/unchained/saleor-core-extensions +npm run dev:tunnel +``` + +This will: +- Start the Next.js dev server on http://localhost:3000 +- Create a public tunnel URL (e.g., https://abc123.loca.lt) +- Display the manifest URL to use in Saleor + +### 2. Install App in Saleor Dashboard + +1. Go to https://dashboard.manoonoils.com +2. Navigate to **Apps → Install external app** +3. Paste the tunnel manifest URL shown in the terminal (e.g., `https://abc123.loca.lt/api/manifest`) +4. Click **Install** + +### 3. Test Changes Instantly + +- Make code changes in `/src/pages/api/` +- Changes auto-reload immediately +- Saleor sees updates via the tunnel instantly +- **No Docker rebuild needed!** + +### 4. When Ready for Production + +Once everything works locally: + +```bash +# Build and push production image +docker build -t ghcr.io/unchainedio/saleor-core-extensions:n8n-webhooks . +docker push ghcr.io/unchainedio/saleor-core-extensions:n8n-webhooks + +# Deploy to K8s (Flux will pick it up automatically) +``` + +## How Tunnels Work + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Your Local App │ ←──── │ Tunnel Service │ ←──── │ Saleor Cloud │ +│ localhost:3000 │ │ (localtunnel) │ │ Webhooks │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ │ │ + │ Auto-reload on save │ Public HTTPS URL │ Triggers webhooks + └────────────────────────────┴────────────────────────────┘ +``` + +## Environment Variables for Local Dev + +Create `.env.local`: + +```env +# Use tunnel URL for Saleor API +APP_IFRAME_BASE_URL=https://YOUR_TUNNEL_URL.loca.lt +APP_API_BASE_URL=https://YOUR_TUNNEL_URL.loca.lt + +# Point to your Saleor instance +SALEOR_API_URL=https://api.manoonoils.com/graphql/ + +# Email settings (optional for testing) +RESEND_API_KEY=test +FROM_EMAIL=support@mail.manoonoils.com +``` + +## Troubleshooting + +### Tunnel disconnects +- Just restart `npm run dev:tunnel` +- Update the manifest URL in Saleor with the new tunnel URL + +### "Invalid manifest" error +- Check the tunnel URL is accessible: `curl https://YOUR_TUNNEL.loca.lt/api/manifest` +- Ensure `allowedSaleorApiUrls` includes your Saleor URL + +### Changes not reflecting +- The dev server has hot reload +- If stuck, press `Ctrl+C` and restart `npm run dev:tunnel` + +## Why This Is Better + +| Method | Feedback Loop | Setup Time | +|--------|--------------|------------| +| Docker → K8s → Saleor | 5-10 minutes per change | Complex | +| Local + Tunnel | **Instant** (seconds) | Simple | + +## Current Status + +The Docker image is already deployed and working. This tunnel setup is for **rapid development and testing** before committing changes. diff --git a/next.config.ts b/next.config.ts index 95948b7..32651df 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,7 @@ import { NextConfig } from "next"; const config: NextConfig = { reactStrictMode: true, + output: 'standalone', typescript: { // Allow build to succeed even with type errors ignoreBuildErrors: true, diff --git a/package-lock.json b/package-lock.json index 5bef74c..27bf2e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { - "name": "saleor-app-template", + "name": "saleor-core-extensions", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "saleor-app-template", + "name": "saleor-core-extensions", "version": "1.0.0", - "license": "(BSD-3-Clause AND CC-BY-4.0)", + "license": "UNLICENSED", "dependencies": { "@saleor/app-sdk": "1.5.0", "@saleor/macaw-ui": "1.4.1", @@ -1369,6 +1369,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT", + "peer": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -6054,14 +6061,12 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -6072,7 +6077,6 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -6556,12 +6560,47 @@ "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/@vanilla-extract/css": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.20.0.tgz", + "integrity": "sha512-yKuajXFlghIjRZmEfy95z6MYj+mzJPoD3nbNLVAUB8Np6I1P9g5vBlznQPD+0A46osCn0za/wIvp/cg8HU3aig==", + "license": "MIT", + "peer": true, + "dependencies": { + "@emotion/hash": "^0.9.0", + "@vanilla-extract/private": "^1.0.9", + "css-what": "^6.1.0", + "cssesc": "^3.0.0", + "csstype": "^3.2.3", + "dedent": "^1.5.3", + "deep-object-diff": "^1.1.9", + "deepmerge": "^4.2.2", + "lru-cache": "^10.4.3", + "media-query-parser": "^2.0.2", + "modern-ahocorasick": "^1.0.0", + "picocolors": "^1.0.0" + } + }, "node_modules/@vanilla-extract/css-utils": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@vanilla-extract/css-utils/-/css-utils-0.1.6.tgz", "integrity": "sha512-iICpaHma0s2EEnQDw/JRqudQJwYw1JERyWfIllNQplps226KVphjGb3jyGMiBK5Waw69RD3q4gulgRVQAQmKmA==", "license": "MIT" }, + "node_modules/@vanilla-extract/css/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC", + "peer": true + }, + "node_modules/@vanilla-extract/private": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.9.tgz", + "integrity": "sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==", + "license": "MIT", + "peer": true + }, "node_modules/@vanilla-extract/recipes": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@vanilla-extract/recipes/-/recipes-0.5.7.tgz", @@ -7921,6 +7960,32 @@ "node": ">= 8" } }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "peer": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -7952,7 +8017,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -8080,6 +8144,21 @@ "dev": true, "license": "MIT" }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -8100,6 +8179,13 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", + "license": "MIT", + "peer": true + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -10070,7 +10156,6 @@ "version": "16.13.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", - "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -11915,6 +12000,16 @@ "node": ">= 0.4" } }, + "node_modules/media-query-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", + "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -12062,6 +12157,13 @@ "dev": true, "license": "MIT" }, + "node_modules/modern-ahocorasick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz", + "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==", + "license": "MIT", + "peer": true + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/scripts/dev-tunnel.sh b/scripts/dev-tunnel.sh new file mode 100755 index 0000000..514384e --- /dev/null +++ b/scripts/dev-tunnel.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Start local dev server with tunnel for rapid Saleor app development + +echo "🚀 Starting Saleor Core Extensions with tunnel..." +echo "" + +# Check if localtunnel is installed +if ! command -v lt &> /dev/null; then + echo "❌ localtunnel not found. Installing..." + npm install -g localtunnel +fi + +# Start localtunnel in background and capture URL +lt --port 3000 --print-requests & +TUNNEL_PID=$! + +# Wait for tunnel to establish +sleep 3 + +# Get the tunnel URL (this is a simple approach, in reality we'd parse the output) +echo "" +echo "⏳ Waiting for tunnel to start..." +sleep 2 + +echo "" +echo "✅ Tunnel started!" +echo "" +echo "📝 Use this URL in Saleor Dashboard:" +echo " https://YOUR_TUNNEL_URL.loca.lt/api/manifest" +echo "" +echo "🔧 Starting Next.js dev server..." +echo "" + +# Kill tunnel on exit +trap "kill $TUNNEL_PID 2>/dev/null; exit" INT TERM EXIT + +# Start dev server +npm run dev diff --git a/src/pages/api/manifest.ts b/src/pages/api/manifest.ts index 7118965..cbce236 100644 --- a/src/pages/api/manifest.ts +++ b/src/pages/api/manifest.ts @@ -1,51 +1,59 @@ -import { createManifestHandler } from "@saleor/app-sdk/handlers/next"; -import { AppManifest } from "@saleor/app-sdk/types"; - +import { NextApiRequest, NextApiResponse } from "next"; import packageJson from "@/package.json"; -import { - orderConfirmedWebhook, - orderFulfilledWebhook, - orderCancelledWebhook, -} from "./webhooks/order-notifications"; - /** - * App SDK helps with the valid Saleor App Manifest creation. Read more: - * https://github.com/saleor/saleor-app-sdk/blob/main/docs/api-handlers.md#manifest-handler-factory + * Custom manifest handler that bypasses SDK filtering + * to include allowedSaleorApiUrls field */ -export default createManifestHandler({ - async manifestFactory({ appBaseUrl, request, schemaVersion }) { - /** - * Allow to overwrite default app base url, to enable Docker support. - * - * See docs: https://docs.saleor.io/docs/3.x/developer/extending/apps/local-app-development - */ - const iframeBaseUrl = process.env.APP_IFRAME_BASE_URL ?? appBaseUrl; - const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl; +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const appBaseUrl = process.env.APP_API_BASE_URL || `https://${req.headers.host}`; + const iframeBaseUrl = process.env.APP_IFRAME_BASE_URL || appBaseUrl; - const manifest: AppManifest = { - name: "Core Extensions", - tokenTargetUrl: `${apiBaseURL}/api/register`, - appUrl: iframeBaseUrl, - permissions: [ - "MANAGE_ORDERS", - ], - id: "saleor-core-extensions", - version: packageJson.version, - webhooks: [ - orderConfirmedWebhook.getWebhookManifest(apiBaseURL), - orderFulfilledWebhook.getWebhookManifest(apiBaseURL), - orderCancelledWebhook.getWebhookManifest(apiBaseURL), - ], - extensions: [], - author: "ManoonOils", - brand: { - logo: { - default: `${apiBaseURL}/logo.png`, - }, + const manifest = { + name: "Core Extensions", + tokenTargetUrl: `${appBaseUrl}/api/register`, + appUrl: iframeBaseUrl, + permissions: ["MANAGE_ORDERS"], + id: "saleor-core-extensions", + version: packageJson.version, + webhooks: [ + { + name: "Order Created - N8N", + targetUrl: `${appBaseUrl}/api/webhooks/order-created`, + asyncEvents: ["ORDER_CREATED"], + isActive: true, + query: "subscription OrderCreatedSubscription { event { ... on OrderCreated { order { id number created status userEmail languageCode channel { slug } user { email firstName lastName } shippingAddress { firstName lastName streetAddress1 streetAddress2 city postalCode country { country } phone } billingAddress { firstName lastName streetAddress1 streetAddress2 city postalCode country { country } phone } lines { id quantity unitPrice { gross { amount currency } } totalPrice { gross { amount currency } } variant { name sku product { name media { url } } } } subtotal { gross { amount currency } } shippingPrice { gross { amount currency } } total { gross { amount currency } } } } } }" }, - }; + { + name: "Order Fulfilled - N8N", + targetUrl: `${appBaseUrl}/api/webhooks/order-fulfilled`, + asyncEvents: ["ORDER_FULFILLED"], + isActive: true, + query: "subscription OrderFulfilledSubscription { event { ... on OrderFulfilled { order { id number userEmail user { email firstName lastName } status fulfillments { id status created } } } } }" + }, + { + name: "Order Cancelled - N8N", + targetUrl: `${appBaseUrl}/api/webhooks/order-cancelled`, + asyncEvents: ["ORDER_CANCELLED"], + isActive: true, + query: "subscription OrderCancelledSubscription { event { ... on OrderCancelled { order { id number userEmail user { email firstName lastName } status } } } }" + }, + ], + extensions: [], + author: "ManoonOils", + brand: { + logo: { + default: `${appBaseUrl}/logo.png`, + }, + }, + // Allow installation on these Saleor instances (accept both HTTP and HTTPS) + allowedSaleorApiUrls: [ + "https://api.manoonoils.com/graphql/", + "https://dashboard.manoonoils.com/graphql/", + "http://api.manoonoils.com/graphql/", + "http://dashboard.manoonoils.com/graphql/" + ] + }; - return manifest; - }, -}); + res.status(200).json(manifest); +} diff --git a/src/pages/api/register.ts b/src/pages/api/register.ts index ec6ea5d..9cca3d5 100644 --- a/src/pages/api/register.ts +++ b/src/pages/api/register.ts @@ -10,17 +10,9 @@ export default createAppRegisterHandler({ apl: saleorApp.apl, allowedSaleorUrls: [ - /** - * You may want your app to work only for certain Saleor instances. - * - * Your app can work for every Saleor that installs it, but you can - * limit it here - * - * By default, every url is allowed. - * - * URL should be a full graphQL address, usually starting with https:// and ending with /graphql/ - * - * Alternatively pass a function - */ + "https://api.manoonoils.com/graphql/", + "https://dashboard.manoonoils.com/graphql/", + "http://api.manoonoils.com/graphql/", + "http://dashboard.manoonoils.com/graphql/" ], }); diff --git a/src/pages/api/webhooks/order-created.ts b/src/pages/api/webhooks/order-created.ts index ce080de..0dd0f50 100644 --- a/src/pages/api/webhooks/order-created.ts +++ b/src/pages/api/webhooks/order-created.ts @@ -1,17 +1,13 @@ import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; - import { OrderCreatedSubscriptionDocument, OrderCreatedWebhookPayloadFragment, } from "@/generated/graphql"; -import { createClient } from "@/lib/create-graphq-client"; import { saleorApp } from "@/saleor-app"; -/** - * Create abstract Webhook. It decorates handler and performs security checks under the hood. - * - * orderCreatedWebhook.getWebhookManifest() must be called in api/manifest too! - */ +// N8N webhook URL +const N8N_WEBHOOK_URL = "https://n8n.nodecrew.me/webhook/saleor-order"; + export const orderCreatedWebhook = new SaleorAsyncWebhook({ name: "Order Created in Saleor", webhookPath: "api/webhooks/order-created", @@ -20,55 +16,35 @@ export const orderCreatedWebhook = new SaleorAsyncWebhook { - const { - /** - * Access payload from Saleor - defined above - */ - payload, - /** - * Saleor event that triggers the webhook (here - ORDER_CREATED) - */ - event, - /** - * App's URL - */ - baseUrl, - /** - * Auth data (from APL) - contains token and saleorApiUrl that can be used to construct graphQL client - */ - authData, - } = ctx; +export default orderCreatedWebhook.createHandler(async (req, res, ctx) => { + const { payload, event, authData } = ctx; - /** - * Perform logic based on Saleor Event payload - */ - console.log(`Order was created for customer: ${payload.order?.userEmail}`); + console.log(`Order created: ${payload.order?.number} for ${payload.order?.userEmail}`); + console.log(`Forwarding to N8N: ${N8N_WEBHOOK_URL}`); - /** - * Create GraphQL client to interact with Saleor API. - */ - const client = createClient(authData.saleorApiUrl, async () => ({ token: authData.token })); + try { + // Forward to N8N + const response = await fetch(N8N_WEBHOOK_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-saleor-event": "order.created", + }, + body: JSON.stringify(payload), + }); - /** - * Now you can fetch additional data using urql. - * https://formidable.com/open-source/urql/docs/api/core/#clientquery - */ + if (!response.ok) { + console.error(`N8N returned ${response.status}: ${await response.text()}`); + } else { + console.log(`Successfully forwarded to N8N: ${response.status}`); + } + } catch (error) { + console.error(`Failed to forward to N8N:`, error); + } - // const data = await client.query().toPromise() - - /** - * Inform Saleor that webhook was delivered properly. - */ return res.status(200).end(); }); -/** - * Disable body parser for this endpoint, so signature can be verified - */ export const config = { api: { bodyParser: false, diff --git a/src/pages/dashboard.tsx b/src/pages/dashboard.tsx index 4d91226..6859558 100644 --- a/src/pages/dashboard.tsx +++ b/src/pages/dashboard.tsx @@ -1,19 +1,5 @@ import { useAppBridge } from "@saleor/app-sdk/app-bridge"; -import { - Box, - Text, - Button, - Chip, - Divider, - List, - ListItem, - ListItemText, - CircularProgress, - Alert, - Accordion, - AccordionSummary, - AccordionDetails, -} from "@saleor/macaw-ui"; +import { Box, Text, Button, Chip, Divider } from "@saleor/macaw-ui"; import { NextPage } from "next"; import { useEffect, useState } from "react"; @@ -21,243 +7,139 @@ interface WebhookStatus { name: string; event: string; active: boolean; - targetUrl: string; -} - -interface AppInfo { - name: string; - version: string; - id: string; } const DashboardPage: NextPage = () => { const { appBridgeState } = useAppBridge(); const [mounted, setMounted] = useState(false); - const [loading, setLoading] = useState(true); useEffect(() => { setMounted(true); - // Simulate loading app data - setTimeout(() => setLoading(false), 1000); }, []); const webhooks: WebhookStatus[] = [ - { - name: "Order Created", - event: "ORDER_CREATED", - active: true, - targetUrl: "N8N Workflow", - }, - { - name: "Order Fulfilled", - event: "ORDER_FULFILLED", - active: true, - targetUrl: "N8N Workflow", - }, - { - name: "Order Cancelled", - event: "ORDER_CANCELLED", - active: true, - targetUrl: "N8N Workflow", - }, + { name: "Order Created", event: "ORDER_CREATED", active: true }, + { name: "Order Fulfilled", event: "ORDER_FULFILLED", active: true }, + { name: "Order Cancelled", event: "ORDER_CANCELLED", active: true }, ]; - const appInfo: AppInfo = { - name: "Core Extensions", - version: "1.0.0", - id: "saleor-core-extensions", - }; - if (!mounted) { - return ( - - - - ); - } - - if (loading) { return ( - - - Loading dashboard... - + Loading... ); } return ( - {/* Header */} - - - Core Extensions Dashboard - - - Email automation for ManoonOils - - + + Core Extensions Dashboard + + + + Email automation for ManoonOils + - {/* Connection Status */} Connection Status + {appBridgeState?.ready ? ( - - - - Connected - - Connected to Saleor Dashboard - - + + Connected + Connected to Saleor Dashboard + ) : ( - - - - Standalone - - Running outside Saleor Dashboard - - + + Standalone + Running outside Saleor Dashboard + )} - {/* Webhooks Section */} Webhooks Configuration - + + Active webhooks forwarding to N8N - + {webhooks.map((webhook) => ( - - - - - - {webhook.targetUrl} - - - {webhook.active ? "Active" : "Inactive"} - - + + + {webhook.name} + {webhook.event} - + {webhook.active ? "Active" : "Inactive"} + ))} - + - {/* Email Configuration */} Email Configuration - - - Customer Emails - - - - - From: - support@mail.manoonoils.com - - - Provider: - Resend - - - Templates: - Order Confirmation, Order Shipped, Order Cancelled - + + Customer Emails + + + From: + support@mail.manoonoils.com - - - - - - Admin Notifications - - - - - Recipients: - me@hytham.me, tamara@hytham.me - - - Subject: - New Order! 🎉 #{orderNumber} - - - Trigger: - All order events - + + Provider: + Resend - - - - - - - {/* App Info */} - - - App Information - - - - Name: - {appInfo.name} - - Version: - {appInfo.version} - - - ID: - {appInfo.id} + + + + Admin Notifications + + + Recipients: + me@hytham.me, tamara@hytham.me + + + Subject: + New Order! #{'{orderNumber}'} + - {/* Actions */} +