├── .yarnrc.yml ├── src ├── assets │ └── email-templates │ │ ├── pug │ │ └── example │ │ │ ├── subject.pug │ │ │ └── html.pug │ │ └── html │ │ ├── welcome-example │ │ └── html.html │ │ └── back-in-stock-example │ │ └── html.html ├── ui-components │ ├── index.ts │ ├── types │ │ ├── models.ts │ │ └── api.ts │ ├── tabs │ │ ├── overview-tab │ │ │ ├── overview-tab.tsx │ │ │ ├── stats │ │ │ │ ├── common │ │ │ │ │ ├── subscriptions-number.tsx │ │ │ │ │ ├── email-message-number.tsx │ │ │ │ │ └── email-message-enabled.tsx │ │ │ │ ├── welcome-message-stats.tsx │ │ │ │ └── back-in-stock-stats.tsx │ │ │ └── emails-sent.tsx │ │ └── settings-tab │ │ │ └── settings-tab.tsx │ └── common │ │ └── email-preview.tsx ├── index.ts ├── admin │ └── routes │ │ └── marketing │ │ ├── settings │ │ ├── types.ts │ │ ├── welcome │ │ │ ├── page.tsx │ │ │ └── documentation.ts │ │ ├── back-in-stock │ │ │ ├── page.tsx │ │ │ └── documentation.ts │ │ ├── settings-info-drawer.tsx │ │ └── settings-email-configuration.tsx │ │ └── page.tsx ├── loaders │ └── README.md ├── api │ ├── store │ │ ├── README.md │ │ ├── me │ │ │ └── marketing │ │ │ │ ├── unsubscribe │ │ │ │ └── route.ts │ │ │ │ └── subscribe │ │ │ │ └── back-in-stock │ │ │ │ └── route.ts │ │ └── api-definition.yaml │ └── admin │ │ └── marketing │ │ ├── emails-sent │ │ └── route.ts │ │ ├── emails-sent-number │ │ └── route.ts │ │ ├── subscriptions │ │ └── count │ │ │ └── route.ts │ │ ├── email-preview │ │ └── route.ts │ │ └── email-settings │ │ └── route.ts ├── subscribers │ ├── marketing │ │ ├── customer-created.ts │ │ └── back-in-stock.ts │ └── README.md ├── services │ ├── types │ │ ├── api.ts │ │ └── email.ts │ ├── constants │ │ └── email.ts │ ├── marketingSubscription.ts │ ├── email.ts │ └── marketing.ts ├── migrations │ ├── README.md │ ├── 1719656976651-MarketingEmailSent.ts │ ├── 1719492511743-MarketingSubscription.ts │ └── 1719492355639-MarketingEmailSettings.ts ├── models │ ├── marketing-email-sent.ts │ ├── marketing-subscription.ts │ ├── marketing-email-settings.ts │ └── README.md └── jobs │ └── README.md ├── docs ├── medusa-marketing-1.png ├── medusa-marketing-2.png ├── medusa-marketing-3.png └── medusa-marketing-logo.png ├── tsconfig.spec.json ├── tsconfig.admin.json ├── tsconfig.server.json ├── .babelrc.js ├── .gitignore ├── .npmignore ├── datasource.js ├── .github └── dependabot.yml ├── tsconfig.json ├── LICENSE ├── index.js ├── medusa-config.js ├── package.json ├── data ├── seed-onboarding.json └── seed.json └── README.md /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules -------------------------------------------------------------------------------- /src/assets/email-templates/pug/example/subject.pug: -------------------------------------------------------------------------------- 1 | = `Hi ${name}, welcome to Mars` -------------------------------------------------------------------------------- /docs/medusa-marketing-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSC-Labs/medusa-marketing/HEAD/docs/medusa-marketing-1.png -------------------------------------------------------------------------------- /docs/medusa-marketing-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSC-Labs/medusa-marketing/HEAD/docs/medusa-marketing-2.png -------------------------------------------------------------------------------- /docs/medusa-marketing-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSC-Labs/medusa-marketing/HEAD/docs/medusa-marketing-3.png -------------------------------------------------------------------------------- /docs/medusa-marketing-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSC-Labs/medusa-marketing/HEAD/docs/medusa-marketing-logo.png -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["dist", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/email-templates/pug/example/html.pug: -------------------------------------------------------------------------------- 1 | p Hi #{email}, 2 | p Welcome to Mars, the red planet. 3 | img(src="https://cdn.pixabay.com/photo/2011/12/13/14/30/mars-11012_1280.jpg") -------------------------------------------------------------------------------- /tsconfig.admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext" 5 | }, 6 | "include": ["src/admin"], 7 | "exclude": ["**/*.spec.js"] 8 | } -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | /* Emit a single file with source maps instead of having a separate file. */ 5 | "inlineSourceMap": true 6 | }, 7 | "exclude": ["src/admin", "**/*.spec.js"] 8 | } -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | let ignore = [`**/dist`] 2 | 3 | // Jest needs to compile this code, but generally we don't want this copied 4 | // to output folders 5 | if (process.env.NODE_ENV !== `test`) { 6 | ignore.push(`**/__tests__`) 7 | } 8 | 9 | module.exports = { 10 | presets: [["babel-preset-medusa-package"], ["@babel/preset-typescript"]], 11 | ignore, 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | .env 3 | .DS_Store 4 | /uploads 5 | /node_modules 6 | yarn-error.log 7 | 8 | .idea 9 | 10 | coverage 11 | 12 | !src/** 13 | 14 | ./tsconfig.tsbuildinfo 15 | package-lock.json 16 | yarn.lock 17 | medusa-db.sql 18 | build 19 | .cache 20 | 21 | .yarn/* 22 | !.yarn/patches 23 | !.yarn/plugins 24 | !.yarn/releases 25 | !.yarn/sdks 26 | !.yarn/versions 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /lib 2 | node_modules 3 | .DS_store 4 | .env* 5 | /*.js 6 | !index.js 7 | yarn.lock 8 | src 9 | .gitignore 10 | .eslintrc 11 | .babelrc 12 | .prettierrc 13 | build 14 | .cache 15 | .yarn 16 | uploads 17 | 18 | # These are files that are included in a 19 | # Medusa project and can be removed from a 20 | # plugin project 21 | medusa-config.js 22 | Dockerfile 23 | medusa-db.sql 24 | develop.sh -------------------------------------------------------------------------------- /datasource.js: -------------------------------------------------------------------------------- 1 | const { DataSource } = require("typeorm") 2 | 3 | const AppDataSource = new DataSource({ 4 | type: "postgres", 5 | port: 5432, 6 | username: "postgres", 7 | password: "postgres", 8 | database: "medusa-z0g5", 9 | entities: [ 10 | ], 11 | migrations: [ 12 | "dist/migrations/*.js", 13 | ], 14 | autoLoadEntities: true 15 | }) 16 | 17 | module.exports = { 18 | datasource: AppDataSource, 19 | } -------------------------------------------------------------------------------- /src/ui-components/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | export {} -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | export * from './ui-components' -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | allow: 8 | - dependency-type: production 9 | groups: 10 | medusa: 11 | patterns: 12 | - "@medusajs*" 13 | - "medusa*" 14 | update-types: 15 | - "minor" 16 | - "patch" 17 | ignore: 18 | - dependency-name: "@medusajs*" 19 | update-types: ["version-update:semver-major"] 20 | - dependency-name: "medusa*" 21 | update-types: ["version-update:semver-major"] 22 | -------------------------------------------------------------------------------- /src/admin/routes/marketing/settings/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | 14 | export type MessageDocumentation = { 15 | trigger?: string, 16 | triggerDescription: string, 17 | availableParameters: string[] 18 | } -------------------------------------------------------------------------------- /src/loaders/README.md: -------------------------------------------------------------------------------- 1 | # Custom loader 2 | 3 | The loader allows you have access to the Medusa service container. This allows you to access the database and the services registered on the container. 4 | you can register custom registrations in the container or run custom code on startup. 5 | 6 | ```ts 7 | // src/loaders/my-loader.ts 8 | 9 | import { AwilixContainer } from 'awilix' 10 | 11 | /** 12 | * 13 | * @param container The container in which the registrations are made 14 | * @param config The options of the plugin or the entire config object 15 | */ 16 | export default (container: AwilixContainer, config: Record): void | Promise => { 17 | /* Implement your own loader. */ 18 | } 19 | ``` -------------------------------------------------------------------------------- /src/api/store/README.md: -------------------------------------------------------------------------------- 1 | # Store API 2 | 3 | Some actions (like `back in stock`) requires customer interaction. For example customers wants to subscribe for a variant which is not available and wants to get notification that it is back in stock. 4 | 5 | This plugin exposes endpoints for your storefront to use it for above scenarios. 6 | 7 | **_NOTE:_** All APIs require logged-in customer. That's why the APIs are defined under `store/me/...`. For more information how it works - please check MedusaJS documentation - https://docs.medusajs.com/development/api-routes/create#protect-store-api-routes. It is needed to properly map subscription to customer id and retrieve proper email address. 8 | 9 | API definition can be found here: [Subscription API](./api-definition.yaml). -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "checkJs": false, 7 | "jsx": "react-jsx", 8 | "declaration": true, 9 | "outDir": "./dist", 10 | "rootDir": "./src", 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "noEmit": false, 14 | "strict": false, 15 | "moduleResolution": "node", 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "forceConsistentCasingInFileNames": true 20 | }, 21 | "include": ["src/"], 22 | "exclude": [ 23 | "dist", 24 | "build", 25 | ".cache", 26 | "tests", 27 | "**/*.spec.js", 28 | "**/*.spec.ts", 29 | "node_modules", 30 | ".eslintrc.js" 31 | ] 32 | } -------------------------------------------------------------------------------- /src/admin/routes/marketing/settings/welcome/page.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { EmailSentType } from "../../../../../ui-components/types/models"; 14 | import SettingsEmailConfigurationPage from "../settings-email-configuration"; 15 | 16 | const SettingsWelcomeEmailPage = () => { 17 | return 18 | } 19 | export default SettingsWelcomeEmailPage -------------------------------------------------------------------------------- /src/admin/routes/marketing/settings/back-in-stock/page.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { EmailSentType } from "../../../../../ui-components/types/models"; 14 | import SettingsEmailConfigurationPage from "../settings-email-configuration"; 15 | 16 | const SettingsBackInStockEmailPage = () => { 17 | return 18 | } 19 | export default SettingsBackInStockEmailPage -------------------------------------------------------------------------------- /src/subscribers/marketing/customer-created.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type SubscriberConfig, 3 | type SubscriberArgs, 4 | CustomerService, 5 | Customer, 6 | Logger, 7 | } from "@medusajs/medusa" 8 | import MarketingService from "../../services/marketing" 9 | 10 | export default async function marketingCustomerCreated({ 11 | data, eventName, container, pluginOptions, 12 | }: SubscriberArgs>) { 13 | const marketingService: MarketingService = container.resolve("marketingService"); 14 | const logger = container.resolve("logger") 15 | logger.debug('Marketing catches creation of customer'); 16 | marketingService.welcomeCustomer(data as Customer); 17 | } 18 | 19 | export const config: SubscriberConfig = { 20 | event: CustomerService.Events.CREATED, 21 | context: { 22 | subscriberId: "marketing-customer-created", 23 | }, 24 | } -------------------------------------------------------------------------------- /src/admin/routes/marketing/settings/welcome/documentation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { MessageDocumentation } from "../types"; 14 | 15 | 16 | export const WelcomeDocumentation: MessageDocumentation = { 17 | trigger: 'customer.created', 18 | triggerDescription: 'This message is triggered when customer creates an account and event "customer.created" is generated.', 19 | availableParameters: [ 20 | "first_name", 21 | "last_name", 22 | "email" 23 | ] 24 | } -------------------------------------------------------------------------------- /src/admin/routes/marketing/settings/back-in-stock/documentation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { MessageDocumentation } from "../types"; 14 | 15 | 16 | export const BackInStockDocumentation: MessageDocumentation = { 17 | triggerDescription: 'This message is triggered when variant.inventory_quantity becomes bigger than 0', 18 | availableParameters: [ 19 | 'variant_title', 20 | 'product_title', 21 | 'product_thumbnail', 22 | 'product_handle', 23 | "first_name", 24 | "last_name" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/services/types/api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { MarketingEmailSettings } from "../../models/marketing-email-settings" 14 | import { EmailTemplateProperties } from "./email" 15 | 16 | export type EmailPreview = { 17 | htmlContent: string 18 | } 19 | 20 | export type AvailableEmailSettings = { 21 | availableTransports: string[], 22 | availableTemplates: EmailTemplateProperties[], 23 | } 24 | 25 | 26 | export type EmailSettings = { 27 | settings?: MarketingEmailSettings, 28 | availableSettings: AvailableEmailSettings 29 | } -------------------------------------------------------------------------------- /src/services/constants/email.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { EmailSentType } from "../types/email"; 14 | 15 | export const EMAIL_TEMPLATE_PARAMETERS_MAP: Record> = { 16 | [EmailSentType.WELCOME] : { 17 | first_name: 'John', 18 | last_name: 'Doe', 19 | email: 'john@doe.com' 20 | }, 21 | [EmailSentType.BACK_IN_STOCK] : { 22 | variant_title: 'Product Variant Title', 23 | product_title: 'Product Title', 24 | first_name: 'John', 25 | last_name: 'Doe' 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/subscribers/marketing/back-in-stock.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type SubscriberConfig, 3 | type SubscriberArgs, 4 | ProductVariantService, 5 | Logger, 6 | } from "@medusajs/medusa" 7 | import MarketingService from "../../services/marketing" 8 | 9 | export default async function marketingBackInStock({ 10 | data, eventName, container, pluginOptions, 11 | }: SubscriberArgs>) { 12 | const { id, fields } = data; 13 | 14 | if (fields.includes('inventory_quantity')) { 15 | const marketingService: MarketingService = container.resolve("marketingService"); 16 | const logger = container.resolve("logger") 17 | logger.debug('Marketing catches change of inventory quantity'); 18 | marketingService.handleInventoryQuantityChange(id); 19 | } 20 | } 21 | 22 | export const config: SubscriberConfig = { 23 | event: ProductVariantService.Events.UPDATED, 24 | context: { 25 | subscriberId: "marketing-back-in-stock", 26 | }, 27 | } -------------------------------------------------------------------------------- /src/migrations/README.md: -------------------------------------------------------------------------------- 1 | # Custom migrations 2 | 3 | You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`. 4 | In that case you also need to provide a migration in order to create the table in the database. 5 | 6 | ## Example 7 | 8 | ### 1. Create the migration 9 | 10 | See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation. 11 | 12 | ```ts 13 | // src/migration/my-migration.ts 14 | 15 | import { MigrationInterface, QueryRunner } from "typeorm" 16 | 17 | export class MyMigration1617703530229 implements MigrationInterface { 18 | name = "myMigration1617703530229" 19 | 20 | public async up(queryRunner: QueryRunner): Promise { 21 | // write you migration here 22 | } 23 | 24 | public async down(queryRunner: QueryRunner): Promise { 25 | // write you migration here 26 | } 27 | } 28 | 29 | ``` -------------------------------------------------------------------------------- /src/ui-components/types/models.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | export enum EmailSentStatus { 14 | PROCESSED = 'processed', 15 | FAILED = 'failed' 16 | } 17 | 18 | export enum EmailSentType { 19 | WELCOME = 'welcome', 20 | BACK_IN_STOCK = 'back in stock', 21 | } 22 | 23 | export type EmailSent = { 24 | id: string, 25 | receiver_email: string, 26 | status: EmailSentStatus; 27 | type: EmailSentType; 28 | created_at: number 29 | } 30 | 31 | export type MarketingEmailSettings = { 32 | id: string, 33 | email_transport: string, 34 | email_template_type: string, 35 | email_template_name: string, 36 | email_type: string, 37 | email_subject: string, 38 | enabled: boolean 39 | } -------------------------------------------------------------------------------- /src/api/admin/marketing/emails-sent/route.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import type { 14 | MedusaRequest, 15 | MedusaResponse, 16 | } from "@medusajs/medusa" 17 | import MarketingService from "../../../../services/marketing"; 18 | 19 | export const GET = async ( 20 | req: MedusaRequest, 21 | res: MedusaResponse 22 | ) => { 23 | 24 | const marketingService: MarketingService = req.scope.resolve('marketingService'); 25 | 26 | try { 27 | const emailsSent = await marketingService.getEmailsSent(); 28 | res.status(200).json({ 29 | result: emailsSent 30 | }); 31 | 32 | } catch (e) { 33 | res.status(400).json({ 34 | message: e.message 35 | }) 36 | } 37 | } -------------------------------------------------------------------------------- /src/models/marketing-email-sent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | 14 | import { BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; 15 | import { generateEntityId } from "@medusajs/utils"; 16 | import { BaseEntity } from "@medusajs/medusa"; 17 | 18 | @Entity() 19 | export class MarketingEmailSent extends BaseEntity { 20 | @PrimaryGeneratedColumn() 21 | id: string; 22 | 23 | @Column() 24 | receiver_email: string; 25 | 26 | @Column() 27 | status: string; 28 | 29 | @Column() 30 | type: string; 31 | 32 | /** 33 | * @apiIgnore 34 | */ 35 | @BeforeInsert() 36 | private beforeInsert(): void { 37 | this.id = generateEntityId(this.id, "memailsent") 38 | } 39 | } -------------------------------------------------------------------------------- /src/services/types/email.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | 14 | export type EmailTransport = { 15 | name: string, 16 | configuration: any 17 | } 18 | 19 | export enum EmailTemplateType { 20 | HTML = 'html', 21 | PUG = 'pug' 22 | } 23 | 24 | export type EmailTemplateProperties = { 25 | templateType: EmailTemplateType, 26 | templateName: string, 27 | } 28 | 29 | export enum EmailSentStatus { 30 | PROCESSED = 'processed', 31 | FAILED = 'failed' 32 | } 33 | 34 | export enum EmailSentType { 35 | WELCOME = 'welcome', 36 | BACK_IN_STOCK = 'back in stock', 37 | } 38 | 39 | export type EmailSettingsBackInStockConfiguration = { 40 | min_inventory_required?: number, 41 | smart_inventory_enabled?: boolean 42 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 RSC-Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/models/marketing-subscription.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { BeforeInsert, Entity, PrimaryGeneratedColumn, Column, JoinColumn, ManyToOne } from "typeorm"; 14 | import { generateEntityId } from "@medusajs/utils"; 15 | import { BaseEntity, Customer } from "@medusajs/medusa"; 16 | 17 | @Entity() 18 | export class MarketingSubscription extends BaseEntity { 19 | @PrimaryGeneratedColumn() 20 | id: string; 21 | 22 | @ManyToOne(() => Customer, { eager: true }) 23 | @JoinColumn({ name: 'customer_id' }) 24 | customer: Customer; 25 | 26 | @Column() 27 | email_type: string; 28 | 29 | @Column() 30 | target_id: string | undefined; 31 | 32 | /** 33 | * @apiIgnore 34 | */ 35 | @BeforeInsert() 36 | private beforeInsert(): void { 37 | this.id = generateEntityId(this.id, "marsub") 38 | } 39 | } -------------------------------------------------------------------------------- /src/migrations/1719656976651-MarketingEmailSent.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from "typeorm"; 2 | 3 | export class MarketingEmailSent1719656976651 implements MigrationInterface { 4 | name = 'EmailSent1719656976651'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.createTable( 8 | new Table({ 9 | name: 'marketing_email_sent', 10 | columns: [ 11 | { name: 'id', type: 'character varying', isPrimary: true }, 12 | { name: 'receiver_email', type: 'character varying'}, 13 | { name: 'status', type: 'character varying'}, 14 | { name: 'type', type: 'character varying'}, 15 | { name: 'created_at', type: 'TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()'}, 16 | { name: 'updated_at', type: 'TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()'}, 17 | { name: 'deleted_at', type: 'TIMESTAMP WITH TIME ZONE', isNullable: true} 18 | ], 19 | }), 20 | true 21 | ); 22 | } 23 | 24 | public async down(queryRunner: QueryRunner): Promise { 25 | await queryRunner.dropTable('marketing_email_sent', true); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/api/admin/marketing/emails-sent-number/route.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import type { 14 | MedusaRequest, 15 | MedusaResponse, 16 | } from "@medusajs/medusa" 17 | import MarketingService from "../../../../services/marketing"; 18 | import { EmailSentType } from "../../../../services/types/email"; 19 | 20 | export const GET = async ( 21 | req: MedusaRequest, 22 | res: MedusaResponse 23 | ) => { 24 | 25 | const marketingService: MarketingService = req.scope.resolve('marketingService'); 26 | const emailSentType: EmailSentType | undefined = req.query.emailType as EmailSentType; 27 | 28 | try { 29 | const emailsSentNumber = await marketingService.getEmailsSentNumber(emailSentType); 30 | res.status(200).json({ 31 | result: emailsSentNumber 32 | }); 33 | 34 | } catch (e) { 35 | res.status(400).json({ 36 | message: e.message 37 | }) 38 | } 39 | } -------------------------------------------------------------------------------- /src/ui-components/tabs/overview-tab/overview-tab.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { Container, Heading, Text } from "@medusajs/ui" 14 | import { Grid } from "@mui/material"; 15 | import { EmailsSent } from "./emails-sent"; 16 | import { WelcomeMessagesStats } from "./stats/welcome-message-stats"; 17 | import { BackInStockStats } from "./stats/back-in-stock-stats"; 18 | 19 | export const OverviewTab = () => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | } -------------------------------------------------------------------------------- /src/api/admin/marketing/subscriptions/count/route.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import type { 14 | MedusaRequest, 15 | MedusaResponse, 16 | } from "@medusajs/medusa" 17 | import MarketingSubscriptionService from "../../../../../services/marketingSubscription"; 18 | import { EmailSentType } from "../../../../../services/types/email"; 19 | 20 | export const GET = async ( 21 | req: MedusaRequest, 22 | res: MedusaResponse 23 | ) => { 24 | 25 | const emailSentType: EmailSentType | undefined = req.query.emailType as EmailSentType; 26 | const marketingSubscriptionService: MarketingSubscriptionService = req.scope.resolve('marketingSubscriptionService'); 27 | 28 | try { 29 | const emailsSent = await marketingSubscriptionService.retrieveNumberOfSubscriptions(emailSentType); 30 | res.status(200).json({ 31 | result: emailsSent 32 | }); 33 | 34 | } catch (e) { 35 | res.status(400).json({ 36 | message: e.message 37 | }) 38 | } 39 | } -------------------------------------------------------------------------------- /src/models/marketing-email-settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { BeforeInsert, Entity, PrimaryGeneratedColumn, Column } from "typeorm"; 14 | import { generateEntityId } from "@medusajs/utils"; 15 | import { BaseEntity } from "@medusajs/medusa"; 16 | 17 | @Entity() 18 | export class MarketingEmailSettings extends BaseEntity { 19 | @PrimaryGeneratedColumn() 20 | id: string; 21 | 22 | @Column() 23 | email_transport: string 24 | 25 | @Column() 26 | email_template_type: string 27 | 28 | @Column() 29 | email_template_name: string 30 | 31 | @Column() 32 | email_type: string 33 | 34 | @Column() 35 | email_subject: string 36 | 37 | @Column() 38 | enabled: boolean 39 | 40 | @Column('jsonb', { nullable: true, default: {}}) 41 | configuration: Record 42 | 43 | /** 44 | * @apiIgnore 45 | */ 46 | @BeforeInsert() 47 | private beforeInsert(): void { 48 | this.id = generateEntityId(this.id, "memailset") 49 | } 50 | } -------------------------------------------------------------------------------- /src/jobs/README.md: -------------------------------------------------------------------------------- 1 | # Custom scheduled jobs 2 | 3 | You may define custom scheduled jobs (cron jobs) by creating files in the `/jobs` directory. 4 | 5 | ```ts 6 | import { 7 | ProductService, 8 | ScheduledJobArgs, 9 | ScheduledJobConfig, 10 | } from "@medusajs/medusa"; 11 | 12 | export default async function myCustomJob({ container }: ScheduledJobArgs) { 13 | const productService: ProductService = container.resolve("productService"); 14 | 15 | const products = await productService.listAndCount(); 16 | 17 | // Do something with the products 18 | } 19 | 20 | export const config: ScheduledJobConfig = { 21 | name: "daily-product-report", 22 | schedule: "0 0 * * *", // Every day at midnight 23 | }; 24 | ``` 25 | 26 | A scheduled job is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when the job is scheduled. The `config` is an object which defines the name of the job, the schedule, and an optional data object. 27 | 28 | The `handler` is a function which takes one parameter, an `object` of type `ScheduledJobArgs` with the following properties: 29 | 30 | - `container` - a `MedusaContainer` instance which can be used to resolve services. 31 | - `data` - an `object` containing data passed to the job when it was scheduled. This object is passed in the `config` object. 32 | - `pluginOptions` - an `object` containing plugin options, if the job is defined in a plugin. 33 | -------------------------------------------------------------------------------- /src/ui-components/tabs/overview-tab/stats/common/subscriptions-number.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* 4 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 5 | * 6 | * MIT License 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { Heading } from "@medusajs/ui" 16 | import { CircularProgress } from "@mui/material"; 17 | import { useAdminCustomQuery } from "medusa-react"; 18 | import { EmailSentType } from "../../../../types/models"; 19 | 20 | type AdminMarketingSubscriptionsCountGetReq = { 21 | emailType?: EmailSentType 22 | }; 23 | 24 | type AdminMarketingSubscriptionsCountResponse = { 25 | result?: number 26 | } 27 | 28 | export const SubscriptionsNumber = ({ emailType } : {emailType: EmailSentType}) => { 29 | const { data, isLoading } = useAdminCustomQuery 30 | ( 31 | "/marketing/subscriptions/count", 32 | [''], 33 | { 34 | emailType: emailType 35 | } 36 | ) 37 | 38 | if (isLoading) { 39 | return ( 40 | 41 | ) 42 | } 43 | 44 | return {data.result} 45 | } -------------------------------------------------------------------------------- /src/ui-components/tabs/overview-tab/stats/common/email-message-number.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { Badge, Container, Heading, Text } from "@medusajs/ui" 14 | import { CircularProgress, Grid } from "@mui/material"; 15 | import { AdminMarketingEmailsSentGetReq, AdminMarketingEmailsSentResponse } from "../../../../types/api"; 16 | import { useAdminCustomQuery } from "medusa-react"; 17 | import { EmailSentType } from "../../../../types/models"; 18 | 19 | type AdminMarketingEmailsSentNumberResponse = { 20 | result?: number 21 | } 22 | 23 | export const EmailMessageNumber = ({ emailType } : {emailType: EmailSentType}) => { 24 | const { data, isLoading } = useAdminCustomQuery 25 | ( 26 | "/marketing/emails-sent-number", 27 | [''], 28 | { 29 | emailType: emailType 30 | } 31 | ) 32 | 33 | if (isLoading) { 34 | return ( 35 | 36 | ) 37 | } 38 | 39 | return {data.result} 40 | } -------------------------------------------------------------------------------- /src/ui-components/types/api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { EmailSent, EmailSentType, MarketingEmailSettings } from "./models"; 14 | 15 | export enum EmailTemplateType { 16 | HTML = 'html', 17 | PUG = 'pug' 18 | } 19 | export type EmailTemplateProperties = { 20 | templateType: EmailTemplateType, 21 | templateName: string 22 | } 23 | 24 | export type AvailableEmailSettings = { 25 | availableTransports: string[], 26 | availableTemplates: EmailTemplateProperties[], 27 | } 28 | 29 | export type AdminMarketingEmailSettingsGetReq = { 30 | emailType: EmailSentType 31 | }; 32 | 33 | 34 | export type AdminMarketingEmailSettingsResponse = { 35 | settings?: MarketingEmailSettings 36 | availableSettings: AvailableEmailSettings 37 | } 38 | 39 | export type AdminMarketingWelcomeMessageSettings = {} 40 | 41 | export type WelcomeMessageSettings = { 42 | 43 | } 44 | 45 | export type AdminMarketingEmailsSentGetReq = { 46 | emailType?: EmailSentType 47 | }; 48 | export type AdminMarketingEmailsSentResponse = { 49 | result?: EmailSent[] 50 | } -------------------------------------------------------------------------------- /src/api/store/me/marketing/unsubscribe/route.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import type { 14 | MedusaRequest, 15 | MedusaResponse, 16 | } from "@medusajs/medusa" 17 | import { EmailSentType } from "../../../../../services/types/email"; 18 | import MarketingSubscriptionService from "../../../../../services/marketingSubscription"; 19 | 20 | export const POST = async ( 21 | req: MedusaRequest, 22 | res: MedusaResponse 23 | ) => { 24 | const id = req.user.customer_id; 25 | const body: any = req.body as any; 26 | const emailType: EmailSentType | undefined = body.emailType; 27 | const targetId: string | undefined = body.targetId; 28 | const marketingSubscriptionService: MarketingSubscriptionService = req.scope.resolve('marketingService'); 29 | 30 | if (!id) { 31 | return res.status(400).json({ 32 | message: 'You need to be logged-in to unsubscribe' 33 | }) 34 | } else { 35 | const result: boolean = await marketingSubscriptionService.unsubscribe(id, emailType, targetId); 36 | return res.status(200).json({ 37 | result: result 38 | }) 39 | } 40 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | const { GracefulShutdownServer } = require("medusa-core-utils") 3 | 4 | const loaders = require("@medusajs/medusa/dist/loaders/index").default 5 | 6 | ;(async() => { 7 | async function start() { 8 | const app = express() 9 | const directory = process.cwd() 10 | 11 | try { 12 | const { container } = await loaders({ 13 | directory, 14 | expressApp: app 15 | }) 16 | const configModule = container.resolve("configModule") 17 | const port = process.env.PORT ?? configModule.projectConfig.port ?? 9000 18 | 19 | const server = GracefulShutdownServer.create( 20 | app.listen(port, (err) => { 21 | if (err) { 22 | return 23 | } 24 | console.log(`Server is ready on port: ${port}`) 25 | }) 26 | ) 27 | 28 | // Handle graceful shutdown 29 | const gracefulShutDown = () => { 30 | server 31 | .shutdown() 32 | .then(() => { 33 | console.info("Gracefully stopping the server.") 34 | process.exit(0) 35 | }) 36 | .catch((e) => { 37 | console.error("Error received when shutting down the server.", e) 38 | process.exit(1) 39 | }) 40 | } 41 | process.on("SIGTERM", gracefulShutDown) 42 | process.on("SIGINT", gracefulShutDown) 43 | } catch (err) { 44 | console.error("Error starting server", err) 45 | process.exit(1) 46 | } 47 | } 48 | 49 | await start() 50 | })() 51 | -------------------------------------------------------------------------------- /src/api/store/me/marketing/subscribe/back-in-stock/route.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import type { 14 | MedusaRequest, 15 | MedusaResponse, 16 | } from "@medusajs/medusa" 17 | import MarketingService from "../../../../../../services/marketing"; 18 | import { MarketingSubscription } from "../../../../../../models/marketing-subscription"; 19 | 20 | export const POST = async ( 21 | req: MedusaRequest, 22 | res: MedusaResponse 23 | ) => { 24 | const id = req.user.customer_id; 25 | const body: any = req.body as any; 26 | const variantId: string | undefined = body.variantId; 27 | const marketingService: MarketingService = req.scope.resolve('marketingService'); 28 | 29 | if (!id) { 30 | return res.status(400).json({ 31 | message: 'You need to be logged-in to subscribe' 32 | }) 33 | } 34 | if (!variantId) { 35 | return res.status(400).json({ 36 | message: 'You need to set variantId for back-in-stock subscription' 37 | }) 38 | } 39 | const subscription: MarketingSubscription = await marketingService.subscribeBackInStock(id, variantId); 40 | return res.status(200).json({ 41 | result: subscription 42 | }) 43 | } -------------------------------------------------------------------------------- /src/migrations/1719492511743-MarketingSubscription.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from "typeorm"; 2 | 3 | export class MarketingSubscription1719492511743 implements MigrationInterface { 4 | 5 | name = 'MarketingSubscription1719492511743'; 6 | 7 | public async up(queryRunner: QueryRunner): Promise { 8 | await queryRunner.createTable( 9 | new Table({ 10 | name: 'marketing_subscription', 11 | columns: [ 12 | { name: 'id', type: 'character varying', isPrimary: true }, 13 | { name: 'customer_id', type: 'character varying' }, 14 | { name: 'email_type', type: 'character varying'}, 15 | { name: 'target_id', type: 'character varying', isNullable: true}, 16 | { name: 'created_at', type: 'TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()'}, 17 | { name: 'updated_at', type: 'TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()'}, 18 | { name: 'deleted_at', type: 'TIMESTAMP WITH TIME ZONE', isNullable: true} 19 | ], 20 | foreignKeys: [ 21 | { 22 | columnNames: ['customer_id'], 23 | referencedColumnNames: ['id'], 24 | referencedTableName: 'public.customer', 25 | }, 26 | ] 27 | }), 28 | true 29 | ); 30 | } 31 | 32 | public async down(queryRunner: QueryRunner): Promise { 33 | await queryRunner.dropTable('marketing_subscription', true); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/admin/routes/marketing/page.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { RouteConfig } from "@medusajs/admin" 14 | import { Gift } from "@medusajs/icons" 15 | import { Tabs, Text, Toaster } from "@medusajs/ui" 16 | import { Box, Grid } from "@mui/material"; 17 | import { OverviewTab } from "../../../ui-components/tabs/overview-tab/overview-tab"; 18 | import { SettingsTab } from "../../../ui-components/tabs/settings-tab/settings-tab"; 19 | 20 | 21 | const MarketingPage = () => { 22 | return ( 23 | 24 | 25 | 26 | Overview 27 | Settings 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ) 39 | } 40 | export const config: RouteConfig = { 41 | link: { 42 | label: "Marketing", 43 | icon: Gift, 44 | }, 45 | } 46 | 47 | export default MarketingPage -------------------------------------------------------------------------------- /src/migrations/1719492355639-MarketingEmailSettings.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from "typeorm"; 2 | 3 | export class MarketingEmailSettings1719492355639 implements MigrationInterface { 4 | name = 'MarketingEmailSettings1719492355639'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.createTable( 8 | new Table({ 9 | name: 'marketing_email_settings', 10 | columns: [ 11 | { name: 'id', type: 'character varying', isPrimary: true }, 12 | { name: 'email_transport', type: 'character varying'}, 13 | { name: 'email_template_type', type: 'character varying'}, 14 | { name: 'email_template_name', type: 'character varying'}, 15 | { name: 'email_type', type: 'character varying'}, 16 | { name: 'email_subject', type: 'character varying'}, 17 | { name: 'enabled', type: 'boolean'}, 18 | { name: 'configuration', type: 'jsonb', isNullable: true}, 19 | { name: 'created_at', type: 'TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()'}, 20 | { name: 'updated_at', type: 'TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()'}, 21 | { name: 'deleted_at', type: 'TIMESTAMP WITH TIME ZONE', isNullable: true} 22 | ], 23 | }), 24 | true 25 | ); 26 | } 27 | 28 | public async down(queryRunner: QueryRunner): Promise { 29 | await queryRunner.dropTable('marketing_email_settings', true); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /medusa-config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | 3 | let ENV_FILE_NAME = ""; 4 | switch (process.env.NODE_ENV) { 5 | case "production": 6 | ENV_FILE_NAME = ".env.production"; 7 | break; 8 | case "staging": 9 | ENV_FILE_NAME = ".env.staging"; 10 | break; 11 | case "test": 12 | ENV_FILE_NAME = ".env.test"; 13 | break; 14 | case "development": 15 | default: 16 | ENV_FILE_NAME = ".env"; 17 | break; 18 | } 19 | 20 | try { 21 | dotenv.config({ path: process.cwd() + "/" + ENV_FILE_NAME }); 22 | } catch (e) {} 23 | 24 | // CORS when consuming Medusa from admin 25 | const ADMIN_CORS = 26 | process.env.ADMIN_CORS || "http://localhost:7000,http://localhost:7001"; 27 | 28 | // CORS to avoid issues when consuming Medusa from a client 29 | const STORE_CORS = process.env.STORE_CORS || "http://localhost:8000"; 30 | 31 | const DATABASE_URL = 32 | process.env.DATABASE_URL || "postgres://localhost/medusa-starter-default"; 33 | 34 | const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379"; 35 | 36 | const plugins = [ 37 | 38 | ]; 39 | 40 | const modules = { 41 | 42 | }; 43 | 44 | /** @type {import('@medusajs/medusa').ConfigModule["projectConfig"]} */ 45 | const projectConfig = { 46 | jwtSecret: process.env.JWT_SECRET, 47 | cookieSecret: process.env.COOKIE_SECRET, 48 | store_cors: STORE_CORS, 49 | database_url: DATABASE_URL, 50 | admin_cors: ADMIN_CORS, 51 | // Uncomment the following lines to enable REDIS 52 | // redis_url: REDIS_URL 53 | }; 54 | 55 | /** @type {import('@medusajs/medusa').ConfigModule} */ 56 | module.exports = { 57 | projectConfig, 58 | plugins, 59 | modules, 60 | }; 61 | -------------------------------------------------------------------------------- /src/api/admin/marketing/email-preview/route.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import type { 14 | MedusaRequest, 15 | MedusaResponse, 16 | } from "@medusajs/medusa" 17 | import EmailService from "../../../../services/email"; 18 | import MarketingService from "../../../../services/marketing"; 19 | import { EmailSentType } from "../../../../services/types/email"; 20 | 21 | export const GET = async ( 22 | req: MedusaRequest, 23 | res: MedusaResponse 24 | ) => { 25 | 26 | const templateType = req.query.templateType; 27 | const templateName = req.query.templateName; 28 | const emailType = req.query.emailType; 29 | const emailService: EmailService = req.scope.resolve('emailService'); 30 | const marketingService: MarketingService = req.scope.resolve('marketingService'); 31 | 32 | try { 33 | const locales = await marketingService.getLocales(emailType as EmailSentType); 34 | if (locales) { 35 | const previewMail = await emailService.previewMail(templateType as string, templateName as string, emailType as EmailSentType, locales); 36 | res.status(200).json({ 37 | result: previewMail 38 | }); 39 | } 40 | } catch (e) { 41 | res.status(400).json({ 42 | message: e.message 43 | }) 44 | } 45 | } -------------------------------------------------------------------------------- /src/subscribers/README.md: -------------------------------------------------------------------------------- 1 | # Custom subscribers 2 | 3 | You may define custom eventhandlers, `subscribers` by creating files in the `/subscribers` directory. 4 | 5 | ```ts 6 | import MyCustomService from "../services/my-custom"; 7 | import { 8 | OrderService, 9 | SubscriberArgs, 10 | SubscriberConfig, 11 | } from "@medusajs/medusa"; 12 | 13 | type OrderPlacedEvent = { 14 | id: string; 15 | no_notification: boolean; 16 | }; 17 | 18 | export default async function orderPlacedHandler({ 19 | data, 20 | eventName, 21 | container, 22 | }: SubscriberArgs) { 23 | const orderService: OrderService = container.resolve(OrderService); 24 | 25 | const order = await orderService.retrieve(data.id, { 26 | relations: ["items", "items.variant", "items.variant.product"], 27 | }); 28 | 29 | // Do something with the order 30 | } 31 | 32 | export const config: SubscriberConfig = { 33 | event: OrderService.Events.PLACED, 34 | }; 35 | ``` 36 | 37 | A subscriber is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when an event is emitted. The `config` is an object which defines which event(s) the subscriber should subscribe to. 38 | 39 | The `handler` is a function which takes one parameter, an `object` of type `SubscriberArgs` with the following properties: 40 | 41 | - `data` - an `object` of type `T` containing information about the event. 42 | - `eventName` - a `string` containing the name of the event. 43 | - `container` - a `MedusaContainer` instance which can be used to resolve services. 44 | - `pluginOptions` - an `object` containing plugin options, if the subscriber is defined in a plugin. 45 | -------------------------------------------------------------------------------- /src/ui-components/tabs/overview-tab/stats/welcome-message-stats.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { Container, Heading, Text } from "@medusajs/ui" 14 | import { Grid } from "@mui/material"; 15 | import { EmailMessageIsEnabled } from "./common/email-message-enabled"; 16 | import { EmailMessageNumber } from "./common/email-message-number"; 17 | import { EmailSentType } from "../../../types/models"; 18 | 19 | export const WelcomeMessagesStats = () => { 20 | return ( 21 | 22 | 23 | 24 | 25 | Welcome 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | emails processed 38 | 39 | 40 | 41 | 42 | 43 | ) 44 | } -------------------------------------------------------------------------------- /src/models/README.md: -------------------------------------------------------------------------------- 1 | # Custom models 2 | 3 | You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`. 4 | 5 | ## Example 6 | 7 | ### 1. Create the Entity 8 | 9 | ```ts 10 | // src/models/post.ts 11 | 12 | import { BeforeInsert, Column, Entity, PrimaryColumn } from "typeorm"; 13 | import { generateEntityId } from "@medusajs/utils"; 14 | import { BaseEntity } from "@medusajs/medusa"; 15 | 16 | @Entity() 17 | export class Post extends BaseEntity { 18 | @Column({type: 'varchar'}) 19 | title: string | null; 20 | 21 | @BeforeInsert() 22 | private beforeInsert(): void { 23 | this.id = generateEntityId(this.id, "post") 24 | } 25 | } 26 | ``` 27 | 28 | ### 2. Create the Migration 29 | 30 | You also need to create a Migration to create the new table in the database. See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation. 31 | 32 | ### 3. Create a Repository 33 | Entities data can be easily accessed and modified using [TypeORM Repositories](https://typeorm.io/working-with-repository). To create a repository, create a file in `src/repositories`. For example, here’s a repository `PostRepository` for the `Post` entity: 34 | 35 | ```ts 36 | // src/repositories/post.ts 37 | 38 | import { EntityRepository, Repository } from "typeorm" 39 | 40 | import { Post } from "../models/post" 41 | 42 | @EntityRepository(Post) 43 | export class PostRepository extends Repository { } 44 | ``` 45 | 46 | See more about defining and accesing your custom [Entities](https://docs.medusajs.com/advanced/backend/entities/overview) in the documentation. -------------------------------------------------------------------------------- /src/ui-components/tabs/overview-tab/stats/common/email-message-enabled.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { Badge, Container, Heading, Text } from "@medusajs/ui" 14 | import { CircularProgress, Grid } from "@mui/material"; 15 | import { AdminMarketingEmailSettingsGetReq, AdminMarketingEmailSettingsResponse } from "../../../../types/api"; 16 | import { useAdminCustomQuery } from "medusa-react"; 17 | import { EmailSentType } from "../../../../types/models"; 18 | 19 | const StatusBadge = ({enabled} : {enabled?: boolean}) => { 20 | if (enabled === undefined) { 21 | return Undefined 22 | } 23 | if (enabled) { 24 | return Enabled 25 | } 26 | return Disabled 27 | } 28 | 29 | export const EmailMessageIsEnabled = ({ emailType } : { emailType: EmailSentType}) => { 30 | const { data, isLoading } = useAdminCustomQuery 31 | ( 32 | "/marketing/email-settings", 33 | [''], 34 | { 35 | emailType: emailType 36 | } 37 | ) 38 | 39 | if (isLoading) { 40 | return ( 41 | 42 | ) 43 | } 44 | 45 | return 46 | } -------------------------------------------------------------------------------- /src/assets/email-templates/html/welcome-example/html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to [Your Shop Name] 7 | 40 | 41 | 42 |
43 |

Welcome to our shop!

44 |

Hi {{first_name}} {{last_name}}

45 |

We’re thrilled to have you here.

46 |

Start exploring our collection and enjoy shopping with us. If you have any questions or need assistance, don’t hesitate to reach out.

47 |

Happy shopping!

48 |

Best regards,

49 |

Your Team

50 | Start Shopping 51 |
52 | 53 | -------------------------------------------------------------------------------- /src/ui-components/tabs/overview-tab/stats/back-in-stock-stats.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { Container, Heading, Text } from "@medusajs/ui" 14 | import { Grid } from "@mui/material"; 15 | import { EmailMessageIsEnabled } from "./common/email-message-enabled"; 16 | import { EmailSentType } from "../../../types/models"; 17 | import { EmailMessageNumber } from "./common/email-message-number"; 18 | import { SubscriptionsNumber } from "./common/subscriptions-number"; 19 | 20 | export const BackInStockStats = () => { 21 | return ( 22 | 23 | 24 | 25 | 26 | Back in stock 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | emails processed 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | subscriptions 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } -------------------------------------------------------------------------------- /src/assets/email-templates/html/back-in-stock-example/html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Great News! {{product_title}} ({{variant_title}}) is Back in Stock! 7 | 40 | 41 | 42 |
43 |

Great News! {{product_title}} ({{variant_title}}) is Back in Stock!

44 |

Hi {{first_name}} {{last_name}},

45 |

We’ve got exciting news for you! The product you’ve been waiting for, {{product_title}} ({{variant_title}}), is now back in stock at shop.

46 | Shop 47 |

Don’t miss out! Click the link below to grab yours before it’s gone again.

48 | Get {{product_title}} ({{variant_title}}) Now 49 |

50 |

If you have any questions or need assistance, feel free to reach out.

51 |

Happy shopping!

52 |

Best regards,

53 |

Your Shop Team

54 |
55 | 56 | -------------------------------------------------------------------------------- /src/ui-components/tabs/settings-tab/settings-tab.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { Container, Heading, Text, Button } from "@medusajs/ui" 14 | import { Grid } from "@mui/material"; 15 | import { Link } from "react-router-dom" 16 | 17 | export const SettingsTab = () => { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | Welcome messages 26 | 27 | 28 | 29 | 30 | Welcome customer after registration 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Back in stock 51 | 52 | 53 | 54 | 55 | Notify customer that variant is back in stock 56 | 57 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | ) 72 | } -------------------------------------------------------------------------------- /src/api/admin/marketing/email-settings/route.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import type { 14 | MedusaRequest, 15 | MedusaResponse, 16 | } from "@medusajs/medusa" 17 | import MarketingService from "../../../../services/marketing"; 18 | import { EmailSentType } from "../../../../services/types/email"; 19 | import { MarketingEmailSettings } from "../../../../models/marketing-email-settings"; 20 | import { EmailSettings } from "../../../../services/types/api"; 21 | 22 | export const GET = async ( 23 | req: MedusaRequest, 24 | res: MedusaResponse 25 | ) => { 26 | 27 | const emailType = req.query.emailType as EmailSentType; 28 | 29 | const marketingService: MarketingService = req.scope.resolve('marketingService'); 30 | 31 | try { 32 | switch (emailType) { 33 | case EmailSentType.BACK_IN_STOCK: 34 | const emailSettingsBackInStock = await marketingService.getEmailSettings(EmailSentType.BACK_IN_STOCK); 35 | return res.status(200).json(emailSettingsBackInStock); 36 | case EmailSentType.WELCOME: 37 | const emailSettingsWelcome = await marketingService.getEmailSettings(EmailSentType.WELCOME); 38 | return res.status(200).json(emailSettingsWelcome); 39 | } 40 | } catch (e) { 41 | res.status(400).json({ 42 | message: e.message 43 | }) 44 | } 45 | } 46 | 47 | export const POST = async ( 48 | req: MedusaRequest, 49 | res: MedusaResponse 50 | ) => { 51 | 52 | const body: any = req.body as any; 53 | const settings: Omit | undefined = body.settings; 54 | const marketingService: MarketingService = req.scope.resolve('marketingService'); 55 | 56 | try { 57 | if (settings !== undefined) { 58 | const newSettings: EmailSettings | undefined = await marketingService.createNewEmailSettings(settings); 59 | if (newSettings !== undefined) { 60 | res.status(201).json({ 61 | settings: newSettings 62 | }); 63 | } else { 64 | res.status(400).json({ 65 | message: 'Cant update settings' 66 | }) 67 | } 68 | } else { 69 | res.status(400).json({ 70 | message: 'Settings not passed' 71 | }) 72 | } 73 | 74 | } catch (e) { 75 | res.status(400).json({ 76 | message: e.message 77 | }) 78 | } 79 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rsc-labs/medusa-marketing", 3 | "version": "0.1.4", 4 | "description": "Send messages to your customers", 5 | "main": "dist/index.js", 6 | "author": "RSC Labs (https://rsoftcon.com)", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/RSC-Labs/medusa-marketing" 11 | }, 12 | "keywords": [ 13 | "medusa-plugin", 14 | "medusa-marketing", 15 | "marketing", 16 | "email", 17 | "medusajs" 18 | ], 19 | "scripts": { 20 | "clean": "cross-env ./node_modules/.bin/rimraf dist", 21 | "copy-assets": "cp -rv src/assets dist/assets", 22 | "build": "cross-env npm run clean && npm run build:server && npm run build:admin", 23 | "build:server": "cross-env npm run clean && tsc -p tsconfig.json", 24 | "build:admin": "cross-env medusa-admin build", 25 | "prepare": "cross-env NODE_ENV=production npm run build:server && npm run copy-assets && medusa-admin bundle" 26 | }, 27 | "dependencies": { 28 | "@medusajs/admin": "7.1.17", 29 | "@medusajs/ui": "^3.0.0", 30 | "@medusajs/utils": "1.11.11", 31 | "@medusajs/icons": "1.2.2", 32 | "@tanstack/react-query": "4.22", 33 | "medusa-interfaces": "^1.3.8", 34 | "medusa-react": "9.0.18", 35 | "@mui/material": "^5.15.3", 36 | "typeorm": "^0.3.16", 37 | "react-hook-form": "^7.49.2", 38 | "@emotion/react": "^11.11.3", 39 | "@emotion/styled": "11.13.0", 40 | "@types/email-templates": "10.0.4", 41 | "email-templates": "12.0.1", 42 | "htmling": "^0.0.8" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "^7.14.3", 46 | "@babel/core": "^7.14.3", 47 | "@medusajs/medusa": "^1.20.6", 48 | "@types/express": "^4.17.13", 49 | "babel-preset-medusa-package": "^1.1.19", 50 | "cross-env": "^7.0.3", 51 | "eslint": "^6.8.0", 52 | "rimraf": "^3.0.2", 53 | "typescript": "^4.5.2" 54 | }, 55 | "peerDependencies": { 56 | "@medusajs/medusa": "^1.20.6", 57 | "react": "^18.2.0", 58 | "react-router-dom": "^6.13.0" 59 | }, 60 | "jest": { 61 | "globals": { 62 | "ts-jest": { 63 | "tsconfig": "tsconfig.spec.json" 64 | } 65 | }, 66 | "moduleFileExtensions": [ 67 | "js", 68 | "json", 69 | "ts" 70 | ], 71 | "testPathIgnorePatterns": [ 72 | "/node_modules/", 73 | "/node_modules/" 74 | ], 75 | "rootDir": "src", 76 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$", 77 | "transform": { 78 | ".ts": "ts-jest" 79 | }, 80 | "collectCoverageFrom": [ 81 | "**/*.(t|j)s" 82 | ], 83 | "coverageDirectory": "./coverage", 84 | "testEnvironment": "node" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/admin/routes/marketing/settings/settings-info-drawer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { LightBulb } from "@medusajs/icons" 14 | import { Badge, Button, Drawer, Heading, IconButton, Text } from "@medusajs/ui" 15 | import { EmailSentType } from "../../../../ui-components/types/models" 16 | import { BackInStockDocumentation } from "./back-in-stock/documentation" 17 | import { WelcomeDocumentation } from "./welcome/documentation" 18 | import { Grid } from "@mui/material" 19 | import { MessageDocumentation } from "./types" 20 | 21 | const parameters: string[] = [ 22 | "first_name", 23 | "last_name", 24 | "email" 25 | ] 26 | 27 | export function InfoDrawer({ emailType } : { emailType: EmailSentType}) { 28 | let documentation: MessageDocumentation | undefined; 29 | 30 | switch (emailType) { 31 | case EmailSentType.BACK_IN_STOCK: 32 | documentation = BackInStockDocumentation; 33 | break; 34 | case EmailSentType.WELCOME: 35 | documentation = WelcomeDocumentation; 36 | break; 37 | } 38 | 39 | if (documentation) { 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Message details 50 | 51 | 52 | 53 | 54 | Trigger 55 | 56 | {documentation.trigger && 57 | customer.created 58 | } 59 | 60 | 61 | {documentation.triggerDescription} 62 | 63 | 64 | 65 | Available parameters 66 | 67 | 68 | 69 | { 70 | `These parameters you can use in your templates` 71 | } 72 | 73 | 74 | {documentation.availableParameters.map(parameter => ( 75 | 76 | {parameter} 77 | 78 | ))} 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ) 89 | } else { 90 | return ( 91 | 92 | 93 | 94 | ) 95 | } 96 | } -------------------------------------------------------------------------------- /data/seed-onboarding.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "currencies": [ 4 | "eur", 5 | "usd" 6 | ] 7 | }, 8 | "users": [], 9 | "regions": [ 10 | { 11 | "id": "test-region-eu", 12 | "name": "EU", 13 | "currency_code": "eur", 14 | "tax_rate": 0, 15 | "payment_providers": [ 16 | "manual" 17 | ], 18 | "fulfillment_providers": [ 19 | "manual" 20 | ], 21 | "countries": [ 22 | "gb", 23 | "de", 24 | "dk", 25 | "se", 26 | "fr", 27 | "es", 28 | "it" 29 | ] 30 | }, 31 | { 32 | "id": "test-region-na", 33 | "name": "NA", 34 | "currency_code": "usd", 35 | "tax_rate": 0, 36 | "payment_providers": [ 37 | "manual" 38 | ], 39 | "fulfillment_providers": [ 40 | "manual" 41 | ], 42 | "countries": [ 43 | "us", 44 | "ca" 45 | ] 46 | } 47 | ], 48 | "shipping_options": [ 49 | { 50 | "name": "PostFake Standard", 51 | "region_id": "test-region-eu", 52 | "provider_id": "manual", 53 | "data": { 54 | "id": "manual-fulfillment" 55 | }, 56 | "price_type": "flat_rate", 57 | "amount": 1000 58 | }, 59 | { 60 | "name": "PostFake Express", 61 | "region_id": "test-region-eu", 62 | "provider_id": "manual", 63 | "data": { 64 | "id": "manual-fulfillment" 65 | }, 66 | "price_type": "flat_rate", 67 | "amount": 1500 68 | }, 69 | { 70 | "name": "PostFake Return", 71 | "region_id": "test-region-eu", 72 | "provider_id": "manual", 73 | "data": { 74 | "id": "manual-fulfillment" 75 | }, 76 | "price_type": "flat_rate", 77 | "is_return": true, 78 | "amount": 1000 79 | }, 80 | { 81 | "name": "I want to return it myself", 82 | "region_id": "test-region-eu", 83 | "provider_id": "manual", 84 | "data": { 85 | "id": "manual-fulfillment" 86 | }, 87 | "price_type": "flat_rate", 88 | "is_return": true, 89 | "amount": 0 90 | }, 91 | { 92 | "name": "FakeEx Standard", 93 | "region_id": "test-region-na", 94 | "provider_id": "manual", 95 | "data": { 96 | "id": "manual-fulfillment" 97 | }, 98 | "price_type": "flat_rate", 99 | "amount": 800 100 | }, 101 | { 102 | "name": "FakeEx Express", 103 | "region_id": "test-region-na", 104 | "provider_id": "manual", 105 | "data": { 106 | "id": "manual-fulfillment" 107 | }, 108 | "price_type": "flat_rate", 109 | "amount": 1200 110 | }, 111 | { 112 | "name": "FakeEx Return", 113 | "region_id": "test-region-na", 114 | "provider_id": "manual", 115 | "data": { 116 | "id": "manual-fulfillment" 117 | }, 118 | "price_type": "flat_rate", 119 | "is_return": true, 120 | "amount": 800 121 | }, 122 | { 123 | "name": "I want to return it myself", 124 | "region_id": "test-region-na", 125 | "provider_id": "manual", 126 | "data": { 127 | "id": "manual-fulfillment" 128 | }, 129 | "price_type": "flat_rate", 130 | "is_return": true, 131 | "amount": 0 132 | } 133 | ], 134 | "products": [], 135 | "categories": [], 136 | "publishable_api_keys": [ 137 | { 138 | "title": "Development" 139 | } 140 | ] 141 | } -------------------------------------------------------------------------------- /src/api/store/api-definition.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | version: "1.0" 4 | title: "Subscriptions" 5 | description: | 6 | This API provides possibility to subscribe for various entities. IMPORTANT: This API requires logged-in customer. For more info see: https://docs.medusajs.com/development/api-routes/create#protect-store-api-routes 7 | paths: 8 | /store/me/subscribe/back-in-stock: 9 | post: 10 | tags: 11 | - "back-in-stock" 12 | summary: "Subscribe for variant when will be back in stock" 13 | parameters: 14 | - name: "variantId" 15 | in: "query" 16 | required: true 17 | schema: 18 | type: "string" 19 | requestBody: 20 | description: Variant to subscribe 21 | required: true 22 | content: 23 | application/json: 24 | schema: 25 | type: object 26 | properties: 27 | variantId: 28 | type: string 29 | nullable: false 30 | 31 | responses: 32 | "201": 33 | description: "Return subscription object" 34 | content: 35 | application/json: 36 | schema: 37 | type: object 38 | properties: 39 | result: 40 | $ref: '#/components/schemas/MarketingSubscription' 41 | "400": 42 | description: "Not OK" 43 | content: 44 | application/json: 45 | schema: 46 | type: object 47 | properties: 48 | message: 49 | type: string 50 | nullable: false 51 | /store/me/unsubscribe: 52 | post: 53 | tags: 54 | - "unsubscribe" 55 | summary: "Unsubscribe" 56 | parameters: 57 | - name: "emailType" 58 | in: "query" 59 | required: true 60 | schema: 61 | type: "string" 62 | - name: "targetId" 63 | in: "query" 64 | required: false 65 | schema: 66 | type: "string" 67 | requestBody: 68 | description: Unsubscribe for email type (required) and targetId (optional). TargetId is needed if you subscribed already for some entity (e.g. like for variantId). 69 | required: true 70 | content: 71 | application/json: 72 | schema: 73 | type: object 74 | properties: 75 | emailType: 76 | type: string 77 | nullable: false 78 | targetId: 79 | type: string 80 | nullable: false 81 | responses: 82 | "201": 83 | description: "Return result of unsubscription" 84 | content: 85 | application/json: 86 | schema: 87 | type: object 88 | properties: 89 | result: 90 | type: boolean 91 | nullable: false 92 | "400": 93 | description: "Not OK" 94 | content: 95 | application/json: 96 | schema: 97 | type: object 98 | properties: 99 | message: 100 | type: string 101 | nullable: false 102 | 103 | 104 | 105 | components: 106 | schemas: 107 | MarketingSubscription: 108 | type: object 109 | properties: 110 | id: 111 | type: string 112 | nullable: false 113 | customer: 114 | type: object 115 | nullable: false 116 | email_type: 117 | type: string 118 | nullable: false 119 | target_id: 120 | type: string 121 | nullable: false 122 | -------------------------------------------------------------------------------- /src/ui-components/common/email-preview.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 RSC-Labs, https://rsoftcon.com/ 3 | * 4 | * MIT License 5 | * 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, 8 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * See the License for the specific language governing permissions and 10 | * limitations under the License. 11 | */ 12 | 13 | import { useEffect, useRef } from 'react'; 14 | import { useAdminCustomQuery } from "medusa-react" 15 | import { CircularProgress, Grid } from "@mui/material"; 16 | import { Alert, Heading, Container, Text } from "@medusajs/ui" 17 | import { EmailSentType } from "../types/models"; 18 | 19 | type EmailPreview = { 20 | htmlContent: string 21 | } 22 | 23 | export type AdminMarketingEmailPreviewQueryReq = { 24 | templateType: string, 25 | templateName: string, 26 | emailType: string 27 | }; 28 | export type AdminMarketingEmailPreviewResponse = { 29 | result?: EmailPreview 30 | } 31 | 32 | const HtmlPreview = ({ htmlContent }) => { 33 | const iframeRef = useRef(null); 34 | 35 | useEffect(() => { 36 | if (iframeRef.current) { 37 | const iframeDoc = iframeRef.current.contentDocument || iframeRef.current.contentWindow.document; 38 | iframeDoc.open(); 39 | iframeDoc.write(htmlContent); 40 | iframeDoc.close(); 41 | } 42 | }, [htmlContent]); 43 | 44 | return ( 45 |