refactor: make app portable for open source
- Remove hardcoded api.manoonoils.com fetch patch - Make allowedSaleorUrls configurable via ALLOWED_SALEOR_URLS env var - Make email logo/company configurable via EMAIL_LOGO_URL, EMAIL_COMPANY_NAME - Add comprehensive README with deployment docs - Keep internal networking preference via SALEOR_API_URL env var
This commit is contained in:
@@ -1,112 +1,140 @@
|
||||
<div align="center">
|
||||
<img width="150" alt="saleor-app-template" src="https://user-images.githubusercontent.com/4006792/215185065-4ef2eda4-ca71-48cc-b14b-c776e0b491b6.png">
|
||||
</div>
|
||||
# Saleor Core Extensions
|
||||
|
||||
<div align="center">
|
||||
<h1>Saleor App Template</h1>
|
||||
</div>
|
||||
A Saleor app that sends email notifications for order events (created, fulfilled, cancelled) using React Email templates.
|
||||
|
||||
<div align="center">
|
||||
<p>Bare-bones boilerplate for writing Saleor Apps with Next.js.</p>
|
||||
</div>
|
||||
## Features
|
||||
|
||||
<div align="center">
|
||||
<a href="https://saleor.io/">Website</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.saleor.io/docs/3.x/">Docs</a>
|
||||
<span> | </span>
|
||||
<a href="https://githubbox.com/saleor/saleor-app-template">CodeSandbox</a>
|
||||
</div>
|
||||
- **Order Created** - Sends confirmation emails to customer and admin
|
||||
- **Order Fulfilled** - Sends shipping notification with tracking
|
||||
- **Order Cancelled** - Sends cancellation notification
|
||||
- **Multi-language** - Supports EN, SR, DE, FR
|
||||
- **React Email** - Professional HTML emails with responsive design
|
||||
|
||||
> [!TIP]
|
||||
> Questions or issues? Check our [discord](https://discord.gg/H52JTZAtSH) channel for help.
|
||||
## Installation
|
||||
|
||||
### What is Saleor App
|
||||
### Option 1: Install via Manifest URL
|
||||
|
||||
Saleor App is the fastest way of extending Saleor with custom logic using [asynchronous](https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks) and [synchronous](https://docs.saleor.io/docs/3.x/developer/extending/apps/synchronous-webhooks/key-concepts) webhooks (and vast Saleor's API). In most cases, creating an App consists of two tasks:
|
||||
In your Saleor Dashboard, go to Apps → Install App → Enter manifest URL:
|
||||
|
||||
- Writing webhook's code executing your custom logic.
|
||||
- Developing configuration UI to be displayed in Saleor Dashboard via specialized view (designated in the App's manifest).
|
||||
```
|
||||
https://your-app-domain.com/api/manifest
|
||||
```
|
||||
|
||||
### What's included?
|
||||
### Option 2: Manual Installation
|
||||
|
||||
- 🚀 Communication between Saleor instance and Saleor App
|
||||
- 📖 Manifest with webhooks using custom query
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/your-org/saleor-core-extensions.git
|
||||
cd saleor-core-extensions
|
||||
|
||||
### Why Next.js
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
You can use any preferred technology to create Saleor Apps, but Next.js is among the most efficient for two reasons. The first is the simplicity of maintaining your API endpoints/webhooks and your apps' configuration React front-end in a single, well-organized project. The second reason is the ease and quality of local development and deployment.
|
||||
# Build Docker image
|
||||
docker build -t ghcr.io/your-org/saleor-core-extensions:latest .
|
||||
|
||||
### Learn more about Apps
|
||||
# Deploy to your K8s cluster
|
||||
kubectl apply -f deployment.yaml
|
||||
```
|
||||
|
||||
[Apps guide](https://docs.saleor.io/docs/3.x/developer/extending/apps/key-concepts)
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `SALEOR_API_URL` | Yes | - | Internal K8s URL for Saleor API (e.g., `http://saleor-api.saleor:8000/graphql/`) |
|
||||
| `ALLOWED_SALEOR_URLS` | No | `http://localhost:3000` | Comma-separated list of allowed Saleor API URLs |
|
||||
| `RESEND_API_KEY` | Yes | - | API key from [Resend.com](https://resend.com) |
|
||||
| `FROM_EMAIL` | No | `support@mail.manoonoils.com` | Sender email address |
|
||||
| `FROM_NAME` | No | `ManoonOils` | Sender name |
|
||||
| `ADMIN_EMAILS` | Yes | - | Comma-separated admin emails for notifications |
|
||||
| `SITE_URL` | No | `https://dev.manoonoils.com` | Public store URL |
|
||||
| `DASHBOARD_URL` | No | `https://dashboard.manoonoils.com` | Saleor dashboard URL |
|
||||
| `APP_IFRAME_BASE_URL` | Yes | - | Public URL where app is hosted |
|
||||
| `APP_API_BASE_URL` | Yes | - | Same as APP_IFRAME_BASE_URL |
|
||||
| `AUTH_DATA_FILE_PATH` | No | `/tmp/.auth-data.json` | Path for auth data storage |
|
||||
| `EMAIL_LOGO_URL` | No | - | URL to company logo for emails |
|
||||
| `EMAIL_COMPANY_NAME` | No | `Store` | Company name in emails |
|
||||
| `EMAIL_FOOTER` | No | auto | Footer text in emails |
|
||||
|
||||
### Kubernetes Deployment
|
||||
|
||||
```yaml
|
||||
env:
|
||||
- name: SALEOR_API_URL
|
||||
value: "http://saleor-api.saleor:8000/graphql/"
|
||||
- name: APP_IFRAME_BASE_URL
|
||||
value: "https://your-app.domain.com"
|
||||
- name: APP_API_BASE_URL
|
||||
value: "https://your-app.domain.com"
|
||||
- name: RESEND_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: core-extensions-secrets
|
||||
key: resend-api-key
|
||||
- name: ADMIN_EMAILS
|
||||
value: "admin@example.com"
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ Webhooks ┌──────────────────┐
|
||||
│ Saleor Cloud │ ───────────────► │ Core Extensions │
|
||||
│ │ │ App │
|
||||
└─────────────────┘ └────────┬─────────┘
|
||||
▲ │
|
||||
│ ▼
|
||||
│ ┌──────────────┐
|
||||
│ │ Resend │
|
||||
└────── GraphQL API ───────────│ (Email) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### Internal Networking
|
||||
|
||||
The app uses `SALEOR_API_URL` to communicate with Saleor API internally, avoiding Cloudflare HTTP restrictions. The stored auth token is reused while the env var controls the API endpoint.
|
||||
|
||||
## Development
|
||||
|
||||
#### Running app locally in development containers
|
||||
|
||||
The easiest way of running app for local development is to use [development containers](https://containers.dev/).
|
||||
If you have Visual Studio Code follow their [guide](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container) on how to open existing folder in container.
|
||||
|
||||
Development container only creates container, you still need to start the server.
|
||||
|
||||
Development container will have port opened:
|
||||
|
||||
1. `3000` - were app dev server will listen to requests
|
||||
|
||||
### Requirements
|
||||
|
||||
Before you start, make sure you have installed:
|
||||
|
||||
- [Node.js 22](https://nodejs.org/en/)
|
||||
- [pnpm 9](https://pnpm.io/)
|
||||
|
||||
1. Install the dependencies by running:
|
||||
|
||||
```
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. Start the local server with:
|
||||
|
||||
```
|
||||
# Start development server
|
||||
pnpm dev
|
||||
|
||||
# Generate GraphQL types
|
||||
pnpm generate
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
```
|
||||
|
||||
3. Expose local environment using tunnel:
|
||||
Use tunneling tools like [localtunnel](https://github.com/localtunnel/localtunnel) or [ngrok](https://ngrok.com/).
|
||||
|
||||
4. Install the application in your dashboard:
|
||||
|
||||
If you use Saleor Cloud or your local server is exposed, you can install your app by following this link:
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
[YOUR_SALEOR_DASHBOARD_URL]/apps/install?manifestUrl=[YOUR_APP_TUNNEL_MANIFEST_URL]
|
||||
src/
|
||||
├── emails/
|
||||
│ ├── BaseLayout.tsx # Email layout with logo/footer
|
||||
│ ├── OrderConfirmation.tsx # Order confirmation email
|
||||
│ ├── OrderShipped.tsx # Order shipped email
|
||||
│ └── OrderCancelled.tsx # Order cancelled email
|
||||
├── lib/
|
||||
│ ├── resend.ts # Email sending logic
|
||||
│ └── create-graphq-client.ts
|
||||
├── pages/
|
||||
│ ├── api/
|
||||
│ │ ├── manifest.ts # App manifest
|
||||
│ │ └── register.ts # App registration
|
||||
│ └── webhooks/
|
||||
│ ├── order-created.ts
|
||||
│ ├── order-fulfilled.ts
|
||||
│ └── order-cancelled.ts
|
||||
└── saleor-app.ts # APL configuration
|
||||
```
|
||||
|
||||
This template host manifest at `/api/manifest`
|
||||
## License
|
||||
|
||||
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
|
||||
|
||||
This project uses a `generate` npm script command to:
|
||||
|
||||
- Generate GraphQL schema and typed functions from Saleor's GraphQL endpoint.
|
||||
- Generate types for Saleor sync webhook responses from JSON schema
|
||||
|
||||
Commit the `generated` folder to your repo as they are necessary for queries and keeping track of the GraphQL / JSON schema changes.
|
||||
|
||||
To generate GraphQL types we are using [GraphQL Codegen](https://www.graphql-code-generator.com/). For generating types from JSON schema we use [json-schema-to-typescript](https://www.npmjs.com/package/json-schema-to-typescript).
|
||||
|
||||
### Storing registration data - APL
|
||||
|
||||
During the registration process, Saleor API passes 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://docs.saleor.io/developer/extending/apps/developing-apps/app-sdk/apl).
|
||||
|
||||
The choice of the APL is made using the `APL` environment variable. If the value is not set, FileAPL is used. Available choices:
|
||||
|
||||
- `file`: no additional setup is required. Good choice for local development. It 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. It can be used for development and production and supports multi-tenancy. Requires `UPSTASH_URL` and `UPSTASH_TOKEN` environment variables to be set
|
||||
|
||||
If you want to use your own database, you can implement your own APL. [Check the documentation to read more](https://docs.saleor.io/developer/extending/apps/developing-apps/app-sdk/apl).
|
||||
MIT
|
||||
|
||||
+31
-19
@@ -21,25 +21,35 @@ interface BaseLayoutProps {
|
||||
|
||||
const translations: Record<string, { footer: string; company: string }> = {
|
||||
sr: {
|
||||
footer: "ManoonOils - Prirodna kozmetika | www.manoonoils.com",
|
||||
company: "ManoonOils",
|
||||
footer: "",
|
||||
company: "",
|
||||
},
|
||||
en: {
|
||||
footer: "ManoonOils - Natural Cosmetics | www.manoonoils.com",
|
||||
company: "ManoonOils",
|
||||
footer: "",
|
||||
company: "",
|
||||
},
|
||||
de: {
|
||||
footer: "ManoonOils - Natürliche Kosmetik | www.manoonoils.com",
|
||||
company: "ManoonOils",
|
||||
footer: "",
|
||||
company: "",
|
||||
},
|
||||
fr: {
|
||||
footer: "ManoonOils - Cosmétiques Naturels | www.manoonoils.com",
|
||||
company: "ManoonOils",
|
||||
footer: "",
|
||||
company: "",
|
||||
},
|
||||
};
|
||||
|
||||
export function BaseLayout({ children, previewText, language, siteUrl }: BaseLayoutProps) {
|
||||
const COMPANY_NAME = process.env.EMAIL_COMPANY_NAME || "Store";
|
||||
const LOGO_URL = process.env.EMAIL_LOGO_URL || "";
|
||||
const DEFAULT_FOOTER = process.env.EMAIL_FOOTER || `${COMPANY_NAME} | ${process.env.SITE_URL || ""}`;
|
||||
|
||||
function getFooter(language: string): string {
|
||||
const t = translations[language] || translations.en;
|
||||
const footer = t.footer || DEFAULT_FOOTER;
|
||||
return footer.replace("{company}", COMPANY_NAME).replace("{siteUrl}", process.env.SITE_URL || "");
|
||||
}
|
||||
|
||||
export function BaseLayout({ children, previewText, language, siteUrl }: BaseLayoutProps) {
|
||||
const footer = getFooter(language);
|
||||
|
||||
return (
|
||||
<Html>
|
||||
@@ -47,18 +57,20 @@ export function BaseLayout({ children, previewText, language, siteUrl }: BaseLay
|
||||
<Preview>{previewText}</Preview>
|
||||
<Body style={styles.body}>
|
||||
<Container style={styles.container}>
|
||||
<Section style={styles.logoSection}>
|
||||
<Img
|
||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||
width="150"
|
||||
height="auto"
|
||||
alt="ManoonOils"
|
||||
style={styles.logo}
|
||||
/>
|
||||
</Section>
|
||||
{LOGO_URL && (
|
||||
<Section style={styles.logoSection}>
|
||||
<Img
|
||||
src={LOGO_URL}
|
||||
width="150"
|
||||
height="auto"
|
||||
alt={COMPANY_NAME}
|
||||
style={styles.logo}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
{children}
|
||||
<Section style={styles.footer}>
|
||||
<Text style={styles.footerText}>{t.footer}</Text>
|
||||
<Text style={styles.footerText}>{footer}</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
// Patch fetch to force HTTPS for api.manoonoils.com
|
||||
const originalFetch = global.fetch;
|
||||
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
let url = input.toString();
|
||||
if (url.startsWith('http://api.manoonoils.com/')) {
|
||||
url = url.replace('http://', 'https://');
|
||||
input = url;
|
||||
}
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
|
||||
import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { saleorApp } from "@/saleor-app";
|
||||
|
||||
const allowedSaleorUrls = process.env.ALLOWED_SALEOR_URLS
|
||||
? process.env.ALLOWED_SALEOR_URLS.split(",").map((url) => url.trim())
|
||||
: ["http://localhost:3000", "https://*.saleor.cloud"];
|
||||
|
||||
export default createAppRegisterHandler({
|
||||
apl: saleorApp.apl,
|
||||
allowedSaleorUrls: [
|
||||
"https://api.manoonoils.com/graphql/",
|
||||
"http://api.manoonoils.com/graphql/",
|
||||
],
|
||||
allowedSaleorUrls,
|
||||
});
|
||||
Reference in New Issue
Block a user