init
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SimplexModule } from '../simplex/SimplexModule';
|
||||
import { ShopSettingsController } from './controllers/ShopSettingsController';
|
||||
import { ShopSettings } from './entities/ShopSettings';
|
||||
import { ShopSettingsService } from './services/ShopSettingsService';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ShopSettings]), SimplexModule],
|
||||
controllers: [ShopSettingsController],
|
||||
providers: [ShopSettingsService],
|
||||
exports: [ShopSettingsService]
|
||||
})
|
||||
export class ShopSettingsModule {}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getShopFaviconMulterConfig } from '../../../config';
|
||||
import { getShopBrandingDir } from '../../../config/uploadPaths';
|
||||
import { createDiskStorageUploadOptions } from '../../../utils/createDiskStorageUploadOptions';
|
||||
import { createUploadFilePipe } from '../../../utils/createUploadFilePipe';
|
||||
|
||||
const uploadDir = getShopBrandingDir();
|
||||
|
||||
export const shopFaviconUploadOptions = createDiskStorageUploadOptions(uploadDir);
|
||||
|
||||
const { allowedMimes, maxFileBytes } = getShopFaviconMulterConfig();
|
||||
|
||||
export const shopFaviconFilePipe = createUploadFilePipe(allowedMimes, maxFileBytes);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getShopLogoMulterConfig } from '../../../config';
|
||||
import { getShopBrandingDir } from '../../../config/uploadPaths';
|
||||
import { createDiskStorageUploadOptions } from '../../../utils/createDiskStorageUploadOptions';
|
||||
import { createUploadFilePipe } from '../../../utils/createUploadFilePipe';
|
||||
|
||||
const uploadDir = getShopBrandingDir();
|
||||
|
||||
export const shopLogoUploadOptions = createDiskStorageUploadOptions(uploadDir);
|
||||
|
||||
const { allowedMimes, maxFileBytes } = getShopLogoMulterConfig();
|
||||
|
||||
export const shopLogoFilePipe = createUploadFilePipe(allowedMimes, maxFileBytes);
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Patch,
|
||||
Post,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { JwtGuard } from '../../../guards/JwtGuard';
|
||||
import { shopFaviconFilePipe, shopFaviconUploadOptions } from '../config/shopFaviconUpload';
|
||||
import { shopLogoFilePipe, shopLogoUploadOptions } from '../config/shopLogoUpload';
|
||||
import { ConnectSimplexNotificationsDto } from '../dto/ConnectSimplexNotificationsDto';
|
||||
import { UpdateNotificationsDto } from '../dto/UpdateNotificationsDto';
|
||||
import { UpdateShippingNoteDto } from '../dto/UpdateShippingNoteDto';
|
||||
import { UpdateSimplexLinkDto } from '../dto/UpdateSimplexLinkDto';
|
||||
import { ShopSettingsService } from '../services/ShopSettingsService';
|
||||
|
||||
@Controller('shop-settings')
|
||||
@UseGuards(JwtGuard)
|
||||
export class ShopSettingsController {
|
||||
constructor(private readonly shopSettingsService: ShopSettingsService) {}
|
||||
|
||||
@Get('/')
|
||||
get() {
|
||||
return this.shopSettingsService.getView();
|
||||
}
|
||||
|
||||
@Patch('/simplex-link')
|
||||
updateSimplexLink(@Body() dto: UpdateSimplexLinkDto) {
|
||||
return this.shopSettingsService.updateSimplexLink(dto);
|
||||
}
|
||||
|
||||
@Patch('/shipping-note')
|
||||
updateShippingNote(@Body() dto: UpdateShippingNoteDto) {
|
||||
return this.shopSettingsService.updateShippingNote(dto);
|
||||
}
|
||||
|
||||
@Patch('/notifications')
|
||||
updateNotifications(@Body() dto: UpdateNotificationsDto) {
|
||||
return this.shopSettingsService.updateNotifications(dto);
|
||||
}
|
||||
|
||||
@Post('/simplex-connect')
|
||||
connectSimplexNotifications(@Body() dto: ConnectSimplexNotificationsDto) {
|
||||
return this.shopSettingsService.connectSimplexNotifications(dto.simplexNotificationLink);
|
||||
}
|
||||
|
||||
@Post('/logo')
|
||||
@UseInterceptors(FileInterceptor('file', shopLogoUploadOptions))
|
||||
uploadLogo(@UploadedFile(shopLogoFilePipe) file: Express.Multer.File) {
|
||||
return this.shopSettingsService.uploadLogo(file.filename);
|
||||
}
|
||||
|
||||
@Post('/favicon')
|
||||
@UseInterceptors(FileInterceptor('file', shopFaviconUploadOptions))
|
||||
uploadFavicon(@UploadedFile(shopFaviconFilePipe) file: Express.Multer.File) {
|
||||
return this.shopSettingsService.uploadFavicon(file.filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
export class ConnectSimplexNotificationsDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MaxLength(512)
|
||||
simplexNotificationLink: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IsBoolean, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class UpdateNotificationsDto {
|
||||
@IsNotEmpty()
|
||||
@IsBoolean()
|
||||
notificationsEnabled: boolean;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsBoolean()
|
||||
notifyOnNewOrder: boolean;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsBoolean()
|
||||
notifyOnOrderMessage: boolean;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
|
||||
|
||||
import { getAppConfig } from '../../../config';
|
||||
|
||||
const {
|
||||
validation: { shippingNoteMinLength, shippingNoteMaxLength }
|
||||
} = getAppConfig();
|
||||
|
||||
export class UpdateShippingNoteDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(shippingNoteMinLength)
|
||||
@MaxLength(shippingNoteMaxLength)
|
||||
shippingNote: string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
export class UpdateSimplexLinkDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MaxLength(512)
|
||||
simplexLink: string;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('shop_settings')
|
||||
export class ShopSettings {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
logoStorageKey: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
faviconStorageKey: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
simplexLink: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
simplexNotificationLink: string | null;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
shippingNote: string | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
notificationsEnabled: boolean;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
notifyOnNewOrder: boolean;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
notifyOnOrderMessage: boolean;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
simplexNotificationContactId: number | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { ConfigService } from '@nestjs/config';
|
||||
import type { Repository } from 'typeorm';
|
||||
import type { SimplexChatClient } from '../../simplex/services/SimplexChatClient';
|
||||
import { ShopSettings } from '../entities/ShopSettings';
|
||||
import { ShopSettingsService } from './ShopSettingsService';
|
||||
|
||||
describe('ShopSettingsService', () => {
|
||||
let service: ShopSettingsService;
|
||||
let shopSettingsRepo: {
|
||||
find: jest.Mock;
|
||||
update: jest.Mock;
|
||||
};
|
||||
let configService: {
|
||||
get: jest.Mock;
|
||||
};
|
||||
|
||||
const settings = {
|
||||
id: 'settings-1',
|
||||
logoStorageKey: 'logo.png',
|
||||
faviconStorageKey: null,
|
||||
simplexLink: 'https://simplex.example',
|
||||
simplexNotificationLink: null,
|
||||
shippingNote: 'Ships in 3 days',
|
||||
notificationsEnabled: false,
|
||||
notifyOnNewOrder: false,
|
||||
notifyOnOrderMessage: false,
|
||||
simplexNotificationContactId: null,
|
||||
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-01-01T00:00:00.000Z')
|
||||
} as ShopSettings;
|
||||
|
||||
beforeEach(() => {
|
||||
shopSettingsRepo = {
|
||||
find: jest.fn().mockResolvedValue([settings]),
|
||||
update: jest.fn().mockResolvedValue(undefined)
|
||||
};
|
||||
|
||||
configService = {
|
||||
get: jest.fn((key: string) => {
|
||||
if (key === 'shopSettings') {
|
||||
return { shopName: 'Test Shop', shopFiatCurrency: 'USD', monero: {} };
|
||||
}
|
||||
|
||||
if (key === 'app') {
|
||||
return { validation: { shippingNoteMinLength: 10 } };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})
|
||||
};
|
||||
|
||||
service = new ShopSettingsService(
|
||||
shopSettingsRepo as unknown as Repository<ShopSettings>,
|
||||
configService as unknown as ConfigService,
|
||||
{} as unknown as SimplexChatClient
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when shop settings have not been initialized', async () => {
|
||||
shopSettingsRepo.find.mockResolvedValue([]);
|
||||
|
||||
await expect(service.findSettings()).rejects.toThrow('Shop settings have not been initialized');
|
||||
});
|
||||
|
||||
it('returns storefront branding urls and public fields', async () => {
|
||||
await expect(service.getStorefrontBranding()).resolves.toEqual({
|
||||
logoUrl: '/uploads/public/shop/logo.png',
|
||||
faviconUrl: null,
|
||||
simplexLink: 'https://simplex.example',
|
||||
shippingNote: 'Ships in 3 days'
|
||||
});
|
||||
});
|
||||
|
||||
it('trims simplex links on update', async () => {
|
||||
await service.updateSimplexLink({ simplexLink: ' https://simplex.example/new ' });
|
||||
|
||||
expect(shopSettingsRepo.update).toHaveBeenCalledWith('settings-1', {
|
||||
simplexLink: 'https://simplex.example/new'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
import { BadGatewayException, Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { getShopBrandingPublicUrl, resolveShopBrandingPath } from '../../../config/uploadPaths';
|
||||
import { Config } from '../../../types/Config';
|
||||
import { removeFileFromDisk } from '../../../utils/removeFileFromDisk';
|
||||
import { SimplexChatClient } from '../../simplex/services/SimplexChatClient';
|
||||
import { UpdateNotificationsDto } from '../dto/UpdateNotificationsDto';
|
||||
import { UpdateShippingNoteDto } from '../dto/UpdateShippingNoteDto';
|
||||
import { UpdateSimplexLinkDto } from '../dto/UpdateSimplexLinkDto';
|
||||
import { ShopSettings } from '../entities/ShopSettings';
|
||||
import { SetupChecklist } from '../types/SetupChecklist';
|
||||
import { ShopSettingsView } from '../types/ShopSettingsView';
|
||||
import { StorefrontBranding } from '../types/StorefrontBranding';
|
||||
|
||||
@Injectable()
|
||||
export class ShopSettingsService {
|
||||
private readonly logger = new Logger(ShopSettingsService.name);
|
||||
|
||||
private connectingSimplexPromise: Promise<ShopSettingsView> | null = null;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ShopSettings)
|
||||
private readonly shopSettingsRepo: Repository<ShopSettings>,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly simplexChatClient: SimplexChatClient
|
||||
) {}
|
||||
|
||||
async getView(): Promise<ShopSettingsView> {
|
||||
const settings = await this.findSettings();
|
||||
|
||||
return this.toView(settings);
|
||||
}
|
||||
|
||||
async getStorefrontBranding(): Promise<StorefrontBranding> {
|
||||
const settings = await this.findSettings();
|
||||
|
||||
const logoUrl = settings.logoStorageKey ? getShopBrandingPublicUrl(settings.logoStorageKey) : null;
|
||||
const faviconUrl = settings.faviconStorageKey ? getShopBrandingPublicUrl(settings.faviconStorageKey) : null;
|
||||
|
||||
return {
|
||||
logoUrl,
|
||||
faviconUrl,
|
||||
simplexLink: settings.simplexLink,
|
||||
shippingNote: settings.shippingNote
|
||||
};
|
||||
}
|
||||
|
||||
async findSettings(): Promise<ShopSettings> {
|
||||
const [settings] = await this.shopSettingsRepo.find({ take: 1 });
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Shop settings have not been initialized');
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
async updateSimplexLink({ simplexLink }: UpdateSimplexLinkDto): Promise<ShopSettingsView> {
|
||||
const settings = await this.findSettings();
|
||||
|
||||
await this.shopSettingsRepo.update(settings.id, {
|
||||
simplexLink: simplexLink.trim()
|
||||
});
|
||||
|
||||
return this.getView();
|
||||
}
|
||||
|
||||
async updateShippingNote({ shippingNote }: UpdateShippingNoteDto): Promise<ShopSettingsView> {
|
||||
const settings = await this.findSettings();
|
||||
|
||||
await this.shopSettingsRepo.update(settings.id, {
|
||||
shippingNote: shippingNote.trim()
|
||||
});
|
||||
|
||||
return this.getView();
|
||||
}
|
||||
|
||||
async updateNotifications({
|
||||
notificationsEnabled,
|
||||
notifyOnNewOrder,
|
||||
notifyOnOrderMessage
|
||||
}: UpdateNotificationsDto): Promise<ShopSettingsView> {
|
||||
const settings = await this.findSettings();
|
||||
|
||||
await this.shopSettingsRepo.update(settings.id, {
|
||||
notificationsEnabled,
|
||||
notifyOnNewOrder,
|
||||
notifyOnOrderMessage
|
||||
});
|
||||
|
||||
return this.getView();
|
||||
}
|
||||
|
||||
async uploadLogo(filename: string): Promise<ShopSettingsView> {
|
||||
const settings = await this.findSettings();
|
||||
|
||||
const previousLogoStorageKey = settings.logoStorageKey;
|
||||
|
||||
await this.shopSettingsRepo.update(settings.id, { logoStorageKey: filename });
|
||||
|
||||
if (previousLogoStorageKey) {
|
||||
const previousLogoPath = resolveShopBrandingPath(previousLogoStorageKey);
|
||||
|
||||
await removeFileFromDisk(previousLogoPath, ShopSettingsService.name);
|
||||
}
|
||||
|
||||
return this.getView();
|
||||
}
|
||||
|
||||
async uploadFavicon(filename: string): Promise<ShopSettingsView> {
|
||||
const settings = await this.findSettings();
|
||||
|
||||
const previousFaviconStorageKey = settings.faviconStorageKey;
|
||||
|
||||
await this.shopSettingsRepo.update(settings.id, { faviconStorageKey: filename });
|
||||
|
||||
if (previousFaviconStorageKey) {
|
||||
const previousFaviconPath = resolveShopBrandingPath(previousFaviconStorageKey);
|
||||
|
||||
await removeFileFromDisk(previousFaviconPath, ShopSettingsService.name);
|
||||
}
|
||||
|
||||
return this.getView();
|
||||
}
|
||||
|
||||
async connectSimplexNotifications(simplexNotificationLink: string): Promise<ShopSettingsView> {
|
||||
if (this.connectingSimplexPromise) {
|
||||
return this.connectingSimplexPromise;
|
||||
}
|
||||
|
||||
this.connectingSimplexPromise = this.runConnectSimplexNotifications(simplexNotificationLink).finally(() => {
|
||||
this.connectingSimplexPromise = null;
|
||||
});
|
||||
|
||||
return this.connectingSimplexPromise;
|
||||
}
|
||||
|
||||
private async runConnectSimplexNotifications(simplexNotificationLink: string): Promise<ShopSettingsView> {
|
||||
const trimmedLink = simplexNotificationLink.trim();
|
||||
|
||||
try {
|
||||
await this.updateSimplexNotificationLink(trimmedLink);
|
||||
|
||||
const contactId = await this.simplexChatClient.connect(trimmedLink);
|
||||
|
||||
await this.setSimplexNotificationContactId(contactId);
|
||||
|
||||
this.logger.log(`SimpleX notification contact ready (id=${contactId})`);
|
||||
|
||||
return this.getView();
|
||||
} catch {
|
||||
await this.setSimplexNotificationContactId(null);
|
||||
|
||||
this.logger.warn('Failed to connect SimpleX notification contact');
|
||||
|
||||
throw new BadGatewayException('Failed to connect to SimpleX');
|
||||
}
|
||||
}
|
||||
|
||||
async updateSimplexNotificationLink(simplexNotificationLink: string): Promise<void> {
|
||||
const settings = await this.findSettings();
|
||||
|
||||
const trimmed = simplexNotificationLink.trim();
|
||||
|
||||
const linkChanged = settings.simplexNotificationLink !== trimmed;
|
||||
|
||||
await this.shopSettingsRepo.update(settings.id, {
|
||||
simplexNotificationLink: trimmed,
|
||||
...(linkChanged ? { simplexNotificationContactId: null } : {})
|
||||
});
|
||||
}
|
||||
|
||||
private async setSimplexNotificationContactId(contactId: number | null): Promise<void> {
|
||||
const settings = await this.findSettings();
|
||||
|
||||
await this.shopSettingsRepo.update(settings.id, {
|
||||
simplexNotificationContactId: contactId
|
||||
});
|
||||
}
|
||||
|
||||
private toView({
|
||||
id,
|
||||
logoStorageKey,
|
||||
faviconStorageKey,
|
||||
simplexLink,
|
||||
simplexNotificationLink,
|
||||
shippingNote,
|
||||
notificationsEnabled,
|
||||
notifyOnNewOrder,
|
||||
notifyOnOrderMessage,
|
||||
simplexNotificationContactId,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}: ShopSettings): ShopSettingsView {
|
||||
const setupChecklist = this.buildSetupChecklist({ logoStorageKey, faviconStorageKey, simplexLink, shippingNote });
|
||||
|
||||
const { shopName, shopFiatCurrency, monero } = this.configService.get('shopSettings') as Config['shopSettings'];
|
||||
|
||||
const logoUrl = logoStorageKey ? getShopBrandingPublicUrl(logoStorageKey) : null;
|
||||
const faviconUrl = faviconStorageKey ? getShopBrandingPublicUrl(faviconStorageKey) : null;
|
||||
|
||||
const isSetupComplete = this.isSetupComplete(setupChecklist);
|
||||
|
||||
return {
|
||||
id,
|
||||
shopName,
|
||||
shopFiatCurrency,
|
||||
monero,
|
||||
logoUrl,
|
||||
faviconUrl,
|
||||
simplexLink,
|
||||
simplexNotificationLink,
|
||||
shippingNote,
|
||||
notificationsEnabled,
|
||||
notifyOnNewOrder,
|
||||
notifyOnOrderMessage,
|
||||
simplexNotificationConnected: simplexNotificationContactId !== null,
|
||||
isSetupComplete,
|
||||
setupChecklist,
|
||||
createdAt,
|
||||
updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
private buildSetupChecklist({
|
||||
logoStorageKey,
|
||||
faviconStorageKey,
|
||||
simplexLink,
|
||||
shippingNote
|
||||
}: Pick<ShopSettings, 'logoStorageKey' | 'faviconStorageKey' | 'simplexLink' | 'shippingNote'>): SetupChecklist {
|
||||
const {
|
||||
validation: { shippingNoteMinLength }
|
||||
} = this.configService.get('app') as Config['app'];
|
||||
|
||||
return {
|
||||
logo: logoStorageKey !== null,
|
||||
favicon: faviconStorageKey !== null,
|
||||
simplexLink: (simplexLink?.length ?? 0) > 0,
|
||||
shippingNote: (shippingNote?.length ?? 0) >= shippingNoteMinLength
|
||||
};
|
||||
}
|
||||
|
||||
private isSetupComplete(checklist: SetupChecklist): boolean {
|
||||
return checklist.logo && checklist.favicon && checklist.simplexLink && checklist.shippingNote;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface SetupChecklist {
|
||||
logo: boolean;
|
||||
favicon: boolean;
|
||||
simplexLink: boolean;
|
||||
shippingNote: boolean;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { MoneroConfirmationTier } from '../../../types/MoneroConfirmationTier';
|
||||
|
||||
export interface ShopSettingsMoneroView {
|
||||
confirmationTiers: MoneroConfirmationTier[];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ShopFiatCurrency } from '../../../types/ShopFiatCurrency';
|
||||
import { SetupChecklist } from './SetupChecklist';
|
||||
import { ShopSettingsMoneroView } from './ShopSettingsMoneroView';
|
||||
|
||||
export interface ShopSettingsView {
|
||||
id: string | null;
|
||||
shopName: string;
|
||||
shopFiatCurrency: ShopFiatCurrency;
|
||||
monero: ShopSettingsMoneroView;
|
||||
logoUrl: string | null;
|
||||
faviconUrl: string | null;
|
||||
simplexLink: string | null;
|
||||
simplexNotificationLink: string | null;
|
||||
shippingNote: string | null;
|
||||
notificationsEnabled: boolean;
|
||||
notifyOnNewOrder: boolean;
|
||||
notifyOnOrderMessage: boolean;
|
||||
simplexNotificationConnected: boolean;
|
||||
isSetupComplete: boolean;
|
||||
setupChecklist: SetupChecklist;
|
||||
createdAt: Date | null;
|
||||
updatedAt: Date | null;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface StorefrontBranding {
|
||||
logoUrl: string | null;
|
||||
faviconUrl: string | null;
|
||||
simplexLink: string | null;
|
||||
shippingNote: string | null;
|
||||
}
|
||||
Reference in New Issue
Block a user