From a47dd13eeae0ed7bb07c65b73d8e2ab37ede2c1a Mon Sep 17 00:00:00 2001 From: dschom Date: Wed, 29 Oct 2025 16:20:11 -0700 Subject: [PATCH 1/4] task(libs/account): Port app errors to libs/accounts/errors Because: - We will need this for subsequent efforts This commit: - Ports `oauth/errors.js` and `errors.js` from auth-server to libs --- libs/accounts/errors/.eslintrc.json | 18 + libs/accounts/errors/README.md | 11 + libs/accounts/errors/jest.config.ts | 21 + libs/accounts/errors/package.json | 9 + libs/accounts/errors/project.json | 37 + libs/accounts/errors/src/app-error.ts | 1837 +++++++++++++++++++++++ libs/accounts/errors/src/constants.ts | 220 +++ libs/accounts/errors/src/index.spec.ts | 337 +++++ libs/accounts/errors/src/index.ts | 6 + libs/accounts/errors/src/oauth-error.ts | 432 ++++++ libs/accounts/errors/src/util.ts | 18 + libs/accounts/errors/tsconfig.json | 23 + libs/accounts/errors/tsconfig.lib.json | 10 + libs/accounts/errors/tsconfig.spec.json | 14 + package.json | 1 + tsconfig.base.json | 1 + yarn.lock | 65 + 17 files changed, 3060 insertions(+) create mode 100644 libs/accounts/errors/.eslintrc.json create mode 100644 libs/accounts/errors/README.md create mode 100644 libs/accounts/errors/jest.config.ts create mode 100644 libs/accounts/errors/package.json create mode 100644 libs/accounts/errors/project.json create mode 100644 libs/accounts/errors/src/app-error.ts create mode 100644 libs/accounts/errors/src/constants.ts create mode 100644 libs/accounts/errors/src/index.spec.ts create mode 100644 libs/accounts/errors/src/index.ts create mode 100644 libs/accounts/errors/src/oauth-error.ts create mode 100644 libs/accounts/errors/src/util.ts create mode 100644 libs/accounts/errors/tsconfig.json create mode 100644 libs/accounts/errors/tsconfig.lib.json create mode 100644 libs/accounts/errors/tsconfig.spec.json diff --git a/libs/accounts/errors/.eslintrc.json b/libs/accounts/errors/.eslintrc.json new file mode 100644 index 00000000000..3456be9b903 --- /dev/null +++ b/libs/accounts/errors/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/accounts/errors/README.md b/libs/accounts/errors/README.md new file mode 100644 index 00000000000..55b6779bf1d --- /dev/null +++ b/libs/accounts/errors/README.md @@ -0,0 +1,11 @@ +# account-errors + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build accounts-errors` to build the library. + +## Running unit tests + +Run `nx test-unit accounts-errors` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/accounts/errors/jest.config.ts b/libs/accounts/errors/jest.config.ts new file mode 100644 index 00000000000..7759880e47b --- /dev/null +++ b/libs/accounts/errors/jest.config.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ +export default { + displayName: 'accounts-errors', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/accounts/errors', + reporters: [ + 'default', + [ + 'jest-junit', + { + outputDirectory: 'artifacts/tests/lib/accounts/errors', + outputName: 'accounts-errors-jest-unit-results.xml', + }, + ], + ], +}; diff --git a/libs/accounts/errors/package.json b/libs/accounts/errors/package.json new file mode 100644 index 00000000000..a4033ae3b7f --- /dev/null +++ b/libs/accounts/errors/package.json @@ -0,0 +1,9 @@ +{ + "name": "@fxa/accounts/errors", + "version": "0.0.1", + "dependencies": {}, + "type": "commonjs", + "main": "./index.cjs", + "types": "./index.d.ts", + "private": true +} diff --git a/libs/accounts/errors/project.json b/libs/accounts/errors/project.json new file mode 100644 index 00000000000..312a91894ee --- /dev/null +++ b/libs/accounts/errors/project.json @@ -0,0 +1,37 @@ +{ + "name": "accounts-errors", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/accounts/errors/src", + "projectType": "library", + "tags": ["scope:shared:lib"], + "targets": { + "build": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/accounts/errors", + "main": "libs/accounts/errors/src/index.ts", + "tsConfig": "libs/accounts/errors/tsconfig.lib.json", + "assets": ["libs/accounts/errors/*.md"], + "format": ["cjs"], + "generatePackageJson": true + } + }, + "test-unit": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/accounts/errors/jest.config.ts", + "testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"] + } + }, + "test-integration": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/accounts/errors/jest.config.ts", + "testPathPattern": ["\\.in\\.spec\\.ts$"] + } + } + } +} diff --git a/libs/accounts/errors/src/app-error.ts b/libs/accounts/errors/src/app-error.ts new file mode 100644 index 00000000000..8739352a39c --- /dev/null +++ b/libs/accounts/errors/src/app-error.ts @@ -0,0 +1,1837 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + BAD_SIGNATURE_ERRORS, + ERRNO, + ERRNO_REVERSE_MAP, + TOO_LARGE, + DEFAULT_ERRROR, + IGNORED_ERROR_NUMBERS, + DEBUGGABLE_PAYLOAD_KEYS, +} from './constants'; +import { OauthError } from './oauth-error'; +import { Request as HapiRequest } from 'hapi'; + +/** + * Augmented Hapi request. Extends request.app interface auth-server specific feilds that this lib uses. + */ +export type Request = Partial> & { + app: { + acceptLanguage: string; + locale: string; + ua: { + os: string; + osVersion: string; + }; + geo: { + city: string; + state: string; + }; + devices: Promise>; + metricsContext: Promise<{ + service: string; + }>; + }; + route: { + path: string; + }; +}; + +/** + * Type guard to determine if an error is of AppError type. + */ +export function isAppError(error: any): error is AppError { + if ( + typeof error.errno === 'number' && + typeof error.code === 'number' && + typeof error.message === 'string' + ) { + return true; + } + return false; +} + +/** + * Represents known / sanctioned application errors. + */ +export class AppError extends Error { + // Rereference ERRNO for backward compatibility reasons + static readonly ERRNO = ERRNO; + + /** Hapi specific field. Indicating unhandled error.*/ + public isBoom: boolean; + + /** VError specific field. Used for wrapping errors. */ + public jse_cause?: AppError | Error; + + /** The standardized error number. See ERRNO's. */ + public errno: number; + + /** The status code. Maps to http status code. */ + public code: number; + + /** Output state written to response object. */ + public output: { + statusCode: number; + payload: { + code: number; + errno: number; + error?: Error; + message: string; + info?: string; + request?: any; + log?: any; + data?: any; + retryAfter?: number; + retryAfterLocalized?: string; + }; + headers: Record; + }; + + constructor( + options: any, + extra?: Record, + headers?: Record, + error?: AppError | Error + ) { + super(options.message || DEFAULT_ERRROR.message); + this.isBoom = true; + this.stack = options.stack; + if (!this.stack) { + Error.captureStackTrace(this, AppError); + } + if (error) { + // This is where verror stores the error cause passed in. + this.jse_cause = error; + } + this.code = options.code || DEFAULT_ERRROR.code; + this.errno = options.errno || DEFAULT_ERRROR.errno; + this.output = { + statusCode: options.code || DEFAULT_ERRROR.code, + payload: { + code: options.code || DEFAULT_ERRROR.code, + errno: this.errno, + error: options.error || DEFAULT_ERRROR.error, + message: this.message, + info: options.info || DEFAULT_ERRROR.info, + retryAfter: extra?.['retryAfter'], + retryAfterLocalized: extra?.['retryAfterLocalized'], + }, + headers: headers || {}, + }; + Object.assign(this.output.payload, extra || {}); + } + + override toString() { + return `Error: ${this.message}`; + } + + header(name: string, value: string) { + this.output.headers[name] = value; + } + + backtrace(traced: string) { + this.output.payload.log = traced; + } + + /** + Translates an error from Hapi format to our format + */ + static translate( + request: Request, + response: + | AppError + | OauthError + | { + output: { + payload?: any; + }; + reason?: string; + stack?: string; + data?: any; + }, + oauthRoutes: Array<{ path: string; config: { cors: boolean } }> + ) { + let error; + if (response instanceof AppError) { + return response; + } + + if ( + request?.route?.path && + OauthError.isOauthRoute(request.route.path, oauthRoutes) + ) { + return OauthError.translate(response); + } else if (response instanceof OauthError) { + return AppError.appErrorFromOauthError(response); + } + const payload = response.output.payload; + const reason = response.reason; + if (!payload) { + error = AppError.unexpectedError(request); + } else if ( + payload.statusCode === 500 && + reason && + /(socket hang up|ECONNREFUSED)/.test(reason) + ) { + // A connection to a remote service either was not made or timed out. + if (response instanceof Error) { + error = AppError.backendServiceFailure( + undefined, + undefined, + undefined, + response + ); + } else { + error = AppError.backendServiceFailure(); + } + } else if (payload.statusCode === 401) { + // These are common errors generated by Hawk auth lib. + if ( + payload.message === 'Unknown credentials' || + payload.message === 'Invalid credentials' + ) { + error = AppError.invalidToken( + `Invalid authentication token: ${payload.message}` + ); + } else if (payload.message === 'Stale timestamp') { + error = AppError.invalidTimestamp(); + } else if (payload.message === 'Invalid nonce') { + error = AppError.invalidNonce(); + } else if (BAD_SIGNATURE_ERRORS.indexOf(payload.message) !== -1) { + error = AppError.invalidSignature(payload.message); + } else { + error = AppError.invalidToken( + `Invalid authentication token: ${payload.message}` + ); + } + } else if (payload.validation) { + if (payload.message?.includes('is required')) { + error = AppError.missingRequestParameter(payload.validation.keys[0]); + } else { + error = AppError.invalidRequestParameter(payload.validation); + } + } else if (payload.statusCode === 413 && TOO_LARGE.test(payload.message)) { + error = AppError.requestBodyTooLarge(); + } else { + error = new AppError({ + message: payload.message, + code: payload.statusCode, + error: payload.error, + errno: payload.errno, + info: payload.info, + stack: response.stack, + }); + + if (response.data) { + error.output.payload.data = JSON.stringify(response.data); + } + + if (payload.statusCode >= 500) { + AppError.decorateErrorWithRequest(error, request); + } + } + return error; + } + + static mapErrnoToKey(error: { errno: number }) { + const errno = error?.errno; + return ERRNO_REVERSE_MAP[errno]; + } + + // Helper functions for creating particular response types. + static dbIncorrectPatchLevel(level: string, levelRequired: boolean) { + return new AppError( + { + code: 400, + error: 'Server Startup', + errno: ERRNO.SERVER_CONFIG_ERROR, + message: 'Incorrect Database Patch Level', + }, + { + level: level, + levelRequired: levelRequired, + } + ); + } + + static backendServiceFailure( + service?: string, + operation?: string, + extra?: Record, + error?: Error + ) { + if (extra) { + return new AppError( + { + code: 500, + error: 'Internal Server Error', + errno: ERRNO.BACKEND_SERVICE_FAILURE, + message: 'System unavailable, try again soon', + }, + { + service, + operation, + ...extra, + }, + {}, + error + ); + } + return new AppError( + { + code: 500, + error: 'Internal Server Error', + errno: ERRNO.BACKEND_SERVICE_FAILURE, + message: 'System unavailable, try again soon', + }, + { + service, + operation, + }, + {}, + error + ); + } + + static accountExists(email: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.ACCOUNT_EXISTS, + message: 'Account already exists', + }, + { + email: email, + } + ); + } + + static unknownAccount(email: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.ACCOUNT_UNKNOWN, + message: 'Unknown account', + }, + { + email: email, + } + ); + } + + static incorrectPassword(dbEmail: string, requestEmail: string) { + if (dbEmail !== requestEmail) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INCORRECT_EMAIL_CASE, + message: 'Incorrect email case', + }, + { + email: dbEmail, + } + ); + } + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INCORRECT_PASSWORD, + message: 'Incorrect password', + }, + { + email: dbEmail, + } + ); + } + + static cannotCreatePassword() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.CANNOT_CREATE_PASSWORD, + message: 'Can not create password, password already set.', + }); + } + + static cannotLoginNoPasswordSet() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.UNABLE_TO_LOGIN_NO_PASSWORD_SET, + message: 'Complete account setup, please reset password to continue.', + }); + } + + static unverifiedAccount() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.ACCOUNT_UNVERIFIED, + message: 'Unconfirmed account', + }); + } + + static invalidVerificationCode(details: Record) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_VERIFICATION_CODE, + message: 'Invalid confirmation code', + }, + details + ); + } + + static invalidRequestBody() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_JSON, + message: 'Invalid JSON in request body', + }); + } + + static invalidRequestParameter(validation: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_PARAMETER, + message: 'Invalid parameter in request body', + }, + { + validation: validation, + } + ); + } + + static missingRequestParameter(param: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.MISSING_PARAMETER, + message: `Missing parameter in request body${param ? `: ${param}` : ''}`, + }, + { + param: param, + } + ); + } + + static invalidSignature(message?: string) { + return new AppError({ + code: 401, + error: 'Unauthorized', + errno: ERRNO.INVALID_REQUEST_SIGNATURE, + message: message || 'Invalid request signature', + }); + } + + static invalidToken(message?: string) { + return new AppError({ + code: 401, + error: 'Unauthorized', + errno: ERRNO.INVALID_TOKEN, + message: message || 'Invalid authentication token in request signature', + }); + } + + static invalidTimestamp() { + return new AppError( + { + code: 401, + error: 'Unauthorized', + errno: ERRNO.INVALID_TIMESTAMP, + message: 'Invalid timestamp in request signature', + }, + { + serverTime: Math.floor(+new Date() / 1000), + } + ); + } + + static invalidNonce() { + return new AppError({ + code: 401, + error: 'Unauthorized', + errno: ERRNO.INVALID_NONCE, + message: 'Invalid nonce in request signature', + }); + } + + static unauthorized(reason: string) { + return new AppError( + { + code: 401, + error: 'Unauthorized', + errno: ERRNO.INVALID_TOKEN, + message: 'Unauthorized for route', + }, + { + detail: reason, + } + ); + } + + static missingContentLength() { + return new AppError({ + code: 411, + error: 'Length Required', + errno: ERRNO.MISSING_CONTENT_LENGTH_HEADER, + message: 'Missing content-length header', + }); + } + + static requestBodyTooLarge() { + return new AppError({ + code: 413, + error: 'Request Entity Too Large', + errno: ERRNO.REQUEST_TOO_LARGE, + message: 'Request body too large', + }); + } + + static tooManyRequests( + retryAfter: number, + retryAfterLocalized?: string, + canUnblock?: boolean + ) { + if (!retryAfter) { + retryAfter = 30; + } + + const extraData: any = { + retryAfter: retryAfter, + }; + + if (retryAfterLocalized) { + extraData.retryAfterLocalized = retryAfterLocalized; + } + + if (canUnblock) { + extraData.verificationMethod = 'email-captcha'; + extraData.verificationReason = 'login'; + } + + const error = new AppError( + { + code: 429, + error: 'Too Many Requests', + errno: ERRNO.THROTTLED, + message: 'Client has sent too many requests', + }, + extraData, + { + 'retry-after': retryAfter.toString(), + } + ); + + return error; + } + + static requestBlocked(canUnblock: boolean) { + let extra; + if (canUnblock) { + extra = { + verificationMethod: 'email-captcha', + verificationReason: 'login', + }; + } + return new AppError( + { + code: 400, + error: 'Request blocked', + errno: ERRNO.REQUEST_BLOCKED, + message: 'The request was blocked for security reasons', + }, + extra + ); + } + + static serviceUnavailable(retryAfter: number) { + if (!retryAfter) { + retryAfter = 30; + } + return new AppError( + { + code: 503, + error: 'Service Unavailable', + errno: ERRNO.SERVER_BUSY, + message: 'Service unavailable', + }, + { + retryAfter: retryAfter, + }, + { + 'retry-after': retryAfter.toString(), + } + ); + } + + static featureNotEnabled(retryAfter: number) { + if (!retryAfter) { + retryAfter = 30; + } + return new AppError( + { + code: 503, + error: 'Feature not enabled', + errno: ERRNO.FEATURE_NOT_ENABLED, + message: 'Feature not enabled', + }, + { + retryAfter: retryAfter, + }, + { + 'retry-after': retryAfter.toString(), + } + ); + } + + static gone() { + return new AppError({ + code: 410, + error: 'Gone', + errno: ERRNO.ENDPOINT_NOT_SUPPORTED, + message: 'This endpoint is no longer supported', + }); + } + + static goneFourOhFour() { + return new AppError({ + code: 404, + error: 'Gone', + errno: ERRNO.ENDPOINT_NOT_SUPPORTED, + message: 'This endpoint is no longer supported', + }); + } + + static mustResetAccount(email: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.ACCOUNT_RESET, + message: 'Account must be reset', + }, + { + email: email, + } + ); + } + + static unknownDevice() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.DEVICE_UNKNOWN, + message: 'Unknown device', + }); + } + + static deviceSessionConflict(deviceId: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.DEVICE_CONFLICT, + message: 'Session already registered by another device', + }, + { deviceId } + ); + } + + static invalidUnblockCode() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_UNBLOCK_CODE, + message: 'Invalid unblock code', + }); + } + + static invalidPhoneNumber() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_PHONE_NUMBER, + message: 'Invalid phone number', + }); + } + + static recoveryCodesAlreadyExist() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_CODES_ALREADY_EXISTS, + message: 'Recovery codes or a verified TOTP token already exist', + }); + } + + static recoveryPhoneNumberAlreadyExists() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_PHONE_NUMBER_ALREADY_EXISTS, + message: 'Recovery phone number already exists', + }); + } + + static recoveryPhoneNumberDoesNotExist() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_PHONE_NUMBER_DOES_NOT_EXIST, + message: 'Recovery phone number does not exist', + }); + } + + static recoveryPhoneRemoveMissingRecoveryCodes() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_PHONE_REMOVE_MISSING_RECOVERY_CODES, + message: + 'Unable to remove recovery phone, missing backup authentication codes.', + }); + } + + static smsSendRateLimitExceeded() { + return new AppError({ + code: 429, + error: 'Too many requests', + errno: ERRNO.SMS_SEND_RATE_LIMIT_EXCEEDED, + message: 'Text message limit reached', + }); + } + + static recoveryPhoneRegistrationLimitReached() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_PHONE_REGISTRATION_LIMIT_REACHED, + message: + 'Limit reached for number off accounts that can be associated with phone number.', + }); + } + + static invalidRegion(region: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_REGION, + message: 'Invalid region', + }, + { + region, + } + ); + } + + static unsupportedLocation(country: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.UNSUPPORTED_LOCATION, + message: 'Location is not supported according to our Terms of Service.', + }, + { + country, + } + ); + } + + static currencyCountryMismatch(currency: string, country: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_REGION, + message: 'Funding source country does not match plan currency.', + }, + { + currency, + country, + } + ); + } + + static currencyCurrencyMismatch(currencyA: string, currencyB: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_CURRENCY, + message: `Changing currencies is not permitted.`, + }, + { + currencyA, + currencyB, + } + ); + } + + static billingAgreementExists(customerId: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.BILLING_AGREEMENT_EXISTS, + message: `Billing agreement already on file for this customer.`, + }, + { + customerId, + } + ); + } + + static missingPaypalPaymentToken(customerId: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.MISSING_PAYPAL_PAYMENT_TOKEN, + message: `PayPal payment token is missing.`, + }, + { + customerId, + } + ); + } + + static missingPaypalBillingAgreement(customerId: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.MISSING_PAYPAL_BILLING_AGREEMENT, + message: `PayPal billing agreement is missing for the existing subscriber.`, + }, + { + customerId, + } + ); + } + + static invalidMessageId() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_MESSAGE_ID, + message: 'Invalid message id', + }); + } + + static messageRejected(reason: string, reasonCode: number) { + return new AppError( + { + code: 500, + error: 'Internal Server Error', + errno: ERRNO.MESSAGE_REJECTED, + message: 'Message rejected', + }, + { + reason, + reasonCode, + } + ); + } + + static emailComplaint(bouncedAt: number) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.BOUNCE_COMPLAINT, + message: 'Email account sent complaint', + }, + { + bouncedAt, + } + ); + } + + static emailBouncedHard(bouncedAt: number) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.BOUNCE_HARD, + message: 'Email account hard bounced', + }, + { + bouncedAt, + } + ); + } + + static emailBouncedSoft(bouncedAt: number) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.BOUNCE_SOFT, + message: 'Email account soft bounced', + }, + { + bouncedAt, + } + ); + } + + static emailExists() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.EMAIL_EXISTS, + message: 'Email already exists', + }); + } + + static cannotDeletePrimaryEmail() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.EMAIL_DELETE_PRIMARY, + message: 'Can not delete primary email', + }); + } + + static unverifiedSession() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.SESSION_UNVERIFIED, + message: 'Unconfirmed session', + }); + } + + static yourPrimaryEmailExists() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.USER_PRIMARY_EMAIL_EXISTS, + message: 'Can not add secondary email that is same as your primary', + }); + } + + static verifiedPrimaryEmailAlreadyExists() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.VERIFIED_PRIMARY_EMAIL_EXISTS, + message: 'Email already exists', + }); + } + + static verifiedSecondaryEmailAlreadyExists() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.VERIFIED_SECONDARY_EMAIL_EXISTS, + message: 'Email already exists', + }); + } + + // This error is thrown when someone attempts to add a secondary email + // that is the same as the primary email of another account, but the account + // was recently created ( < 24hrs). + static unverifiedPrimaryEmailNewlyCreated() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.UNVERIFIED_PRIMARY_EMAIL_NEWLY_CREATED, + message: 'Email already exists', + }); + } + + static unverifiedPrimaryEmailHasActiveSubscription() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.UNVERIFIED_PRIMARY_EMAIL_HAS_ACTIVE_SUBSCRIPTION, + message: 'Account for this email has an active subscription', + }); + } + + static maxSecondaryEmailsReached() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.MAX_SECONDARY_EMAILS_REACHED, + message: 'You have reached the maximum allowed secondary emails', + }); + } + + static alreadyOwnsEmail() { + return new AppError({ + code: 400, + error: 'Conflict', + errno: ERRNO.ACCOUNT_OWNS_EMAIL, + message: 'This email already exists on your account', + }); + } + + static cannotLoginWithSecondaryEmail() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.LOGIN_WITH_SECONDARY_EMAIL, + message: 'Sign in with this email type is not currently supported', + }); + } + + static unknownSecondaryEmail() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.SECONDARY_EMAIL_UNKNOWN, + message: 'Unknown email', + }); + } + + static cannotResetPasswordWithSecondaryEmail() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RESET_PASSWORD_WITH_SECONDARY_EMAIL, + message: 'Reset password with this email type is not currently supported', + }); + } + + static invalidSigninCode() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_SIGNIN_CODE, + message: 'Invalid signin code', + }); + } + + static cannotChangeEmailToUnverifiedEmail() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.CHANGE_EMAIL_TO_UNVERIFIED_EMAIL, + message: 'Can not change primary email to an unconfirmed email', + }); + } + + static cannotChangeEmailToUnownedEmail() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.CHANGE_EMAIL_TO_UNOWNED_EMAIL, + message: + 'Can not change primary email to an email that does not belong to this account', + }); + } + + static cannotLoginWithEmail() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.LOGIN_WITH_INVALID_EMAIL, + message: 'This email can not currently be used to login', + }); + } + + static cannotResendEmailCodeToUnownedEmail() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RESEND_EMAIL_CODE_TO_UNOWNED_EMAIL, + message: + 'Can not resend email code to an email that does not belong to this account', + }); + } + + static cannotSendEmail(isNewAddress: boolean) { + if (!isNewAddress) { + return new AppError({ + code: 500, + error: 'Internal Server Error', + errno: ERRNO.FAILED_TO_SEND_EMAIL, + message: 'Failed to send email', + }); + } + return new AppError({ + code: 422, + error: 'Unprocessable Entity', + errno: ERRNO.FAILED_TO_SEND_EMAIL, + message: 'Failed to send email', + }); + } + + static invalidTokenVerficationCode(details?: Record) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_TOKEN_VERIFICATION_CODE, + message: 'Invalid token confirmation code', + }, + details + ); + } + + static expiredTokenVerficationCode(details?: Record) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.EXPIRED_TOKEN_VERIFICATION_CODE, + message: 'Expired token confirmation code', + }, + details + ); + } + + static totpTokenAlreadyExists() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.TOTP_TOKEN_EXISTS, + message: 'TOTP token already exists for this account.', + }); + } + + static totpTokenDoesNotExist() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.TOTP_SECRET_DOES_NOT_EXIST, + message: 'TOTP secret does not exist for this account.', + }); + } + + static totpTokenNotFound() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.TOTP_TOKEN_NOT_FOUND, + message: 'TOTP token not found.', + }); + } + + static recoveryCodeNotFound() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_CODE_NOT_FOUND, + message: 'Backup authentication code not found.', + }); + } + + static unavailableDeviceCommand() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.DEVICE_COMMAND_UNAVAILABLE, + message: 'Unavailable device command.', + }); + } + + static recoveryKeyNotFound() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_KEY_NOT_FOUND, + message: 'Account recovery key not found.', + }); + } + + static recoveryKeyInvalid() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_KEY_INVALID, + message: 'Account recovery key is not valid.', + }); + } + + static totpRequired() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.TOTP_REQUIRED, + message: + 'This request requires two step authentication enabled on your account.', + }); + } + + static recoveryKeyExists() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_KEY_EXISTS, + message: 'Account recovery key already exists.', + }); + } + + static unknownClientId(clientId?: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.UNKNOWN_CLIENT_ID, + message: 'Unknown client_id', + }, + { + clientId, + } + ); + } + + static incorrectClientSecret(clientId?: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INCORRECT_CLIENT_SECRET, + message: 'Incorrect client_secret', + }, + { + clientId, + } + ); + } + + static staleAuthAt(authAt: number) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.STALE_AUTH_AT, + message: 'Stale auth timestamp', + }, + { + authAt, + } + ); + } + + static notPublicClient() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.NOT_PUBLIC_CLIENT, + message: 'Not a public client', + }); + } + + static redisConflict() { + return new AppError({ + code: 409, + error: 'Conflict', + errno: ERRNO.REDIS_CONFLICT, + message: 'Redis WATCH detected a conflicting update', + }); + } + + static incorrectRedirectURI(redirectUri?: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INCORRECT_REDIRECT_URI, + message: 'Incorrect redirect URI', + }, + { + redirectUri, + } + ); + } + + static unknownAuthorizationCode(code?: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.UNKNOWN_AUTHORIZATION_CODE, + message: 'Unknown authorization code', + }, + { + code, + } + ); + } + + static mismatchAuthorizationCode(code?: string, clientId?: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.MISMATCH_AUTHORIZATION_CODE, + message: 'Mismatched authorization code', + }, + { + code, + clientId, + } + ); + } + + static expiredAuthorizationCode(code?: string, expiredAt?: number) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.EXPIRED_AUTHORIZATION_CODE, + message: 'Expired authorization code', + }, + { + code, + expiredAt, + } + ); + } + + static invalidResponseType() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_RESPONSE_TYPE, + message: 'Invalid response_type', + }); + } + + static invalidScopes(invalidScopes?: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_SCOPES, + message: 'Requested scopes are not allowed', + }, + { + invalidScopes, + } + ); + } + + static missingPkceParameters() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.MISSING_PKCE_PARAMETERS, + message: 'Public clients require PKCE OAuth parameters', + }); + } + + static invalidPkceChallenge(pkceHashValue?: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_PKCE_CHALLENGE, + message: 'Public clients require PKCE OAuth parameters', + }, + { + pkceHashValue, + } + ); + } + + static invalidPromoCode(promotionCode?: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_PROMOTION_CODE, + message: 'Invalid promotion code', + }, + { + promotionCode, + } + ); + } + + static unknownCustomer(uid?: string) { + return new AppError( + { + code: 404, + error: 'Not Found', + errno: ERRNO.UNKNOWN_SUBSCRIPTION_CUSTOMER, + message: 'Unknown customer', + }, + { + uid, + } + ); + } + + static unknownSubscription(subscriptionId: string) { + return new AppError( + { + code: 404, + error: 'Not Found', + errno: ERRNO.UNKNOWN_SUBSCRIPTION, + message: 'Unknown subscription', + }, + { + subscriptionId, + } + ); + } + + static unknownAppName(appName: string) { + return new AppError( + { + code: 404, + error: 'Not Found', + errno: ERRNO.IAP_UNKNOWN_APPNAME, + message: 'Unknown app name', + }, + { + appName, + } + ); + } + + static unknownSubscriptionPlan(planId: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.UNKNOWN_SUBSCRIPTION_PLAN, + message: 'Unknown subscription plan', + }, + { + planId, + } + ); + } + + static rejectedSubscriptionPaymentToken( + message: string, + paymentError: Error + ) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.REJECTED_SUBSCRIPTION_PAYMENT_TOKEN, + message, + }, + paymentError + ); + } + + static rejectedCustomerUpdate(message: string, paymentError: Error) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.REJECTED_CUSTOMER_UPDATE, + message, + }, + paymentError + ); + } + + static subscriptionAlreadyCancelled() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.SUBSCRIPTION_ALREADY_CANCELLED, + message: 'Subscription has already been cancelled', + }); + } + + static invalidPlanUpdate() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_PLAN_UPDATE, + message: 'Subscription plan is not a valid update', + }); + } + + static paymentFailed() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.PAYMENT_FAILED, + message: 'Payment method failed', + }); + } + + static subscriptionAlreadyChanged() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.SUBSCRIPTION_ALREADY_CHANGED, + message: 'Subscription has already been cancelled', + }); + } + + static subscriptionAlreadyExists() { + return new AppError({ + code: 409, + error: 'Already subscribed', + errno: ERRNO.SUBSCRIPTION_ALREADY_EXISTS, + message: 'User already subscribed.', + }); + } + + static userAlreadySubscribedToProduct() { + return new AppError({ + code: 409, + error: 'Already subscribed to product with different plan', + errno: ERRNO.SUBSCRIPTION_ALREADY_EXISTS, + message: 'User already subscribed to product with different plan.', + }); + } + + static iapInvalidToken(error?: Error) { + const extra = error ? [{}, undefined, error] : []; + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.IAP_INVALID_TOKEN, + message: `Invalid IAP token${error?.message ? `: ${error.message}` : ''}`, + }, + ...extra + ); + } + + static iapPurchaseConflict(error: Error) { + const extra = error ? [{}, undefined, error] : []; + return new AppError( + { + code: 403, + error: 'Forbidden', + errno: ERRNO.IAP_PURCHASE_ALREADY_REGISTERED, + message: 'Purchase has been registered to another user.', + }, + ...extra + ); + } + + static invalidInvoicePreviewRequest( + error: Error, + message: string, + priceId: string, + customer: string + ) { + const extra = error ? [{}, undefined, error] : []; + return new AppError( + { + code: 500, + error: 'Internal Server Error', + errno: ERRNO.INVALID_INVOICE_PREVIEW_REQUEST, + message, + }, + { + priceId, + customer, + }, + ...extra + ); + } + + static iapInternalError(error: Error) { + const extra = error ? [{}, undefined, error] : []; + return new AppError( + { + code: 500, + error: 'Internal Server Error', + errno: ERRNO.IAP_INTERNAL_OTHER, + message: 'IAP Internal Error', + }, + ...extra + ); + } + + static insufficientACRValues(foundValue: string) { + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.INSUFFICIENT_ACR_VALUES, + message: + 'Required Authentication Context Reference values could not be satisfied', + }, + { + foundValue, + } + ); + } + + static unknownRefreshToken() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.REFRESH_TOKEN_UNKNOWN, + message: 'Unknown refresh token', + }); + } + + static invalidOrExpiredOtpCode() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_EXPIRED_OTP_CODE, + message: 'Invalid or expired confirmation code', + }); + } + + static thirdPartyAccountError() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.THIRD_PARTY_ACCOUNT_ERROR, + message: + 'Could not login with third party account, please try again later', + }); + } + + static disabledClientId(clientId: string, retryAfter?: number) { + if (!retryAfter) { + retryAfter = 30; + } + return new AppError( + { + code: 503, + error: 'Client Disabled', + errno: ERRNO.DISABLED_CLIENT_ID, + message: 'This client has been temporarily disabled', + }, + { + clientId, + retryAfter, + }, + { + 'retry-after': retryAfter.toString(), + } + ); + } + + static internalValidationError(op: string, data: unknown, error: Error) { + return new AppError( + { + code: 500, + error: 'Internal Server Error', + errno: ERRNO.INTERNAL_VALIDATION_ERROR, + message: 'An internal validation check failed.', + }, + { + op, + data, + }, + {}, + error + ); + } + + static unexpectedError(request?: Request) { + const error = new AppError({}); + AppError.decorateErrorWithRequest(error, request); + return error; + } + + static missingSubscriptionForSourceError(op: string, data: any) { + return new AppError( + { + code: 500, + error: 'Missing subscription for source', + errno: ERRNO.UNKNOWN_SUBSCRIPTION_FOR_SOURCE, + message: 'Failed to find a subscription associated with Stripe source.', + }, + { + op, + data, + } + ); + } + + static accountCreationRejected() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.ACCOUNT_CREATION_REJECTED, + message: 'Account creation rejected.', + }); + } + + static subscriptionPromotionCodeNotApplied(error?: Error, message?: string) { + const extra = error ? [{}, undefined, error] : []; + return new AppError( + { + code: 400, + error: 'Bad Request', + errno: ERRNO.SUBSCRIPTION_PROMO_CODE_NOT_APPLIED, + message, + }, + ...extra + ); + } + + static invalidCloudTaskEmailType() { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.INVALID_CLOUDTASK_EMAILTYPE, + message: 'Invalid email type', + }); + } + + private static decorateErrorWithRequest(error: AppError, request?: Request) { + if (request) { + error.output.payload.request = { + // request.app.devices and request.app.metricsContext are async, so can't be included here + acceptLanguage: request.app.acceptLanguage, + locale: request.app.locale, + userAgent: request.app.ua, + method: request.method, + path: request.path, + query: request.query, + payload: scrubPii(request.payload), + headers: scrubHeaders(request.headers), + }; + } + } + + private static appErrorFromOauthError(err: any) { + switch (err.errno) { + case 101: + return AppError.unknownClientId(err.clientId); + case 102: + return AppError.incorrectClientSecret(err.clientId); + case 103: + return AppError.incorrectRedirectURI(err.redirectUri); + case 104: + return AppError.invalidToken(); + case 105: + return AppError.unknownAuthorizationCode(err.code); + case 106: + return AppError.mismatchAuthorizationCode(err.code, err.clientId); + case 107: + return AppError.expiredAuthorizationCode(err.code, err.expiredAt); + case 108: + return AppError.invalidToken(); + case 109: + return AppError.invalidRequestParameter(err.validation); + case 110: + return AppError.invalidResponseType(); + case 114: + return AppError.invalidScopes(err.invalidScopes); + case 116: + return AppError.notPublicClient(); + case 117: + return AppError.invalidPkceChallenge(err.pkceHashValue); + case 118: + return AppError.missingPkceParameters(); + case 119: + return AppError.staleAuthAt(err.authAt); + case 120: + return AppError.insufficientACRValues(err.foundValue); + case 121: + return AppError.invalidRequestParameter('grant_type'); + case 122: + return AppError.unknownRefreshToken(); + case 201: + return AppError.serviceUnavailable(err.retryAfter); + case 202: + return AppError.disabledClientId(err.clientId); + default: + return err; + } + } +} + +/** + * Tries to remove PII from payload data. + * @param payload + * @returns + */ +function scrubPii(payload: any) { + if (!payload) { + return; + } + + return Object.entries(payload).reduce((scrubbed: any, [key, value]) => { + if (DEBUGGABLE_PAYLOAD_KEYS.has(key)) { + scrubbed[key] = value; + } + + return scrubbed; + }, {}); +} + +/** + * Deletes feilds with senstive data from headers. + * @param headers + * @returns + */ +function scrubHeaders( + headers?: Record +): Record { + if (headers == null) { + return {}; + } + + const scrubbed = { ...headers }; + delete scrubbed['x-forwarded-for']; + return scrubbed; +} + +/** + * Prevents errors from being captured by Sentry. + * + * @param {Error} error An error with an error number. Note that errors of type vError will + * use the underlying jse_cause error if possible. + */ +export function ignoreErrors(error: AppError) { + if (!error) { + return; + } + + // Prefer jse_cause, but fallback to top level error if needed + const statusCode = + determineStatusCode(error.jse_cause) || determineStatusCode(error); + + const errno = (() => { + if (error && error.jse_cause && 'errno' in error.jse_cause) { + return error.jse_cause.errno; + } + return error.errno; + })(); + + // Ignore non 500 status codes and specific error numbers + return ( + (statusCode && statusCode < 500) || IGNORED_ERROR_NUMBERS.includes(errno) + ); +} + +/** + * Given an error tries to determine the HTTP status code associated with the error. + * @param {*} error + * @returns + */ + +/** + * Uses some fallback logic to determine an error's underlying HTTP status code. + * @param error + * @returns + */ +function determineStatusCode(error?: any) { + if (!error) { + return; + } + + return error.statusCode || error.output?.statusCode || error.code; +} diff --git a/libs/accounts/errors/src/constants.ts b/libs/accounts/errors/src/constants.ts new file mode 100644 index 00000000000..36f41094b57 --- /dev/null +++ b/libs/accounts/errors/src/constants.ts @@ -0,0 +1,220 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Standard error numbers + */ +export const ERRNO = { + SERVER_CONFIG_ERROR: 100, + ACCOUNT_EXISTS: 101, + ACCOUNT_UNKNOWN: 102, + INCORRECT_PASSWORD: 103, + ACCOUNT_UNVERIFIED: 104, + INVALID_VERIFICATION_CODE: 105, + INVALID_JSON: 106, + INVALID_PARAMETER: 107, + MISSING_PARAMETER: 108, + INVALID_REQUEST_SIGNATURE: 109, + INVALID_TOKEN: 110, + INVALID_TIMESTAMP: 111, + MISSING_CONTENT_LENGTH_HEADER: 112, + REQUEST_TOO_LARGE: 113, + THROTTLED: 114, + INVALID_NONCE: 115, + ENDPOINT_NOT_SUPPORTED: 116, + INCORRECT_EMAIL_CASE: 120, + // ACCOUNT_LOCKED: 121, + // ACCOUNT_NOT_LOCKED: 122, + DEVICE_UNKNOWN: 123, + DEVICE_CONFLICT: 124, + REQUEST_BLOCKED: 125, + ACCOUNT_RESET: 126, + INVALID_UNBLOCK_CODE: 127, + // MISSING_TOKEN: 128, + INVALID_PHONE_NUMBER: 129, + INVALID_REGION: 130, + INVALID_MESSAGE_ID: 131, + MESSAGE_REJECTED: 132, + BOUNCE_COMPLAINT: 133, + BOUNCE_HARD: 134, + BOUNCE_SOFT: 135, + EMAIL_EXISTS: 136, + EMAIL_DELETE_PRIMARY: 137, + SESSION_UNVERIFIED: 138, + USER_PRIMARY_EMAIL_EXISTS: 139, + VERIFIED_PRIMARY_EMAIL_EXISTS: 140, + MAX_SECONDARY_EMAILS_REACHED: 188, + ACCOUNT_OWNS_EMAIL: 189, + // If there exists an account that was created under 24hrs and + // has not verified their email address, this error is thrown + // if another user attempts to add that email to their account + // as a secondary email. + UNVERIFIED_PRIMARY_EMAIL_NEWLY_CREATED: 141, + LOGIN_WITH_SECONDARY_EMAIL: 142, + SECONDARY_EMAIL_UNKNOWN: 143, + VERIFIED_SECONDARY_EMAIL_EXISTS: 144, + RESET_PASSWORD_WITH_SECONDARY_EMAIL: 145, + INVALID_SIGNIN_CODE: 146, + CHANGE_EMAIL_TO_UNVERIFIED_EMAIL: 147, + CHANGE_EMAIL_TO_UNOWNED_EMAIL: 148, + LOGIN_WITH_INVALID_EMAIL: 149, + RESEND_EMAIL_CODE_TO_UNOWNED_EMAIL: 150, + FAILED_TO_SEND_EMAIL: 151, + INVALID_TOKEN_VERIFICATION_CODE: 152, + EXPIRED_TOKEN_VERIFICATION_CODE: 153, + TOTP_TOKEN_EXISTS: 154, + TOTP_TOKEN_NOT_FOUND: 155, + RECOVERY_CODE_NOT_FOUND: 156, + DEVICE_COMMAND_UNAVAILABLE: 157, + RECOVERY_KEY_NOT_FOUND: 158, + RECOVERY_KEY_INVALID: 159, + TOTP_REQUIRED: 160, + RECOVERY_KEY_EXISTS: 161, + UNKNOWN_CLIENT_ID: 162, + INVALID_SCOPES: 163, + STALE_AUTH_AT: 164, + REDIS_CONFLICT: 165, + NOT_PUBLIC_CLIENT: 166, + INCORRECT_REDIRECT_URI: 167, + INVALID_RESPONSE_TYPE: 168, + MISSING_PKCE_PARAMETERS: 169, + INSUFFICIENT_ACR_VALUES: 170, + INCORRECT_CLIENT_SECRET: 171, + UNKNOWN_AUTHORIZATION_CODE: 172, + MISMATCH_AUTHORIZATION_CODE: 173, + EXPIRED_AUTHORIZATION_CODE: 174, + INVALID_PKCE_CHALLENGE: 175, + UNKNOWN_SUBSCRIPTION_CUSTOMER: 176, + UNKNOWN_SUBSCRIPTION: 177, + UNKNOWN_SUBSCRIPTION_PLAN: 178, + REJECTED_SUBSCRIPTION_PAYMENT_TOKEN: 179, + SUBSCRIPTION_ALREADY_CANCELLED: 180, + REJECTED_CUSTOMER_UPDATE: 181, + REFRESH_TOKEN_UNKNOWN: 182, + INVALID_EXPIRED_OTP_CODE: 183, + SUBSCRIPTION_ALREADY_CHANGED: 184, + INVALID_PLAN_UPDATE: 185, + PAYMENT_FAILED: 186, + SUBSCRIPTION_ALREADY_EXISTS: 187, + UNKNOWN_SUBSCRIPTION_FOR_SOURCE: 188, + BILLING_AGREEMENT_EXISTS: 192, + MISSING_PAYPAL_PAYMENT_TOKEN: 193, + MISSING_PAYPAL_BILLING_AGREEMENT: 194, + UNVERIFIED_PRIMARY_EMAIL_HAS_ACTIVE_SUBSCRIPTION: 195, + IAP_INVALID_TOKEN: 196, + IAP_INTERNAL_OTHER: 197, + IAP_UNKNOWN_APPNAME: 198, + INVALID_PROMOTION_CODE: 199, + SERVER_BUSY: 201, + FEATURE_NOT_ENABLED: 202, + BACKEND_SERVICE_FAILURE: 203, + DISABLED_CLIENT_ID: 204, + THIRD_PARTY_ACCOUNT_ERROR: 205, + CANNOT_CREATE_PASSWORD: 206, + ACCOUNT_CREATION_REJECTED: 207, + IAP_PURCHASE_ALREADY_REGISTERED: 208, + INVALID_INVOICE_PREVIEW_REQUEST: 209, + UNABLE_TO_LOGIN_NO_PASSWORD_SET: 210, + INVALID_CURRENCY: 211, + SUBSCRIPTION_PROMO_CODE_NOT_APPLIED: 212, + UNSUPPORTED_LOCATION: 213, + RECOVERY_PHONE_NUMBER_ALREADY_EXISTS: 214, + RECOVERY_PHONE_NUMBER_DOES_NOT_EXIST: 215, + SMS_SEND_RATE_LIMIT_EXCEEDED: 216, + INVALID_CLOUDTASK_EMAILTYPE: 217, + RECOVERY_PHONE_REMOVE_MISSING_RECOVERY_CODES: 218, + RECOVERY_PHONE_REGISTRATION_LIMIT_REACHED: 219, + TOTP_SECRET_DOES_NOT_EXIST: 220, + RECOVERY_CODES_ALREADY_EXISTS: 221, + INTERNAL_VALIDATION_ERROR: 998, + UNEXPECTED_ERROR: 999, +}; + +/** + * Takes an object and swaps keys with values. Useful when a value -> key look up is needed. + * @param obj - Object to swap keys and values on + * @returns An object with keys and values reveresed. + */ +function swapObjectKeysAndValues(obj: { + [key: string]: string | number; +}) { + const result: { [key: string | number]: string } = {}; + for (const key in obj) { + result[obj[key]] = key; + } + return result; +} +/** + * A reversed map of errnos. + */ +export const ERRNO_REVERSE_MAP = swapObjectKeysAndValues(ERRNO); + +/** + * The Default Unexpected Error State + */ +export const DEFAULT_ERRROR = { + code: 500, + error: 'Internal Server Error', + errno: ERRNO.UNEXPECTED_ERROR, + message: 'Unspecified error', + info: 'https://mozilla.github.io/ecosystem-platform/api#section/Response-format', +}; + +/** + * List of errors that should not be sent to Sentry + */ +export const IGNORED_ERROR_NUMBERS = [ + ERRNO.BOUNCE_HARD, + ERRNO.BOUNCE_SOFT, + ERRNO.BOUNCE_COMPLAINT, +]; + +/** + * Regex to determine if message is a payload too large message. + */ +export const TOO_LARGE = /^Payload (?:content length|size) greater than maximum allowed/; + +/** + * Set of errors indicating a bad request signature + */ +export const BAD_SIGNATURE_ERRORS = [ + 'Bad mac', + 'Unknown algorithm', + 'Missing required payload hash', + 'Payload is invalid', +]; + +/** + * Payload properties that might help us debug unexpected errors + * when they show up in production. Obviously we don't want to + * accidentally send any sensitive data or PII to a 3rd-party, + * so the set is opt-in rather than opt-out. + */ +export const DEBUGGABLE_PAYLOAD_KEYS = new Set([ + 'availableCommands', + 'capabilities', + 'client_id', + 'code', + 'command', + 'duration', + 'excluded', + 'features', + 'messageId', + 'metricsContext', + 'name', + 'preVerified', + 'publicKey', + 'reason', + 'redirectTo', + 'reminder', + 'scope', + 'service', + 'target', + 'to', + 'TTL', + 'ttl', + 'type', + 'unblockCode', + 'verificationMethod', +]); diff --git a/libs/accounts/errors/src/index.spec.ts b/libs/accounts/errors/src/index.spec.ts new file mode 100644 index 00000000000..caabd62a0c8 --- /dev/null +++ b/libs/accounts/errors/src/index.spec.ts @@ -0,0 +1,337 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { fullStack } from 'verror'; +import { AppError, Request } from './app-error'; +import { OauthError } from './oauth-error'; +import { RequestRoute } from 'hapi'; + +describe('AppErrors', () => { + const mockApp = { + acceptLanguage: 'en, fr', + locale: 'en', + geo: { + city: 'Mountain View', + state: 'California', + }, + ua: { + os: 'Android', + osVersion: '9', + }, + devices: Promise.resolve([{ id: 1 }]), + metricsContext: Promise.resolve({ + service: 'sync', + }), + }; + + const mockRequest = { + app: mockApp, + route: { path: '/foo' }, + }; + + // In a server environment, this should be configured or dynamically + // determined from paths registered by the server! + const mockOAuthRoutes = [ + { + path: '/token', + config: { + cors: true, + }, + }, + ]; + + it('converts an OauthError into AppError when not an oauth route', function () { + const oauthError = OauthError.invalidAssertion(); + expect(oauthError.errno).toEqual(104); + + const result = AppError.translate( + { + app: mockApp, + route: { path: '/v1/oauth/token' }, + }, + oauthError, + mockOAuthRoutes + ); + expect(result).toBeInstanceOf(AppError); + expect(result.errno).toEqual(110); + }); + + it('keeps an OauthError with an oauth route', () => { + const oauthError = OauthError.invalidAssertion(); + expect(oauthError.errno).toEqual(104); + + const result = AppError.translate( + { app: mockApp, route: { path: '/v1/token' } }, + oauthError, + mockOAuthRoutes + ); + + expect(result).toBeInstanceOf(OauthError); + expect(result.errno).toEqual(104); + }); + + it('should translate with missing required parameters', () => { + const result = AppError.translate( + mockRequest, + { + output: { + payload: { + message: `foo${'is required'}`, + validation: { + keys: ['bar', 'baz'], + }, + }, + }, + }, + mockOAuthRoutes + ); + expect(result).toBeInstanceOf(AppError); + expect(result.errno).toEqual(108); + expect(result.message).toEqual('Missing parameter in request body: bar'); + expect(result.output.statusCode).toEqual(400); + expect(result.output.payload.error).toEqual('Bad Request'); + expect(result.output.payload.errno).toEqual(result.errno); + expect(result.output.payload.message).toEqual(result.message); + expect(result.output.payload.param).toEqual('bar'); + }); + + it('should translate with payload data', () => { + const data = { + TIMESTAMP: '2021-01-16T01:43:33Z', + CORRELATIONID: '950b61c42c10d', + ACK: 'Failure', + VERSION: '204', + BUILD: '55251101', + L_ERRORCODE0: '11451', + L_SHORTMESSAGE0: 'Billing Agreement Id or transaction Id is not valid', + L_LONGMESSAGE0: 'Billing Agreement Id or transaction Id is not valid', + L_SEVERITYCODE0: 'Error', + }; + + const result = AppError.translate( + mockRequest, + { + output: { + statusCode: 500, + payload: { + error: 'Internal Server Error', + }, + }, + data: data, + }, + mockOAuthRoutes + ); + + expect(JSON.stringify(data)).toEqual(result.output.payload.data); + }); + + it('should translate with invalid parameter', () => { + const result = AppError.translate( + mockRequest, + { + output: { + payload: { + validation: 'foo', + }, + }, + }, + mockOAuthRoutes + ); + expect(result).toBeInstanceOf(AppError); + expect(result.errno).toEqual(107); + expect(result.message).toEqual('Invalid parameter in request body'); + expect(result.output.statusCode).toEqual(400); + expect(result.output.payload.error).toEqual('Bad Request'); + expect(result.output.payload.errno).toEqual(result.errno); + expect(result.output.payload.message).toEqual(result.message); + expect(result.output.payload.validation).toEqual('foo'); + }); + + it('should translate with missing payload', () => { + const result = AppError.translate( + mockRequest, + { + output: {}, + }, + mockOAuthRoutes + ); + expect(result).toBeInstanceOf(AppError); + expect(result.errno).toEqual(999); + expect(result.message).toEqual('Unspecified error'); + expect(result.output.statusCode).toEqual(500); + expect(result.output.payload.error).toEqual('Internal Server Error'); + expect(result.output.payload.errno).toEqual(result.errno); + expect(result.output.payload.message).toEqual(result.message); + }); + + it('maps an errno to its key', () => { + const error = AppError.cannotLoginNoPasswordSet(); + const actual = AppError.mapErrnoToKey(error); + expect(actual).toEqual('UNABLE_TO_LOGIN_NO_PASSWORD_SET'); + }); + + it('backend error includes a cause error when supplied', () => { + const originalError = new Error('Service timed out.'); + const err = AppError.backendServiceFailure( + 'test', + 'checking', + {}, + originalError + ); + const fullError = fullStack(err); + expect(fullError).toContain('caused by:'); + expect(fullError).toContain('Error: Service timed out.'); + }); + + it('tooManyRequests', () => { + let result = AppError.tooManyRequests(900, 'in 15 minutes'); + expect(result).toBeInstanceOf(AppError); + expect(result.errno).toEqual(114); + expect(result.message).toEqual('Client has sent too many requests'); + expect(result.output.statusCode).toEqual(429); + expect(result.output.payload.error).toEqual('Too Many Requests'); + expect(result.output.payload.retryAfter).toEqual(900); + expect(result.output.payload.retryAfterLocalized).toEqual('in 15 minutes'); + + result = AppError.tooManyRequests(900); + expect(result.output.payload.retryAfter).toEqual(900); + expect(!result.output.payload.retryAfterLocalized).toBeTruthy(); + }); + + it('iapInvalidToken', () => { + const defaultErrorMessage = 'Invalid IAP token'; + let result = AppError.iapInvalidToken(); + expect(result).toBeInstanceOf(AppError); + expect(result.errno).toEqual(196); + expect(result.message).toEqual(defaultErrorMessage); + expect(result.output.statusCode).toEqual(400); + expect(result.output.payload.error).toEqual('Bad Request'); + + result = AppError.iapInvalidToken({ + name: 'Invalid IAP Token', + message: '', + }); + expect(result.message).toEqual(defaultErrorMessage); + + result = AppError.iapInvalidToken({ + name: 'Invalid IAP Token', + message: 'Wow helpful extra info', + }); + expect(result.message).toEqual( + `${defaultErrorMessage}: Wow helpful extra info` + ); + }); + + it('unexpectedError without request data', () => { + const err = AppError.unexpectedError(); + expect(err).toBeInstanceOf(AppError); + expect(err).toBeInstanceOf(Error); + expect(err.errno).toEqual(999); + expect(err.message).toEqual('Unspecified error'); + expect(err.output.statusCode).toEqual(500); + expect(err.output.payload.error).toEqual('Internal Server Error'); + expect(err.output.payload.request).toBeUndefined(); + }); + + it('unexpectedError with request data', () => { + const err = AppError.unexpectedError({ + app: { + acceptLanguage: 'en, fr', + locale: 'en', + geo: { + city: 'Mountain View', + state: 'California', + }, + ua: { + os: 'Android', + osVersion: '9', + }, + devices: Promise.resolve([{ id: 1 }]), + metricsContext: Promise.resolve({ + service: 'sync', + }), + }, + method: 'GET', + path: '/v1/wibble', + route: { + path: '/v1/wibble', + } as unknown as RequestRoute, + query: { + foo: 'bar', + }, + payload: { + baz: 'qux', + email: 'foo@example.com', + displayName: 'Foo Bar', + metricsContext: { + utmSource: 'thingy', + }, + service: 'sync', + }, + headers: { + // x-forwarded-for is stripped out because it contains internal server IPs + // See https://github.com/mozilla/fxa-private/issues/66 + 'x-forwarded-for': '192.168.1.1 192.168.2.2', + wibble: 'blee', + }, + } as unknown as Request); + expect(err.errno).toEqual(999); + expect(err.message).toEqual('Unspecified error'); + expect(err.output.statusCode).toEqual(500); + expect(err.output.payload.error).toEqual('Internal Server Error'); + expect(err.output.payload.request).toEqual({ + acceptLanguage: 'en, fr', + locale: 'en', + userAgent: { + os: 'Android', + osVersion: '9', + }, + method: 'GET', + path: '/v1/wibble', + query: { + foo: 'bar', + }, + payload: { + metricsContext: { + utmSource: 'thingy', + }, + service: 'sync', + }, + headers: { + wibble: 'blee', + }, + }); + }); + + const reasons = ['socket hang up', 'ECONNREFUSED']; + reasons.forEach((reason) => { + it(`converts ${reason} errors to backend service error`, () => { + const result = AppError.translate( + mockRequest, + { + output: { + payload: { + errno: 999, + statusCode: 500, + }, + }, + reason, + }, + mockOAuthRoutes + ); + + expect(result).toBeInstanceOf(AppError); + expect(result.errno).toEqual(203); + expect(result.message).toEqual('System unavailable, try again soon'); + expect(result.output.statusCode).toEqual(500); + expect(result.output.payload.error).toEqual('Internal Server Error'); + expect(result.output.payload.errno).toEqual( + AppError.ERRNO.BACKEND_SERVICE_FAILURE + ); + expect(result.output.payload.message).toEqual( + 'System unavailable, try again soon' + ); + }); + }); +}); diff --git a/libs/accounts/errors/src/index.ts b/libs/accounts/errors/src/index.ts new file mode 100644 index 00000000000..aaa04cf8d9f --- /dev/null +++ b/libs/accounts/errors/src/index.ts @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export * from './app-error'; +export * from './oauth-error'; diff --git a/libs/accounts/errors/src/oauth-error.ts b/libs/accounts/errors/src/oauth-error.ts new file mode 100644 index 00000000000..9c07eac4cad --- /dev/null +++ b/libs/accounts/errors/src/oauth-error.ts @@ -0,0 +1,432 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { DEFAULT_ERRROR } from './constants'; + +// Leaving this here for historical reasons. But we should probably get rid +// of references to this legacy buff package... See FXA-12594 +const hex = require('buf').to.hex; + +// Deprecated. New error types should be defined in lib/error.js +export class OauthError extends Error { + isBoom: boolean; + errno: number; + output: { + statusCode: number; + payload: { + code: number; + errno: number; + error: any; + message: string; + info: Record; + log?:unknown; + }; + headers: Record; + }; + + constructor(options: any, extra?: any, headers?: any) { + super(options.message || DEFAULT_ERRROR.message); + this.message = options.message || DEFAULT_ERRROR.message; + this.isBoom = true; + if (options.stack) { + this.stack = options.stack; + } else { + Error.captureStackTrace(this, OauthError); + } + this.errno = options.errno || DEFAULT_ERRROR.errno; + this.output = { + statusCode: options.code || DEFAULT_ERRROR.code, + payload: { + code: options.code || DEFAULT_ERRROR.code, + errno: this.errno, + error: options.error || DEFAULT_ERRROR.error, + message: this.message, + info: options.info || DEFAULT_ERRROR.info, + }, + headers: headers || {}, + }; + merge(this.output.payload, extra); + } + + override toString() { + return 'Error: ' + this.message; + } + + header(name:string, value:string|number) { + this.output.headers[name] = value; + } + + translate(response:{output:{payload:any}, stack:string}) { + if (response instanceof OauthError) { + return response; + } + + let error; + const payload = response.output.payload; + if (payload.validation) { + error = OauthError.invalidRequestParameter(payload.validation); + } else if (payload.statusCode === 415) { + error = OauthError.invalidContentType(); + } else { + error = new OauthError({ + message: payload.message, + code: payload.statusCode, + error: payload.error, + errno: payload.errno, + stack: response.stack, + }); + } + + return error; + } + + static isOauthRoute(path:string, routes?:Array<{path:string, config:{cors:any}}>) { + if (!routes) { + return false; + } + + return ( + // For now we use a fragile heuristic that all oauth routes set cors config + // TODO: when we merge oauth errors into auth, rethink this. + routes.findIndex((r) => `/v1${r.path}` === path && r.config.cors) > -1 + ); + } + + static translate( + response: { + output: { + payload?:any; + }, + stack?:string + } | OauthError + ):OauthError { + + if (response instanceof OauthError) { + return response; + } + + const payload = response.output.payload; + + if (payload?.validation) { + return OauthError.invalidRequestParameter(payload.validation); + } + + if (payload?.statusCode === 415) { + return OauthError.invalidContentType(); + } + + return new OauthError({ + message: payload?.message, + code: payload?.statusCode, + error: payload?.error, + errno: payload?.errno, + stack: response.stack, + }); + } + + backtrace(traced:unknown) { + this.output.payload.log = traced; + }; + + static unexpectedError() { + return new OauthError({}); + } + + static unknownClient(clientId:string|Buffer) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 101, + message: 'Unknown client', + }, + { + clientId: hex(clientId), + } + ); + } + + static incorrectSecret(clientId:string) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 102, + message: 'Incorrect secret', + }, + { + clientId: hex(clientId), + } + ); + } + + static incorrectRedirect(uri:string) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 103, + message: 'Incorrect redirect_uri', + }, + { + redirectUri: uri, + } + ); + } + + static invalidAssertion() { + return new OauthError({ + code: 401, + error: 'Bad Request', + errno: 104, + message: 'Invalid assertion', + }); + } + + static unknownCode(code:string) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 105, + message: 'Unknown code', + }, + { + requestCode: code, + } + ); + } + + static mismatchCode(code:string, clientId:string) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 106, + message: 'Incorrect code', + }, + { + requestCode: hex(code), + client: hex(clientId), + } + ); + } + + static expiredCode(code:string, expiredAt:string) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 107, + message: 'Expired code', + }, + { + requestCode: hex(code), + expiredAt: expiredAt, + } + ); + } + + static invalidToken() { + return new OauthError({ + code: 400, + error: 'Bad Request', + errno: 108, + message: 'Invalid token', + }); + } + + static invalidRequestParameter(val:string) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 109, + message: 'Invalid request parameter', + }, + { + validation: val, + } + ); + } + + static invalidResponseType() { + return new OauthError({ + code: 400, + error: 'Bad Request', + errno: 110, + message: 'Invalid response_type', + }); + } + + static unauthorized(reason:string) { + return new OauthError( + { + code: 401, + error: 'Unauthorized', + errno: 111, + message: 'Unauthorized for route', + }, + { + detail: reason, + } + ); + } + + static forbidden() { + return new OauthError({ + code: 403, + error: 'Forbidden', + errno: 112, + message: 'Forbidden', + }); + } + + static invalidContentType() { + return new OauthError({ + code: 415, + error: 'Unsupported Media Type', + errno: 113, + message: + 'Content-Type must be either application/json or ' + + 'application/x-www-form-urlencoded', + }); + } + + static invalidScopes(scopes:string) { + return new OauthError( + { + code: 400, + error: 'Invalid scopes', + errno: 114, + message: 'Requested scopes are not allowed', + }, + { + invalidScopes: scopes, + } + ); + } + + static expiredToken(expiredAt:number) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 115, + message: 'Expired token', + }, + { + expiredAt: expiredAt, + } + ); + } + + static notPublicClient(clientId:string) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 116, + message: 'Not a public client', + }, + { + clientId: hex(clientId), + } + ); + } + + static mismatchCodeChallenge(pkceHashValue:string) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 117, + message: 'Incorrect code_challenge', + }, + { + requestCodeChallenge: pkceHashValue, + } + ); + } + + static missingPkceParameters() { + return new OauthError({ + code: 400, + error: 'PKCE parameters missing', + errno: 118, + message: 'Public clients require PKCE OAuth parameters', + }); + } + + static staleAuthAt(authAt:number) { + return new OauthError( + { + code: 401, + error: 'Bad Request', + errno: 119, + message: 'Stale authentication timestamp', + }, + { + authAt: authAt, + } + ); + } + + static mismatchAcr(foundValue:boolean) { + return new OauthError( + { + code: 400, + error: 'Bad Request', + errno: 120, + message: 'Mismatch acr value', + }, + { foundValue } + ); + } + + static invalidGrantType() { + return new OauthError({ + code: 400, + error: 'Bad Request', + errno: 121, + message: 'Invalid grant_type', + }); + } + + static unknownToken() { + return new OauthError({ + code: 400, + error: 'Bad Request', + errno: 122, + message: 'Unknown token', + }); + } + + // N.B. `errno: 201` is traditionally our generic "service unavailable" error, + // so let's reserve it for that purpose here as well. + + static disabledClient(clientId:string) { + return new OauthError( + { + code: 503, + error: 'Client Disabled', + errno: 202, // TODO reconcile this with the auth-server version + message: 'This client has been temporarily disabled', + }, + { clientId } + ); + } +} + +/** + * Utility function to merge one object into an other. + * @param target The main object + * @param other The object being merged into the main object. + */ +function merge(target: any, other: any) { + const keys = Object.keys(other || {}); + for (let i = 0; i < keys.length; i++) { + target[keys[i]] = other[keys[i]]; + } +} diff --git a/libs/accounts/errors/src/util.ts b/libs/accounts/errors/src/util.ts new file mode 100644 index 00000000000..6bca9209437 --- /dev/null +++ b/libs/accounts/errors/src/util.ts @@ -0,0 +1,18 @@ +export function swapObjectKeysAndValues(obj: { + [key: string]: string | number; +}) { + const result: { [key: string | number]: string } = {}; + for (const key in obj) { + result[obj[key]] = key; + } + return result; +} + +// Parse a comma-separated list with allowance for varied whitespace +export function commaSeparatedListToArray(s: string) { + return (s || '') + .trim() + .split(',') + .map((c) => c.trim()) + .filter((c) => !!c); +} diff --git a/libs/accounts/errors/tsconfig.json b/libs/accounts/errors/tsconfig.json new file mode 100644 index 00000000000..86622aca55a --- /dev/null +++ b/libs/accounts/errors/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/accounts/errors/tsconfig.lib.json b/libs/accounts/errors/tsconfig.lib.json new file mode 100644 index 00000000000..4befa7f0990 --- /dev/null +++ b/libs/accounts/errors/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/accounts/errors/tsconfig.spec.json b/libs/accounts/errors/tsconfig.spec.json new file mode 100644 index 00000000000..69a251f328c --- /dev/null +++ b/libs/accounts/errors/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/package.json b/package.json index 4416455290e..604d8a7a415 100644 --- a/package.json +++ b/package.json @@ -208,6 +208,7 @@ "@types/babel__preset-env": "^7", "@types/bn.js": "^5", "@types/classnames": "2.3.1", + "@types/hapi": "^18.0.15", "@types/jest": "29.5.14", "@types/jsdom": "^21", "@types/module-alias": "^2", diff --git a/tsconfig.base.json b/tsconfig.base.json index e60f3290439..afaef6f83ee 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,6 +24,7 @@ "useUnknownInCatchVariables": false, "baseUrl": ".", "paths": { + "@fxa/accounts/errors": ["libs/accounts/errors/src/index.ts"], "@fxa/accounts/rate-limit": ["libs/accounts/rate-limit/src/index.ts"], "@fxa/accounts/recovery-phone": [ "libs/accounts/recovery-phone/src/index.ts" diff --git a/yarn.lock b/yarn.lock index 8e0bffe698a..27a5b5d30ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20881,6 +20881,13 @@ __metadata: languageName: node linkType: hard +"@types/boom@npm:*": + version: 7.3.5 + resolution: "@types/boom@npm:7.3.5" + checksum: 10c0/6b8577f26b22950b4d0fd1770b30f0d959c6d15275a72a381b91aea3772313675c6293fac6cd484cb40eec7715f1d321cacaacd7b510a68c3d21932af5c5f837 + languageName: node + linkType: hard + "@types/bunyan@npm:1.8.11": version: 1.8.11 resolution: "@types/bunyan@npm:1.8.11" @@ -20918,6 +20925,13 @@ __metadata: languageName: node linkType: hard +"@types/catbox@npm:*": + version: 10.0.9 + resolution: "@types/catbox@npm:10.0.9" + checksum: 10c0/ec86aa00b64cf8912152cc937fc06604fecf602f37529aa1ae5c2991be9c957376bcb3df816c244bdbcd66f8822e2b36ac1b46a7ebc03e7d3d4c8e721271021e + languageName: node + linkType: hard + "@types/chai-as-promised@npm:^7": version: 7.1.8 resolution: "@types/chai-as-promised@npm:7.1.8" @@ -21327,6 +21341,22 @@ __metadata: languageName: node linkType: hard +"@types/hapi@npm:^18.0.15": + version: 18.0.15 + resolution: "@types/hapi@npm:18.0.15" + dependencies: + "@types/boom": "npm:*" + "@types/catbox": "npm:*" + "@types/iron": "npm:*" + "@types/mimos": "npm:*" + "@types/node": "npm:*" + "@types/podium": "npm:*" + "@types/shot": "npm:*" + joi: "npm:^17.3.0" + checksum: 10c0/c17e6c1fbe1601c49f6d1616ffbe1ccc1275716c6a0ab98be780a51c11e6c2c3f8ab8edb0af184176434fdf49d9e2a1e2c86b50c3322fe7d16fd8e47762157c6 + languageName: node + linkType: hard + "@types/hapi__catbox@npm:*": version: 10.2.6 resolution: "@types/hapi__catbox@npm:10.2.6" @@ -21467,6 +21497,15 @@ __metadata: languageName: node linkType: hard +"@types/iron@npm:*": + version: 5.0.5 + resolution: "@types/iron@npm:5.0.5" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/02300572301894077e19a63f069a8b8c41e185e4fe0115c33ffd18f304e3bc5cee58c9c901ff59f1782282f2feb7bae3139afd2024754e4f1ca7e60c4a47e11a + languageName: node + linkType: hard + "@types/isomorphic-fetch@npm:0.0.34": version: 0.0.34 resolution: "@types/isomorphic-fetch@npm:0.0.34" @@ -21803,6 +21842,15 @@ __metadata: languageName: node linkType: hard +"@types/mimos@npm:*": + version: 3.0.6 + resolution: "@types/mimos@npm:3.0.6" + dependencies: + "@types/mime-db": "npm:*" + checksum: 10c0/5a8693e27e0abce0641ec07b38b27f126e11c6ab1e9ea7ad52ff60065617ad1ac5e8209b5041d9184a9ab713aad022c4bee06aebfb6c33a9dfa1a18d1d35029a + languageName: node + linkType: hard + "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -22071,6 +22119,13 @@ __metadata: languageName: node linkType: hard +"@types/podium@npm:*": + version: 1.0.4 + resolution: "@types/podium@npm:1.0.4" + checksum: 10c0/d7a602f306b31eb5b62c8bdcc0d9f31034fbbf0b5f00bac552f7702128c121b4e21d291b19de2042683642d5af83243c8c6642e8846e6ef3c137594cc20b670b + languageName: node + linkType: hard + "@types/postcss-import@npm:^12": version: 12.0.1 resolution: "@types/postcss-import@npm:12.0.1" @@ -22459,6 +22514,15 @@ __metadata: languageName: node linkType: hard +"@types/shot@npm:*": + version: 4.0.5 + resolution: "@types/shot@npm:4.0.5" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/c01275b11a6d2694b1c3ee3b31cbfe7b8a48b35a73253185cea03e0ad6fc949a2c348ff40688601d39b8d837581312b96200df9f844bb1f6f786ce4710aa36b7 + languageName: node + linkType: hard + "@types/sinon-chai@npm:3.2.5": version: 3.2.5 resolution: "@types/sinon-chai@npm:3.2.5" @@ -35281,6 +35345,7 @@ __metadata: "@types/babel__preset-env": "npm:^7" "@types/bn.js": "npm:^5" "@types/classnames": "npm:2.3.1" + "@types/hapi": "npm:^18.0.15" "@types/jest": "npm:29.5.14" "@types/jsdom": "npm:^21" "@types/module-alias": "npm:^2" From 5f4f24a494d651cd3868542d6638790a2e1ad60f Mon Sep 17 00:00:00 2001 From: dschom Date: Wed, 29 Oct 2025 16:21:44 -0700 Subject: [PATCH 2/4] task(libs/accounts): Port email sending code to libs Because: - We want need a standalone library that can be used to send emails This Commit: - Ports the email sending code from auth-server into libs --- libs/accounts/email-sender/.eslintrc.json | 18 ++ libs/accounts/email-sender/README.md | 11 + libs/accounts/email-sender/jest.config.ts | 21 ++ libs/accounts/email-sender/package.json | 9 + libs/accounts/email-sender/project.json | 37 +++ libs/accounts/email-sender/src/bounces.ts | 106 +++++++ .../accounts/email-sender/src/email-sender.ts | 276 ++++++++++++++++++ libs/accounts/email-sender/src/index.ts | 6 + libs/accounts/email-sender/tsconfig.json | 23 ++ libs/accounts/email-sender/tsconfig.lib.json | 10 + libs/accounts/email-sender/tsconfig.spec.json | 14 + tsconfig.base.json | 1 + 12 files changed, 532 insertions(+) create mode 100644 libs/accounts/email-sender/.eslintrc.json create mode 100644 libs/accounts/email-sender/README.md create mode 100644 libs/accounts/email-sender/jest.config.ts create mode 100644 libs/accounts/email-sender/package.json create mode 100644 libs/accounts/email-sender/project.json create mode 100644 libs/accounts/email-sender/src/bounces.ts create mode 100644 libs/accounts/email-sender/src/email-sender.ts create mode 100644 libs/accounts/email-sender/src/index.ts create mode 100644 libs/accounts/email-sender/tsconfig.json create mode 100644 libs/accounts/email-sender/tsconfig.lib.json create mode 100644 libs/accounts/email-sender/tsconfig.spec.json diff --git a/libs/accounts/email-sender/.eslintrc.json b/libs/accounts/email-sender/.eslintrc.json new file mode 100644 index 00000000000..3456be9b903 --- /dev/null +++ b/libs/accounts/email-sender/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/accounts/email-sender/README.md b/libs/accounts/email-sender/README.md new file mode 100644 index 00000000000..d3da2882fcd --- /dev/null +++ b/libs/accounts/email-sender/README.md @@ -0,0 +1,11 @@ +# accounts-email-sender + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build accounts-email-senders` to build the library. + +## Running unit tests + +Run `nx test-unit accounts-email-senders` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/accounts/email-sender/jest.config.ts b/libs/accounts/email-sender/jest.config.ts new file mode 100644 index 00000000000..ac0d72e2840 --- /dev/null +++ b/libs/accounts/email-sender/jest.config.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ +export default { + displayName: 'accounts-email-sender', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/accounts/email-sender', + reporters: [ + 'default', + [ + 'jest-junit', + { + outputDirectory: 'artifacts/tests/lib/accounts/email-sender', + outputName: 'accounts-email-sender-jest-unit-results.xml', + }, + ], + ], +}; diff --git a/libs/accounts/email-sender/package.json b/libs/accounts/email-sender/package.json new file mode 100644 index 00000000000..d7f86331311 --- /dev/null +++ b/libs/accounts/email-sender/package.json @@ -0,0 +1,9 @@ +{ + "name": "@fxa/accounts/email-sender", + "version": "0.0.1", + "dependencies": {}, + "type": "commonjs", + "main": "./index.cjs", + "types": "./index.d.ts", + "private": true +} diff --git a/libs/accounts/email-sender/project.json b/libs/accounts/email-sender/project.json new file mode 100644 index 00000000000..c83cb1de467 --- /dev/null +++ b/libs/accounts/email-sender/project.json @@ -0,0 +1,37 @@ +{ + "name": "accounts-email-sender", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/accounts/email-sender/src", + "projectType": "library", + "tags": ["scope:shared:lib"], + "targets": { + "build": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/accounts/email-sender", + "main": "libs/accounts/email-sender/src/index.ts", + "tsConfig": "libs/accounts/email-sender/tsconfig.lib.json", + "assets": ["libs/accounts/email-sender/*.md"], + "format": ["cjs"], + "generatePackageJson": true + } + }, + "test-unit": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/accounts/email-sender/jest.config.ts", + "testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"] + } + }, + "test-integration": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/accounts/email-sender/jest.config.ts", + "testPathPattern": ["\\.in\\.spec\\.ts$"] + } + } + } +} diff --git a/libs/accounts/email-sender/src/bounces.ts b/libs/accounts/email-sender/src/bounces.ts new file mode 100644 index 00000000000..ef781c2f9bb --- /dev/null +++ b/libs/accounts/email-sender/src/bounces.ts @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { AppError } from '@fxa/accounts/errors'; + +export type BouncesConfig = { + enabled: boolean; + hard: Record; + soft: Record; + complaint: Record; + ignoreTemplates: string[]; +}; + +export type Bounce = { + bounceType: number; + createdAt: number; +}; + +export type Tally = { + count: number; + latest: number; +}; + +export type BounceDb = { + emailBounces: { + findByEmail(email: string): Promise>; + }; +}; + +const BOUNCE_TYPE_HARD = 1; +const BOUNCE_TYPE_SOFT = 2; +const BOUNCE_TYPE_COMPLAINT = 3; + +export class Bounces { + private readonly bounceRules: Record; + + constructor( + private readonly config: BouncesConfig, + private readonly db: BounceDb + ) { + this.bounceRules = { + [BOUNCE_TYPE_HARD]: Object.freeze(config.hard || {}), + [BOUNCE_TYPE_SOFT]: Object.freeze(config.soft || {}), + [BOUNCE_TYPE_COMPLAINT]: Object.freeze(config.complaint || {}), + }; + } + + async check(email: string, template: string) { + if (this.config.enabled) { + return await this.checkBounces(email, template); + } + } + + async checkBounces(email: string, template: string) { + if (this.config.ignoreTemplates.includes(template)) { + return; + } + + const bounces = await this.db.emailBounces.findByEmail(email); + this.applyRules(bounces); + } + + private applyRules(bounces: Array) { + const tallies: Record = { + [BOUNCE_TYPE_HARD]: { + count: 0, + latest: 0, + }, + [BOUNCE_TYPE_COMPLAINT]: { + count: 0, + latest: 0, + }, + [BOUNCE_TYPE_SOFT]: { + count: 0, + latest: 0, + }, + }; + const now = Date.now(); + + bounces.forEach((bounce: Bounce) => { + const type = bounce.bounceType; + const ruleSet = this.bounceRules[type]; + if (ruleSet) { + const tally = tallies[type]; + const tier = ruleSet[tally.count]; + if (!tally.latest) { + tally.latest = bounce.createdAt; + } + if (tier && bounce.createdAt > now - tier) { + if (type === BOUNCE_TYPE_HARD) { + throw AppError.emailBouncedHard(tally.latest); + } + if (type === BOUNCE_TYPE_COMPLAINT) { + throw AppError.emailComplaint(tally.latest); + } + if (type === BOUNCE_TYPE_SOFT) { + throw AppError.emailBouncedSoft(tally.latest); + } + } + tally.count++; + } + }); + return tallies; + } +} diff --git a/libs/accounts/email-sender/src/email-sender.ts b/libs/accounts/email-sender/src/email-sender.ts new file mode 100644 index 00000000000..746a19ac97d --- /dev/null +++ b/libs/accounts/email-sender/src/email-sender.ts @@ -0,0 +1,276 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { SES } from '@aws-sdk/client-ses'; +import { Bounces } from './bounces'; +import { StatsD } from 'hot-shots'; +import { ILogger } from '@fxa/shared/log'; +import * as nodemailer from 'nodemailer'; +import { isAppError } from '@fxa/accounts/errors'; + +/** + * Config required for the mailer + */ +export type MailerConfig = { + /** SMTP Host */ + host: string; + /** The SMTP server port */ + port: number; + /** If host is 'secured' */ + secure: boolean; + /** If we can ignore TLS, this should only be the case for local dev/test */ + ignoreTLS: boolean; + /** Can connections be pooled */ + pool: boolean; + /** Max number of connections */ + maxConnections: number; + /** Max messages that can be outstanding */ + maxMessages: number; + /** Max rate at which messages can be sent. */ + sendingRate: number; + /** Connectiong timeout for smtp server. */ + connectionTimeout: number; + /** Greeting time out for smtp server. */ + greetingTimeout: number; + /** Socket timeout for smtp server connection. */ + socketTimeout: number; + /** DNS timeout for smtp server connection. */ + dnsTimeout: number; + + /** Optional user name. If not supplied, we fallback to local SES config. */ + user?: string; + /** Optional password. If not supplied, we fallback to local SES config. */ + password?: string; + sesConfigurationSet?: any; + sender: string; +}; + +/** + * Represents an email that can be sent out + */ +export type Email = { + /** Email recipient */ + to: string; + /** Optional cc recipeents */ + cc?: string; + /** Email sender */ + from: string; + /** Name of template used to render email */ + template: string; + /** The version of the template used to render email */ + version: number; + /** Subject of email */ + subject: string; + /** Preview text of email */ + preview: string; + /** HTML content of email */ + html: string; + /** Text content of email */ + text: string; + /** Email headers */ + headers: Record; +}; + +/** + * Sends an email to end end user. + */ +export class EmailSender { + private readonly emailClient: nodemailer.Transporter; + + constructor( + private readonly config: MailerConfig, + private readonly bounces: Bounces, + private readonly statsd: StatsD, + private readonly log: ILogger + ) { + + // Determine auth credentials + const auth = (() => { + + // If the user name and password are set use this + if (config.user && config.password) { + return { + auth: { + user: config.user, + pass: config.password, + }, + }; + } + + // Otherwise fallback to the SES configuration + const ses = new SES({ + // The key apiVersion is no longer supported in v3, and can be removed. + // @deprecated The client uses the "latest" apiVersion. + apiVersion: '2010-12-01', + }); + return { + SES: { ses }, + sendingRate: 5, + maxConnections: 10, + }; + })(); + + // Build node mailer options + const options = { + host: config.host, + secure: config.secure, + ignoreTLS: !config.secure, + port: config.port, + pool: config.pool, + maxConnections: config.maxConnections, + maxMessages: config.maxMessages, + connectionTimeout: config.connectionTimeout, + greetingTimeout: config.greetingTimeout, + socketTimeout: config.socketTimeout, + dnsTimeout: config.dnsTimeout, + sendingRate: this.config.sendingRate, + ...auth, + }; + this.emailClient = nodemailer.createTransport(options); + } + + async buildHeaders({ + template, + context, + headers, + }: { + template: { + name: string; + version: number; + metricsName?: string; + }; + context: { + serverName: string; + language: string; + deviceId?: string; + flowId?: string; + flowBeginTime?: number; + service?: string; + uid?: string; + entrypoint?: string; + cmsRpClientId?: string; + }; + headers: Record; + }) { + const optionalHeader = (key: string, value?: string | number) => { + if (value) { + return { [key]: value.toString() }; + } + return {}; + }; + + headers = { + 'Content-Language': context.language, + 'X-Template-Name': template.name, + 'X-Template-Version': template.version.toString(), + ...headers, + ...optionalHeader('X-Device-Id', context.deviceId), + ...optionalHeader('X-Flow-Id', context.flowId), + ...optionalHeader('X-Flow-Begin-Time', context.flowBeginTime), + ...optionalHeader('X-Service-Id', context.service), + ...optionalHeader('X-Uid', context.uid), + }; + + if (this.config.sesConfigurationSet) { + const sesHeaders = [ + `messageType=fxa-${template.metricsName || template.name}`, + 'app=fxa', + `service=${context.serverName}`, + `ses:feedback-id-a=fxa-${template.name}`, + ]; + if (context.cmsRpClientId && context.entrypoint) { + sesHeaders.push(`cmsRp=${context.cmsRpClientId}-${context.entrypoint}`); + } + + // Note on SES Event Publishing: The X-SES-CONFIGURATION-SET and + // X-SES-MESSAGE-TAGS email headers will be stripped by SES from the + // actual outgoing email messages. + headers['X-SES-CONFIGURATION-SET'] = this.config.sesConfigurationSet; + headers['X-SES-MESSAGE-TAGS'] = sesHeaders.join(', '); + } + + return headers; + } + + async send({ + to, + template, + headers, + from, + cc, + subject, + preview, + text, + html, + }: Email) { + try { + await this.bounces.check(to, template); + } catch (err) { + const tags = { template: template, error: err.errno }; + this.statsd.increment('email.bounce.limit', tags); + + this.log.error('email.bounce.limit', { + err: err.message, + errno: err.errno, + to, + template, + }); + return; + } + + this.log.debug('mailer.send', { + email: to, + template, + headers: Object.keys(headers).join(','), + }); + + // Send email + await new Promise((resolve, reject) => { + const email = { + from, + to, + cc, + subject, + preview, + text, + html, + xMailer: false, + headers, + }; + + this.emailClient.sendMail(email, (err: Error | null, status) => { + if (err) { + if (isAppError(err)) { + this.log.error('mailer.send.error', { + err: err.message, + code: err.code, + errno: err.errno, + message: status && status.message, + to, + template, + }); + } else { + this.log.error('mailer.send.error', { + err: err.message, + message: status && status.message, + to, + template, + }); + } + return reject(err); + } + + this.log.debug('mailer.send', { + status: status && status.message, + id: status && status.messageId, + to, + }); + + // We expose the headers, becuase there's legacy logic in auth-server that + // uses the header values to + return resolve({ email, status }); + }); + }); + } +} diff --git a/libs/accounts/email-sender/src/index.ts b/libs/accounts/email-sender/src/index.ts new file mode 100644 index 00000000000..1a60d6d036a --- /dev/null +++ b/libs/accounts/email-sender/src/index.ts @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export * from './email-sender'; +export * from './bounces'; diff --git a/libs/accounts/email-sender/tsconfig.json b/libs/accounts/email-sender/tsconfig.json new file mode 100644 index 00000000000..86622aca55a --- /dev/null +++ b/libs/accounts/email-sender/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/accounts/email-sender/tsconfig.lib.json b/libs/accounts/email-sender/tsconfig.lib.json new file mode 100644 index 00000000000..4befa7f0990 --- /dev/null +++ b/libs/accounts/email-sender/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/accounts/email-sender/tsconfig.spec.json b/libs/accounts/email-sender/tsconfig.spec.json new file mode 100644 index 00000000000..69a251f328c --- /dev/null +++ b/libs/accounts/email-sender/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index afaef6f83ee..e823613d231 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -25,6 +25,7 @@ "baseUrl": ".", "paths": { "@fxa/accounts/errors": ["libs/accounts/errors/src/index.ts"], + "@fxa/accounts/email-sender": ["libs/accounts/email-sender/src/index.ts"], "@fxa/accounts/rate-limit": ["libs/accounts/rate-limit/src/index.ts"], "@fxa/accounts/recovery-phone": [ "libs/accounts/recovery-phone/src/index.ts" From 0f599c80d44e44b31549b561ffc6978aba29d552 Mon Sep 17 00:00:00 2001 From: dschom Date: Mon, 27 Oct 2025 09:54:30 -0700 Subject: [PATCH 3/4] task(libs/accounts): Port email sending code to libs Because: - We want need a standalone library that can be used to send emails This Commit: - Ports the email sending code from auth-server into libs --- tsconfig.base.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.base.json b/tsconfig.base.json index e823613d231..06bdc5d467f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -26,6 +26,7 @@ "paths": { "@fxa/accounts/errors": ["libs/accounts/errors/src/index.ts"], "@fxa/accounts/email-sender": ["libs/accounts/email-sender/src/index.ts"], + "@fxa/accounts/email-renderer": ["libs/accounts/email-renderer/src/index.ts"], "@fxa/accounts/rate-limit": ["libs/accounts/rate-limit/src/index.ts"], "@fxa/accounts/recovery-phone": [ "libs/accounts/recovery-phone/src/index.ts" From 8bac81b48304f7f5faff6c7ce0282daf9cfb8f36 Mon Sep 17 00:00:00 2001 From: dschom Date: Wed, 29 Oct 2025 12:02:05 -0700 Subject: [PATCH 4/4] task(libs/accounts): Port email rendering code to libs Because: - We need a standalone library that can be used to render emails This Commit: - Ports the email rendering code from auth-server to libs - Converts to typescript - Cleans up code and improves consistency. --- .gitignore | 3 + libs/accounts/email-renderer/.eslintrc.json | 18 + .../email-renderer/.storybook/main.ts | 70 ++ .../email-renderer/.storybook/preview.ts | 32 + .../.storybook/tsconfig.storybook.json | 3 + libs/accounts/email-renderer/README.md | 49 + libs/accounts/email-renderer/jest.config.ts | 21 + libs/accounts/email-renderer/package.json | 9 + libs/accounts/email-renderer/project.json | 67 ++ libs/accounts/email-renderer/src/bindings.ts | 189 +++ .../src/css/appBadges/index.css | 139 +++ .../src/css/bannerWarning/index.css | 148 +++ .../src/css/brandMessaging/index.css | 150 +++ .../email-renderer/src/css/button/index.css | 138 +++ .../email-renderer/src/css/fxa/index.css | 180 +++ .../email-renderer/src/css/global.css | 123 ++ .../email-renderer/src/css/locale-dir.css | 13 + .../src/css/subscription/index.css | 225 ++++ .../email-renderer/src/css/userInfo/index.css | 190 +++ libs/accounts/email-renderer/src/global.scss | 197 ++++ .../accounts/email-renderer/src/index.spec.ts | 42 + libs/accounts/email-renderer/src/index.ts | 8 + libs/accounts/email-renderer/src/l10n.ts | 169 +++ .../email-renderer/src/layouts/fxa/en.ftl | 11 + .../email-renderer/src/layouts/fxa/index.mjml | 66 ++ .../email-renderer/src/layouts/fxa/index.scss | 115 ++ .../src/layouts/fxa/index.stories.ts | 54 + .../email-renderer/src/layouts/fxa/index.ts | 21 + .../email-renderer/src/layouts/fxa/index.txt | 14 + .../email-renderer/src/layouts/fxa/mocks.ts | 6 + .../src/layouts/fxa/strapi.mjml | 59 + .../src/layouts/fxa/strapi.stories.ts | 29 + .../email-renderer/src/layouts/fxa/strapi.txt | 10 + .../src/layouts/subscription/en.ftl | 36 + .../src/layouts/subscription/index.mjml | 116 ++ .../src/layouts/subscription/index.scss | 169 +++ .../src/layouts/subscription/index.stories.ts | 88 ++ .../src/layouts/subscription/index.ts | 15 + .../src/layouts/subscription/index.txt | 61 + .../src/layouts/subscription/mocks.ts | 6 + .../email-renderer/src/locale-dir.scss | 17 + .../email-renderer/src/mjml-browser-helper.ts | 114 ++ .../partials/accountDeletionInfoBlock/en.ftl | 4 + .../accountDeletionInfoBlock/index.mjml | 20 + .../accountDeletionInfoBlock/index.stories.ts | 24 + .../accountDeletionInfoBlock/index.txt | 5 + .../src/partials/appBadges/en.ftl | 21 + .../src/partials/appBadges/index.mjml | 55 + .../src/partials/appBadges/index.scss | 23 + .../src/partials/appBadges/index.txt | 8 + .../automatedEmailChangePassword/en.ftl | 6 + .../automatedEmailChangePassword/index.mjml | 16 + .../index.stories.ts | 25 + .../automatedEmailChangePassword/index.txt | 5 + .../automatedEmailInactiveAccount/en.ftl | 1 + .../automatedEmailInactiveAccount/index.mjml | 13 + .../index.stories.ts | 24 + .../automatedEmailInactiveAccount/index.txt | 1 + .../partials/automatedEmailNoAction/en.ftl | 3 + .../automatedEmailNoAction/index.mjml | 14 + .../automatedEmailNoAction/index.stories.ts | 24 + .../partials/automatedEmailNoAction/index.txt | 1 + .../automatedEmailNotAuthorized/en.ftl | 2 + .../index.stories.ts | 25 + .../automatedEmailNotAuthorized/index.txt | 2 + .../partials/automatedEmailRecoveryKey/en.ftl | 44 + .../automatedEmailRecoveryKey/index.mjml | 45 + .../index.stories.ts | 44 + .../automatedEmailRecoveryKey/index.txt | 23 + .../automatedEmailResetPassword/en.ftl | 5 + .../automatedEmailResetPassword/index.mjml | 16 + .../index.stories.ts | 25 + .../automatedEmailResetPassword/index.txt | 4 + .../en.ftl | 8 + .../index.mjml | 17 + .../index.stories.ts | 27 + .../index.txt | 8 + .../src/partials/bannerWarning/en.ftl | 4 + .../src/partials/bannerWarning/index.mjml | 24 + .../src/partials/bannerWarning/index.scss | 22 + .../src/partials/bannerWarning/index.txt | 3 + .../src/partials/brandMessaging/en.ftl | 1 + .../src/partials/brandMessaging/index.mjml | 19 + .../src/partials/brandMessaging/index.scss | 25 + .../src/partials/brandMessaging/index.txt | 6 + .../src/partials/button/index.mjml | 13 + .../src/partials/button/index.scss | 20 + .../src/partials/cancellationSurvey/en.ftl | 3 + .../partials/cancellationSurvey/index.mjml | 13 + .../src/partials/cancellationSurvey/index.txt | 2 + .../src/partials/changePassword/en.ftl | 1 + .../src/partials/changePassword/index.txt | 2 + .../src/partials/icon/index.mjml | 18 + .../email-renderer/src/partials/images.mjml | 63 + .../src/partials/manageAccount/en.ftl | 2 + .../src/partials/manageAccount/index.txt | 2 + .../email-renderer/src/partials/metadata.mjml | 26 + .../email-renderer/src/partials/mocks.ts | 6 + .../src/partials/paymentPlanDetails/en.ftl | 11 + .../partials/paymentPlanDetails/index.mjml | 30 + .../src/partials/paymentPlanDetails/index.txt | 6 + .../src/partials/paymentProvider/en.ftl | 14 + .../src/partials/paymentProvider/index.mjml | 27 + .../src/partials/paymentProvider/index.txt | 11 + .../src/partials/subscriptionCharges/en.ftl | 53 + .../partials/subscriptionCharges/index.mjml | 207 ++++ .../partials/subscriptionCharges/index.txt | 40 + .../src/partials/subscriptionSupport/en.ftl | 3 + .../partials/subscriptionSupport/index.mjml | 13 + .../partials/subscriptionSupport/index.txt | 2 + .../subscriptionSupportContact/en.ftl | 5 + .../subscriptionSupportContact/index.mjml | 9 + .../subscriptionSupportContact/index.txt | 2 + .../subscriptionSupportGetHelp/en.ftl | 7 + .../subscriptionSupportGetHelp/index.mjml | 25 + .../subscriptionSupportGetHelp/index.txt | 5 + .../subscriptionUpdateBillingEnsure/en.ftl | 3 + .../index.mjml | 9 + .../subscriptionUpdateBillingEnsure/index.txt | 2 + .../subscriptionUpdateBillingTry/en.ftl | 3 + .../subscriptionUpdateBillingTry/index.mjml | 13 + .../subscriptionUpdateBillingTry/index.txt | 2 + .../partials/subscriptionUpdatePayment/en.ftl | 3 + .../subscriptionUpdatePayment/index.mjml | 13 + .../subscriptionUpdatePayment/index.txt | 2 + .../src/partials/support/en.ftl | 7 + .../src/partials/support/index.mjml | 9 + .../src/partials/support/index.txt | 1 + .../src/partials/userDevice/en.ftl | 9 + .../src/partials/userDevice/index.mjml | 19 + .../src/partials/userDevice/index.stories.ts | 64 + .../src/partials/userDevice/index.txt | 14 + .../src/partials/userDevice/mocks.ts | 26 + .../src/partials/userInfo/index.mjml | 32 + .../src/partials/userInfo/index.scss | 18 + .../src/partials/userInfo/index.stories.ts | 48 + .../src/partials/userInfo/index.txt | 5 + .../src/partials/userInfo/mocks.ts | 22 + .../src/partials/userLocation/en.ftl | 16 + .../src/partials/userLocation/index.mjml | 28 + .../partials/userLocation/index.stories.ts | 58 + .../src/partials/userLocation/index.txt | 12 + .../src/partials/userLocation/mocks.ts | 22 + .../src/partials/viewInvoice/en.ftl | 5 + .../src/partials/viewInvoice/index.mjml | 7 + .../src/partials/viewInvoice/index.txt | 1 + .../src/renderer/bindings-browser.ts | 96 ++ .../src/renderer/bindings-node.ts | 89 ++ .../email-renderer/src/renderer/bindings.ts | 196 ++++ .../src/renderer/email-link-builder.ts | 20 + .../src/renderer/email-renderer.ts | 222 ++++ .../src/renderer/fxa-email-renderer.ts | 684 +++++++++++ .../email-renderer/src/renderer/index.ts | 12 + .../src/renderer/subplat-email-renderer.ts | 304 +++++ .../email-renderer/src/renderer/vendor/ejs.js | 2 + .../email-renderer/src/sass-compile-files.ts | 69 ++ .../email-renderer/src/storybook-email.tsx | 180 +++ .../accounts/email-renderer/src/storybook.css | 81 ++ .../src/templates/_storybook/index.mjml | 40 + .../src/templates/_storybook/index.txt | 1 + .../src/templates/_storybook/strapi.mjml | 40 + .../src/templates/_storybook/strapi.txt | 1 + .../src/templates/adminResetAccounts/en.ftl | 2 + .../templates/adminResetAccounts/index.mjml | 25 + .../adminResetAccounts/index.stories.ts | 33 + .../src/templates/adminResetAccounts/index.ts | 20 + .../templates/adminResetAccounts/index.txt | 3 + .../src/templates/cadReminderFirst/en.ftl | 6 + .../src/templates/cadReminderFirst/index.mjml | 24 + .../cadReminderFirst/index.stories.ts | 26 + .../src/templates/cadReminderFirst/index.ts | 23 + .../src/templates/cadReminderFirst/index.txt | 8 + .../src/templates/cadReminderSecond/en.ftl | 5 + .../templates/cadReminderSecond/index.mjml | 31 + .../cadReminderSecond/index.stories.ts | 26 + .../src/templates/cadReminderSecond/index.ts | 23 + .../src/templates/cadReminderSecond/index.txt | 9 + .../src/templates/downloadSubscription/en.ftl | 8 + .../templates/downloadSubscription/index.mjml | 36 + .../downloadSubscription/index.stories.ts | 56 + .../templates/downloadSubscription/index.ts | 28 + .../templates/downloadSubscription/index.txt | 6 + .../fraudulentAccountDeletion/en.ftl | 8 + .../fraudulentAccountDeletion/index.mjml | 31 + .../index.stories.ts | 25 + .../fraudulentAccountDeletion/index.ts | 18 + .../fraudulentAccountDeletion/index.txt | 9 + .../inactiveAccountFinalWarning/en.ftl | 10 + .../inactiveAccountFinalWarning/index.mjml | 30 + .../index.stories.ts | 26 + .../inactiveAccountFinalWarning/index.ts | 26 + .../inactiveAccountFinalWarning/index.txt | 12 + .../inactiveAccountFirstWarning/en.ftl | 11 + .../inactiveAccountFirstWarning/index.mjml | 33 + .../index.stories.ts | 26 + .../inactiveAccountFirstWarning/index.ts | 26 + .../inactiveAccountFirstWarning/index.txt | 14 + .../inactiveAccountSecondWarning/en.ftl | 10 + .../inactiveAccountSecondWarning/index.mjml | 30 + .../index.stories.ts | 25 + .../inactiveAccountSecondWarning/index.ts | 26 + .../inactiveAccountSecondWarning/index.txt | 12 + .../email-renderer/src/templates/index.ts | 1 + .../src/templates/lowRecoveryCodes/en.ftl | 18 + .../src/templates/lowRecoveryCodes/index.mjml | 43 + .../lowRecoveryCodes/index.stories.ts | 44 + .../src/templates/lowRecoveryCodes/index.ts | 32 + .../src/templates/lowRecoveryCodes/index.txt | 29 + .../src/templates/newDeviceLogin/en.ftl | 12 + .../src/templates/newDeviceLogin/index.mjml | 29 + .../templates/newDeviceLogin/index.stories.ts | 47 + .../src/templates/newDeviceLogin/index.ts | 54 + .../src/templates/newDeviceLogin/index.txt | 11 + .../src/templates/newDeviceLogin/strapi.mjml | 32 + .../newDeviceLogin/strapi.stories.ts | 41 + .../src/templates/newDeviceLogin/strapi.txt | 13 + .../templates/passwordChangeRequired/en.ftl | 11 + .../passwordChangeRequired/index.mjml | 35 + .../passwordChangeRequired/index.stories.ts | 24 + .../templates/passwordChangeRequired/index.ts | 24 + .../passwordChangeRequired/index.txt | 12 + .../src/templates/passwordChanged/en.ftl | 3 + .../src/templates/passwordChanged/index.mjml | 18 + .../passwordChanged/index.stories.ts | 26 + .../src/templates/passwordChanged/index.ts | 29 + .../src/templates/passwordChanged/index.txt | 8 + .../src/templates/passwordForgotOtp/en.ftl | 8 + .../templates/passwordForgotOtp/index.mjml | 44 + .../passwordForgotOtp/index.stories.ts | 26 + .../src/templates/passwordForgotOtp/index.ts | 33 + .../src/templates/passwordForgotOtp/index.txt | 13 + .../src/templates/passwordReset/en.ftl | 4 + .../src/templates/passwordReset/index.mjml | 23 + .../templates/passwordReset/index.stories.ts | 26 + .../src/templates/passwordReset/index.ts | 29 + .../src/templates/passwordReset/index.txt | 9 + .../passwordResetAccountRecovery/en.ftl | 8 + .../passwordResetAccountRecovery/index.mjml | 33 + .../index.stories.ts | 27 + .../passwordResetAccountRecovery/index.ts | 35 + .../passwordResetAccountRecovery/index.txt | 13 + .../passwordResetRecoveryPhone/en.ftl | 5 + .../passwordResetRecoveryPhone/index.mjml | 23 + .../index.stories.ts | 30 + .../passwordResetRecoveryPhone/index.ts | 38 + .../passwordResetRecoveryPhone/index.txt | 10 + .../passwordResetWithRecoveryKeyPrompt/en.ftl | 9 + .../index.mjml | 32 + .../index.stories.ts | 28 + .../index.ts | 35 + .../index.txt | 13 + .../templates/postAddAccountRecovery/en.ftl | 7 + .../postAddAccountRecovery/index.mjml | 35 + .../postAddAccountRecovery/index.stories.ts | 28 + .../templates/postAddAccountRecovery/index.ts | 35 + .../postAddAccountRecovery/index.txt | 9 + .../src/templates/postAddLinkedAccount/en.ftl | 5 + .../templates/postAddLinkedAccount/index.mjml | 18 + .../postAddLinkedAccount/index.stories.ts | 39 + .../templates/postAddLinkedAccount/index.ts | 34 + .../templates/postAddLinkedAccount/index.txt | 9 + .../src/templates/postAddRecoveryPhone/en.ftl | 11 + .../templates/postAddRecoveryPhone/index.mjml | 33 + .../postAddRecoveryPhone/index.stories.ts | 30 + .../templates/postAddRecoveryPhone/index.ts | 40 + .../templates/postAddRecoveryPhone/index.txt | 15 + .../postAddTwoStepAuthentication/en.ftl | 13 + .../postAddTwoStepAuthentication/index.mjml | 48 + .../index.stories.ts | 42 + .../postAddTwoStepAuthentication/index.ts | 39 + .../postAddTwoStepAuthentication/index.txt | 21 + .../postChangeAccountRecovery/en.ftl | 5 + .../postChangeAccountRecovery/index.mjml | 38 + .../index.stories.ts | 29 + .../postChangeAccountRecovery/index.ts | 36 + .../postChangeAccountRecovery/index.txt | 9 + .../src/templates/postChangePrimary/en.ftl | 6 + .../templates/postChangePrimary/index.mjml | 22 + .../postChangePrimary/index.stories.ts | 26 + .../src/templates/postChangePrimary/index.ts | 23 + .../src/templates/postChangePrimary/index.txt | 9 + .../templates/postChangeRecoveryPhone/en.ftl | 5 + .../postChangeRecoveryPhone/index.mjml | 25 + .../postChangeRecoveryPhone/index.stories.ts | 26 + .../postChangeRecoveryPhone/index.ts | 33 + .../postChangeRecoveryPhone/index.txt | 10 + .../postChangeTwoStepAuthentication/en.ftl | 9 + .../index.mjml | 34 + .../index.stories.ts | 29 + .../postChangeTwoStepAuthentication/index.ts | 39 + .../postChangeTwoStepAuthentication/index.txt | 13 + .../templates/postConsumeRecoveryCode/en.ftl | 7 + .../postConsumeRecoveryCode/index.mjml | 24 + .../postConsumeRecoveryCode/index.stories.ts | 29 + .../postConsumeRecoveryCode/index.ts | 39 + .../postConsumeRecoveryCode/index.txt | 9 + .../src/templates/postNewRecoveryCodes/en.ftl | 5 + .../templates/postNewRecoveryCodes/index.mjml | 24 + .../postNewRecoveryCodes/index.stories.ts | 27 + .../templates/postNewRecoveryCodes/index.ts | 34 + .../templates/postNewRecoveryCodes/index.txt | 9 + .../postRemoveAccountRecovery/en.ftl | 5 + .../postRemoveAccountRecovery/includes.json | 10 + .../postRemoveAccountRecovery/index.mjml | 35 + .../index.stories.ts | 27 + .../postRemoveAccountRecovery/index.ts | 34 + .../postRemoveAccountRecovery/index.txt | 9 + .../templates/postRemoveRecoveryPhone/en.ftl | 6 + .../postRemoveRecoveryPhone/index.mjml | 28 + .../postRemoveRecoveryPhone/index.stories.ts | 26 + .../postRemoveRecoveryPhone/index.ts | 32 + .../postRemoveRecoveryPhone/index.txt | 11 + .../src/templates/postRemoveSecondary/en.ftl | 6 + .../templates/postRemoveSecondary/index.mjml | 31 + .../postRemoveSecondary/index.stories.ts | 25 + .../templates/postRemoveSecondary/index.ts | 22 + .../templates/postRemoveSecondary/index.txt | 9 + .../postRemoveTwoStepAuthentication/en.ftl | 6 + .../includes.json | 0 .../index.mjml | 26 + .../index.stories.ts | 27 + .../postRemoveTwoStepAuthentication/index.ts | 34 + .../postRemoveTwoStepAuthentication/index.txt | 11 + .../templates/postSigninRecoveryCode/en.ftl | 6 + .../postSigninRecoveryCode/index.mjml | 27 + .../postSigninRecoveryCode/index.stories.ts | 27 + .../templates/postSigninRecoveryCode/index.ts | 38 + .../postSigninRecoveryCode/index.txt | 12 + .../templates/postSigninRecoveryPhone/en.ftl | 6 + .../postSigninRecoveryPhone/index.mjml | 27 + .../postSigninRecoveryPhone/index.stories.ts | 27 + .../postSigninRecoveryPhone/index.ts | 38 + .../postSigninRecoveryPhone/index.txt | 12 + .../src/templates/postVerify/en.ftl | 7 + .../src/templates/postVerify/index.mjml | 33 + .../src/templates/postVerify/index.stories.ts | 39 + .../src/templates/postVerify/index.ts | 24 + .../src/templates/postVerify/index.txt | 13 + .../src/templates/postVerifySecondary/en.ftl | 6 + .../templates/postVerifySecondary/index.mjml | 33 + .../postVerifySecondary/index.stories.ts | 26 + .../templates/postVerifySecondary/index.ts | 22 + .../templates/postVerifySecondary/index.txt | 7 + .../src/templates/recovery/en.ftl | 7 + .../src/templates/recovery/index.mjml | 35 + .../src/templates/recovery/index.stories.ts | 26 + .../src/templates/recovery/index.ts | 32 + .../src/templates/recovery/index.txt | 12 + .../subscriptionAccountDeletion/en.ftl | 9 + .../subscriptionAccountDeletion/index.mjml | 23 + .../index.stories.ts | 29 + .../subscriptionAccountDeletion/index.ts | 21 + .../subscriptionAccountDeletion/index.txt | 7 + .../subscriptionAccountFinishSetup/en.ftl | 9 + .../includes.json | 10 + .../subscriptionAccountFinishSetup/index.mjml | 34 + .../index.stories.ts | 58 + .../subscriptionAccountFinishSetup/index.ts | 25 + .../subscriptionAccountFinishSetup/index.txt | 13 + .../subscriptionAccountReminderFirst/en.ftl | 6 + .../index.mjml | 31 + .../index.stories.ts | 27 + .../subscriptionAccountReminderFirst/index.ts | 24 + .../index.txt | 12 + .../subscriptionAccountReminderSecond/en.ftl | 6 + .../index.mjml | 31 + .../index.stories.ts | 27 + .../index.ts | 24 + .../index.txt | 12 + .../templates/subscriptionCancellation/en.ftl | 16 + .../subscriptionCancellation/index.mjml | 34 + .../subscriptionCancellation/index.stories.ts | 32 + .../subscriptionCancellation/index.ts | 24 + .../subscriptionCancellation/index.txt | 14 + .../templates/subscriptionDowngrade/en.ftl | 18 + .../subscriptionDowngrade/index.mjml | 27 + .../subscriptionDowngrade/index.stories.ts | 35 + .../templates/subscriptionDowngrade/index.ts | 26 + .../templates/subscriptionDowngrade/index.txt | 11 + .../en.ftl | 7 + .../index.mjml | 23 + .../index.stories.ts | 27 + .../index.ts | 19 + .../index.txt | 7 + .../templates/subscriptionFirstInvoice/en.ftl | 14 + .../subscriptionFirstInvoice/index.mjml | 42 + .../subscriptionFirstInvoice/index.stories.ts | 160 +++ .../subscriptionFirstInvoice/index.ts | 48 + .../subscriptionFirstInvoice/index.txt | 16 + .../subscriptionPaymentExpired/en.ftl | 7 + .../subscriptionPaymentExpired/index.mjml | 20 + .../index.stories.ts | 26 + .../subscriptionPaymentExpired/index.ts | 19 + .../subscriptionPaymentExpired/index.txt | 9 + .../subscriptionPaymentFailed/en.ftl | 8 + .../subscriptionPaymentFailed/index.mjml | 32 + .../index.stories.ts | 27 + .../subscriptionPaymentFailed/index.ts | 20 + .../subscriptionPaymentFailed/index.txt | 10 + .../en.ftl | 8 + .../index.mjml | 26 + .../index.stories.ts | 26 + .../index.ts | 19 + .../index.txt | 10 + .../templates/subscriptionReactivation/en.ftl | 10 + .../subscriptionReactivation/index.mjml | 21 + .../subscriptionReactivation/index.stories.ts | 28 + .../subscriptionReactivation/index.ts | 21 + .../subscriptionReactivation/index.txt | 5 + .../subscriptionRenewalReminder/en.ftl | 17 + .../subscriptionRenewalReminder/index.mjml | 32 + .../index.stories.ts | 30 + .../subscriptionRenewalReminder/index.ts | 23 + .../subscriptionRenewalReminder/index.txt | 15 + .../src/templates/subscriptionReplaced/en.ftl | 8 + .../subscriptionReplaced/includes.json | 6 + .../templates/subscriptionReplaced/index.mjml | 29 + .../subscriptionReplaced/index.stories.ts | 24 + .../templates/subscriptionReplaced/index.ts | 17 + .../templates/subscriptionReplaced/index.txt | 9 + .../subscriptionSubsequentInvoice/en.ftl | 10 + .../includes.json | 6 + .../subscriptionSubsequentInvoice/index.mjml | 30 + .../index.stories.ts | 176 +++ .../subscriptionSubsequentInvoice/index.ts | 41 + .../subscriptionSubsequentInvoice/index.txt | 12 + .../src/templates/subscriptionUpgrade/en.ftl | 48 + .../templates/subscriptionUpgrade/index.mjml | 46 + .../subscriptionUpgrade/index.stories.ts | 115 ++ .../templates/subscriptionUpgrade/index.ts | 26 + .../templates/subscriptionUpgrade/index.txt | 76 ++ .../subscriptionsPaymentExpired/en.ftl | 3 + .../subscriptionsPaymentExpired/index.mjml | 30 + .../index.stories.ts | 36 + .../subscriptionsPaymentExpired/index.ts | 20 + .../subscriptionsPaymentExpired/index.txt | 11 + .../en.ftl | 4 + .../index.mjml | 36 + .../index.stories.ts | 36 + .../index.ts | 20 + .../index.txt | 13 + .../src/templates/unblockCode/en.ftl | 11 + .../src/templates/unblockCode/index.mjml | 32 + .../templates/unblockCode/index.stories.ts | 27 + .../src/templates/unblockCode/index.ts | 34 + .../src/templates/unblockCode/index.txt | 8 + .../verificationReminderFinal/en.ftl | 4 + .../verificationReminderFinal/index.mjml | 17 + .../index.stories.ts | 24 + .../verificationReminderFinal/index.ts | 21 + .../verificationReminderFinal/index.txt | 7 + .../verificationReminderFirst/en.ftl | 7 + .../verificationReminderFirst/index.mjml | 28 + .../index.stories.ts | 24 + .../verificationReminderFirst/index.ts | 21 + .../verificationReminderFirst/index.txt | 11 + .../verificationReminderSecond/en.ftl | 6 + .../verificationReminderSecond/includes.json | 10 + .../verificationReminderSecond/index.mjml | 31 + .../index.stories.ts | 24 + .../verificationReminderSecond/index.ts | 21 + .../verificationReminderSecond/index.txt | 13 + .../src/templates/verify/en.ftl | 4 + .../src/templates/verify/index.mjml | 22 + .../src/templates/verify/index.stories.ts | 27 + .../src/templates/verify/index.ts | 34 + .../src/templates/verify/index.txt | 10 + .../src/templates/verifyAccountChange/en.ftl | 19 + .../verifyAccountChange/includes.json | 0 .../templates/verifyAccountChange/index.mjml | 36 + .../verifyAccountChange/index.stories.ts | 28 + .../templates/verifyAccountChange/index.ts | 36 + .../templates/verifyAccountChange/index.txt | 13 + .../src/templates/verifyLogin/en.ftl | 6 + .../src/templates/verifyLogin/includes.json | 10 + .../src/templates/verifyLogin/index.mjml | 30 + .../templates/verifyLogin/index.stories.ts | 35 + .../src/templates/verifyLogin/index.ts | 35 + .../src/templates/verifyLogin/index.txt | 12 + .../src/templates/verifyLoginCode/en.ftl | 11 + .../src/templates/verifyLoginCode/index.mjml | 34 + .../verifyLoginCode/index.stories.ts | 28 + .../src/templates/verifyLoginCode/index.ts | 42 + .../src/templates/verifyLoginCode/index.txt | 13 + .../src/templates/verifyLoginCode/strapi.mjml | 34 + .../verifyLoginCode/strapi.stories.ts | 34 + .../src/templates/verifyLoginCode/strapi.txt | 13 + .../src/templates/verifyPrimary/en.ftl | 6 + .../src/templates/verifyPrimary/includes.json | 10 + .../src/templates/verifyPrimary/index.mjml | 41 + .../templates/verifyPrimary/index.stories.ts | 28 + .../src/templates/verifyPrimary/index.ts | 35 + .../src/templates/verifyPrimary/index.txt | 13 + .../src/templates/verifySecondaryCode/en.ftl | 11 + .../templates/verifySecondaryCode/index.mjml | 33 + .../verifySecondaryCode/index.stories.ts | 27 + .../templates/verifySecondaryCode/index.ts | 38 + .../templates/verifySecondaryCode/index.txt | 13 + .../src/templates/verifyShortCode/en.ftl | 9 + .../src/templates/verifyShortCode/index.mjml | 32 + .../verifyShortCode/index.stories.ts | 26 + .../src/templates/verifyShortCode/index.ts | 39 + .../src/templates/verifyShortCode/index.txt | 13 + .../src/templates/verifyShortCode/strapi.mjml | 32 + .../verifyShortCode/strapi.stories.ts | 34 + .../src/templates/verifyShortCode/strapi.txt | 13 + libs/accounts/email-renderer/tsconfig.json | 23 + .../accounts/email-renderer/tsconfig.lib.json | 10 + .../email-renderer/tsconfig.spec.json | 14 + package.json | 17 +- yarn.lock | 1029 +++++++---------- 511 files changed, 15366 insertions(+), 617 deletions(-) create mode 100644 libs/accounts/email-renderer/.eslintrc.json create mode 100644 libs/accounts/email-renderer/.storybook/main.ts create mode 100644 libs/accounts/email-renderer/.storybook/preview.ts create mode 100644 libs/accounts/email-renderer/.storybook/tsconfig.storybook.json create mode 100644 libs/accounts/email-renderer/README.md create mode 100644 libs/accounts/email-renderer/jest.config.ts create mode 100644 libs/accounts/email-renderer/package.json create mode 100644 libs/accounts/email-renderer/project.json create mode 100644 libs/accounts/email-renderer/src/bindings.ts create mode 100644 libs/accounts/email-renderer/src/css/appBadges/index.css create mode 100644 libs/accounts/email-renderer/src/css/bannerWarning/index.css create mode 100644 libs/accounts/email-renderer/src/css/brandMessaging/index.css create mode 100644 libs/accounts/email-renderer/src/css/button/index.css create mode 100644 libs/accounts/email-renderer/src/css/fxa/index.css create mode 100644 libs/accounts/email-renderer/src/css/global.css create mode 100644 libs/accounts/email-renderer/src/css/locale-dir.css create mode 100644 libs/accounts/email-renderer/src/css/subscription/index.css create mode 100644 libs/accounts/email-renderer/src/css/userInfo/index.css create mode 100644 libs/accounts/email-renderer/src/global.scss create mode 100644 libs/accounts/email-renderer/src/index.spec.ts create mode 100644 libs/accounts/email-renderer/src/index.ts create mode 100644 libs/accounts/email-renderer/src/l10n.ts create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/en.ftl create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/index.mjml create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/index.scss create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/index.ts create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/index.txt create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/mocks.ts create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/strapi.mjml create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/strapi.stories.ts create mode 100644 libs/accounts/email-renderer/src/layouts/fxa/strapi.txt create mode 100644 libs/accounts/email-renderer/src/layouts/subscription/en.ftl create mode 100644 libs/accounts/email-renderer/src/layouts/subscription/index.mjml create mode 100644 libs/accounts/email-renderer/src/layouts/subscription/index.scss create mode 100644 libs/accounts/email-renderer/src/layouts/subscription/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/layouts/subscription/index.ts create mode 100644 libs/accounts/email-renderer/src/layouts/subscription/index.txt create mode 100644 libs/accounts/email-renderer/src/layouts/subscription/mocks.ts create mode 100644 libs/accounts/email-renderer/src/locale-dir.scss create mode 100644 libs/accounts/email-renderer/src/mjml-browser-helper.ts create mode 100644 libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/appBadges/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/appBadges/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/appBadges/index.scss create mode 100644 libs/accounts/email-renderer/src/partials/appBadges/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailNoAction/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/bannerWarning/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/bannerWarning/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/bannerWarning/index.scss create mode 100644 libs/accounts/email-renderer/src/partials/bannerWarning/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/brandMessaging/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/brandMessaging/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/brandMessaging/index.scss create mode 100644 libs/accounts/email-renderer/src/partials/brandMessaging/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/button/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/button/index.scss create mode 100644 libs/accounts/email-renderer/src/partials/cancellationSurvey/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/cancellationSurvey/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/cancellationSurvey/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/changePassword/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/changePassword/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/icon/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/images.mjml create mode 100644 libs/accounts/email-renderer/src/partials/manageAccount/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/manageAccount/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/metadata.mjml create mode 100644 libs/accounts/email-renderer/src/partials/mocks.ts create mode 100644 libs/accounts/email-renderer/src/partials/paymentPlanDetails/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/paymentPlanDetails/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/paymentPlanDetails/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/paymentProvider/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/paymentProvider/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/paymentProvider/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionCharges/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionCharges/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionCharges/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionSupport/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionSupport/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionSupport/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionSupportContact/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionSupportContact/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionSupportContact/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/support/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/support/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/support/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/userDevice/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/userDevice/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/userDevice/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/userDevice/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/userDevice/mocks.ts create mode 100644 libs/accounts/email-renderer/src/partials/userInfo/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/userInfo/index.scss create mode 100644 libs/accounts/email-renderer/src/partials/userInfo/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/userInfo/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/userInfo/mocks.ts create mode 100644 libs/accounts/email-renderer/src/partials/userLocation/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/userLocation/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/userLocation/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/partials/userLocation/index.txt create mode 100644 libs/accounts/email-renderer/src/partials/userLocation/mocks.ts create mode 100644 libs/accounts/email-renderer/src/partials/viewInvoice/en.ftl create mode 100644 libs/accounts/email-renderer/src/partials/viewInvoice/index.mjml create mode 100644 libs/accounts/email-renderer/src/partials/viewInvoice/index.txt create mode 100644 libs/accounts/email-renderer/src/renderer/bindings-browser.ts create mode 100644 libs/accounts/email-renderer/src/renderer/bindings-node.ts create mode 100644 libs/accounts/email-renderer/src/renderer/bindings.ts create mode 100644 libs/accounts/email-renderer/src/renderer/email-link-builder.ts create mode 100644 libs/accounts/email-renderer/src/renderer/email-renderer.ts create mode 100644 libs/accounts/email-renderer/src/renderer/fxa-email-renderer.ts create mode 100644 libs/accounts/email-renderer/src/renderer/index.ts create mode 100644 libs/accounts/email-renderer/src/renderer/subplat-email-renderer.ts create mode 100644 libs/accounts/email-renderer/src/renderer/vendor/ejs.js create mode 100644 libs/accounts/email-renderer/src/sass-compile-files.ts create mode 100644 libs/accounts/email-renderer/src/storybook-email.tsx create mode 100644 libs/accounts/email-renderer/src/storybook.css create mode 100644 libs/accounts/email-renderer/src/templates/_storybook/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/_storybook/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/_storybook/strapi.mjml create mode 100644 libs/accounts/email-renderer/src/templates/_storybook/strapi.txt create mode 100644 libs/accounts/email-renderer/src/templates/adminResetAccounts/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/adminResetAccounts/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/adminResetAccounts/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/adminResetAccounts/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/adminResetAccounts/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderFirst/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderFirst/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderFirst/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderFirst/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderFirst/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderSecond/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderSecond/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderSecond/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderSecond/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/cadReminderSecond/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/downloadSubscription/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/downloadSubscription/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/downloadSubscription/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/downloadSubscription/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/downloadSubscription/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/lowRecoveryCodes/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/newDeviceLogin/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/newDeviceLogin/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/newDeviceLogin/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/newDeviceLogin/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/newDeviceLogin/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.mjml create mode 100644 libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.txt create mode 100644 libs/accounts/email-renderer/src/templates/passwordChangeRequired/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/passwordChanged/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/passwordChanged/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/passwordChanged/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordChanged/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordChanged/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/passwordForgotOtp/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/passwordReset/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/passwordReset/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/passwordReset/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordReset/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordReset/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postAddAccountRecovery/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postAddLinkedAccount/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postChangePrimary/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postChangePrimary/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postChangePrimary/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postChangePrimary/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postChangePrimary/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/includes.json create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveSecondary/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/includes.json create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postVerify/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postVerify/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postVerify/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postVerify/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postVerify/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/postVerifySecondary/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/postVerifySecondary/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/postVerifySecondary/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/postVerifySecondary/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/postVerifySecondary/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/recovery/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/recovery/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/recovery/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/recovery/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/recovery/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/includes.json create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionCancellation/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionDowngrade/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReactivation/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReplaced/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReplaced/includes.json create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/includes.json create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionUpgrade/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/unblockCode/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/unblockCode/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/unblockCode/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/unblockCode/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/unblockCode/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFinal/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFirst/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderSecond/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderSecond/includes.json create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verify/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verify/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verify/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verify/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verify/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verifyAccountChange/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verifyAccountChange/includes.json create mode 100644 libs/accounts/email-renderer/src/templates/verifyAccountChange/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verifyAccountChange/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyAccountChange/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyAccountChange/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verifyLogin/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verifyLogin/includes.json create mode 100644 libs/accounts/email-renderer/src/templates/verifyLogin/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verifyLogin/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyLogin/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyLogin/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verifyLoginCode/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verifyLoginCode/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verifyLoginCode/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyLoginCode/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyLoginCode/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.txt create mode 100644 libs/accounts/email-renderer/src/templates/verifyPrimary/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verifyPrimary/includes.json create mode 100644 libs/accounts/email-renderer/src/templates/verifyPrimary/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verifyPrimary/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyPrimary/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyPrimary/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verifySecondaryCode/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verifyShortCode/en.ftl create mode 100644 libs/accounts/email-renderer/src/templates/verifyShortCode/index.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verifyShortCode/index.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyShortCode/index.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyShortCode/index.txt create mode 100644 libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.mjml create mode 100644 libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.stories.ts create mode 100644 libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.txt create mode 100644 libs/accounts/email-renderer/tsconfig.json create mode 100644 libs/accounts/email-renderer/tsconfig.lib.json create mode 100644 libs/accounts/email-renderer/tsconfig.spec.json diff --git a/.gitignore b/.gitignore index b477bc03041..d8716e8fcf2 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,9 @@ Thumbs.db # command is called .venv +## Lib Specific +libs/accounts/email-renderer/public/locales + ## Package-specific ## # circleci diff --git a/libs/accounts/email-renderer/.eslintrc.json b/libs/accounts/email-renderer/.eslintrc.json new file mode 100644 index 00000000000..3456be9b903 --- /dev/null +++ b/libs/accounts/email-renderer/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/accounts/email-renderer/.storybook/main.ts b/libs/accounts/email-renderer/.storybook/main.ts new file mode 100644 index 00000000000..52b491fb2dc --- /dev/null +++ b/libs/accounts/email-renderer/.storybook/main.ts @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); +const path = require('path'); + +export default { + framework: { + name: '@storybook/html-webpack5', + options: {}, +}, + stories: [ + '../src/**/*.stories.ts' + ], + staticDirs: process.env.STORYBOOK_BUILD !== 'true' ? ['..'] : undefined, + addons: [ + '@storybook/addon-webpack5-compiler-babel', + '@storybook/addon-docs', + '@storybook/addon-controls', + '@storybook/addon-toolbars', + ], + core: { + builder: 'webpack5', + }, + babel: async (options) => { + const babelConfig = { + ...options, + "sourceType": "unambiguous", + "presets": [ + [ + "@babel/preset-env", + { + "targets": "> 0.25%, last 2 versions, not dead" + } + ], + "@babel/preset-typescript", + ], + "plugins": [] + } + console.log('babel config!'); + + return babelConfig; + }, + + features: { storyStoreV7: true }, + // Added to resolve path aliases set in /tsconfig.base.json + // tsconfig.storybook.json is necessary to replace the *.ts extension in tsconfig.base.json + // with a *.js extension. Other than that it should remain the same. + async webpackFinal(config, { configType }) { + config.resolve.plugins = [ + new TsconfigPathsPlugin({ + configFile: path.resolve(__dirname, './tsconfig.storybook.json'), + }), + ]; + + config.resolve.fallback = { + // fs: false, // Keep this as it's the standard Storybook approach. + + // // This is often needed when using internal Node libs in the browser: + // path: false, + // os: false, + // "http": require.resolve("stream-http"), + // "https": require.resolve("https-browserify"), + + // // **Potential fix for fs** - by adding 'process' which is another common Node dependency + // process: require.resolve('process/browser'), + }; + return config; + }, +}; diff --git a/libs/accounts/email-renderer/.storybook/preview.ts b/libs/accounts/email-renderer/.storybook/preview.ts new file mode 100644 index 00000000000..17609378114 --- /dev/null +++ b/libs/accounts/email-renderer/.storybook/preview.ts @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import '../src/storybook.css'; + +export const parameters = { + actions: { argTypesRegex: '^on[A-Z].*' }, +}; + +export const globalTypes = { + direction: { + name: 'Text directionality', + description: 'Set text to LTR or RTL direction', + defaultValue: 'ltr', + toolbar: { + icon: 'transfer', + items: [ + { + value: 'ltr', + right: '➡️', + title: 'Left to Right', + }, + { + value: 'rtl', + right: '⬅️', + title: 'Right to Left', + }, + ], + }, + }, +}; diff --git a/libs/accounts/email-renderer/.storybook/tsconfig.storybook.json b/libs/accounts/email-renderer/.storybook/tsconfig.storybook.json new file mode 100644 index 00000000000..3c43903cfdd --- /dev/null +++ b/libs/accounts/email-renderer/.storybook/tsconfig.storybook.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} diff --git a/libs/accounts/email-renderer/README.md b/libs/accounts/email-renderer/README.md new file mode 100644 index 00000000000..a462271edfa --- /dev/null +++ b/libs/accounts/email-renderer/README.md @@ -0,0 +1,49 @@ +# account email renderer + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build accounts-email-renderer` to build the library. + +## Running unit tests + +Run `nx test accounts-email-renderer` to execute the unit tests via [Jest](https://jestjs.io). + +## Viewing Story Books + +Run `nx storybook accounts-email-renderer` + +## Using this library in your service + +The install is like most other libs, except there's a good chance you'll have to copy assests from this lib into your build's dist folder. + +The simplest way to do this is with the copyfiles utility. Here's an example gist, from admin-server's package.json + + `"copy-email-assets": "copyfiles --up 1 '../../libs/accounts/email-renderer/**/*.{mjml,ftl,txt,css}' dist/libs/ ",` + +### Other Gotchas +One tricky thing about how this project is crafted is that the lib is designed to be used in Node on the server server side, but the storybooks are +run in a web browser context. Be careful not include the `node-bindings.ts` from storybook. Doing so will result in weird errors about missing +polyfils! Don't simply try adding this polyfils. Instead, make sure `node-bindings.ts` isn't accidentally getting imported by storybook. + +### Adding a new Email + +To add a new email, do the following. + +1. Go to src/templates, and copy one of the exiting folders renaming it as desired. +2. Next update the index.mjml to construct your template. +3. Next update index.txt to construct your text version of the email. +4. Next update en.ftl and make sure all l10n id's are in place. +5. Next update index.ts. You should have: + a. Strongly typed `TemplateData` showing the property the template expected + b. A template const that reflects the template names, and matches the folder name. + c. A version const that reflects the current 'version' of the template + d. A layout const that reflects the intended layout. + d. Includes, which help us render a subject, action, or preview of the email +6. Next create index.stories.ts filling out the various states and render states as needed. +7. Next open the `fxa-email-render.ts` and follow the pattern there. + a. Import the new template folder + b. Create method corresponding to the template folder + c. Follow the established pattern to wire up the template and expose it. +8. Finally run `nx storybook accounts-email-renderer` to preview what the email looks. diff --git a/libs/accounts/email-renderer/jest.config.ts b/libs/accounts/email-renderer/jest.config.ts new file mode 100644 index 00000000000..9227f0310fc --- /dev/null +++ b/libs/accounts/email-renderer/jest.config.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ +export default { + displayName: 'accounts-email-renderer', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/accounts/email-renderer', + reporters: [ + 'default', + [ + 'jest-junit', + { + outputDirectory: 'artifacts/tests/lib/accounts/email-renderer', + outputName: 'accounts-email-renderer-jest-unit-results.xml', + }, + ], + ], +}; diff --git a/libs/accounts/email-renderer/package.json b/libs/accounts/email-renderer/package.json new file mode 100644 index 00000000000..442b6f90173 --- /dev/null +++ b/libs/accounts/email-renderer/package.json @@ -0,0 +1,9 @@ +{ + "name": "@fxa/accounts/email-renderer", + "version": "0.0.1", + "dependencies": {}, + "type": "commonjs", + "main": "./index.cjs", + "types": "./index.d.ts", + "private": true +} diff --git a/libs/accounts/email-renderer/project.json b/libs/accounts/email-renderer/project.json new file mode 100644 index 00000000000..b7d28a6444e --- /dev/null +++ b/libs/accounts/email-renderer/project.json @@ -0,0 +1,67 @@ +{ + "name": "accounts-email-renderer", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/accounts/email-renderer/src", + "projectType": "library", + "tags": ["scope:shared:lib"], + "targets": { + "build": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "dependsOn": ["l10n-prime"], + "options": { + "outputPath": "dist/libs/accounts/email-renderer", + "main": "libs/accounts/email-renderer/src/index.ts", + "tsConfig": "libs/accounts/email-renderer/tsconfig.lib.json", + "assets": ["libs/accounts/email-renderer/*.md"], + "format": ["cjs"], + "generatePackageJson": true + } + }, + "test-unit": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/accounts/email-renderer/jest.config.ts", + "testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"] + } + }, + "test-integration": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/accounts/email-renderer/jest.config.ts", + "testPathPattern": ["\\.in\\.spec\\.ts$"] + } + }, + "build-storybook": { + "executor": "@nx/storybook:build", + "outputs": ["{options.outputDir}"], + "options": { + "outputDir": "dist/storybook/accounts/email-renderer", + "configDir": "libs/accounts/email-renderer/.storybook" + } + }, + "storybook": { + "executor": "@nx/storybook:storybook", + "options": { + "port": 4400, + "configDir": "libs/accounts/email-renderer/.storybook", + "browserTarget": "accounts-email-renderer:build" + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "l10n-prime": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "_scripts/l10n/prime.sh libs/accounts/email-renderer" + ] + } + } + } +} diff --git a/libs/accounts/email-renderer/src/bindings.ts b/libs/accounts/email-renderer/src/bindings.ts new file mode 100644 index 00000000000..dc7da298d9d --- /dev/null +++ b/libs/accounts/email-renderer/src/bindings.ts @@ -0,0 +1,189 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// NOTE: This file handled with browser ESLint bindings +// instead of NodeJS for DOM typings support +/* eslint-env browser */ + +import { LocalizerOpts, ILocalizerBindings } from './l10n'; + +// Supporting Types +export type EjsOpts = { + root?: string; +}; + +export type MjmlOpts = { + validationLevel?: 'strict' | 'soft' | 'skip'; + filePath?: string; + ignoreIncludes?: boolean; + minify?: boolean; +}; + +export type TemplateOpts = { + basePath: string; + cssPath: string; +}; + +export type RenderOpts = { + templates: TemplateOpts; + ejs: EjsOpts; + mjml: MjmlOpts; +}; + +type TemplateContextValue = + | string + | Record + | number + | Date + | null + | undefined; + +// Eventually we can list all available values here, or separate them by template +export type TemplateValues = { + numberRemaining?: number; + subscriptions?: Record[]; + [key: string]: TemplateContextValue; +}; + +export type ComponentTarget = 'index' | 'strapi'; + +export interface TemplateContext { + acceptLanguage?: string; + template: string; + version: number; + layout?: string; + target?: ComponentTarget; + subject?: string; + templateValues?: TemplateValues; +} + +export interface RendererContext extends TemplateContext, TemplateValues { + // cssPath is relative to where rendering occurs + cssPath: string; + subject: string; + action?: string; + preview?: string; + clientName?: string; +} + +export type EjsComponent = { + mjml: string; + text: string; +}; +export type TemplateResult = { + html: string; + text: string; + rootElement: Element; +}; +export type RendererOpts = RenderOpts & LocalizerOpts; +type ComponentType = 'templates' | 'layouts'; + +/** + * Abstraction for binding the renderer to different contexts, e.g. node vs browser. + */ +export abstract class RendererBindings implements ILocalizerBindings { + /** + * Customized options for the renderer + */ + abstract opts: RendererOpts; + + /** + * Renders a mjml template with support for fluent localization. + * @param template Name of template + * @param context Contains either values sent through mailer.send or mock values from Storybook + * @param layout Optional layout, which acts as wrapper for for template + * @returns Rendered template + */ + async renderTemplate( + template: string, + context: TemplateContext, + layout?: string, + target: ComponentTarget = 'index' + ): Promise { + context = { ...context, template }; + + let component = this.renderEjsComponent( + await this.getComponent('templates', template, target), + context + ); + + // Wrap component with layout + if (layout) { + component = this.renderEjsComponent( + await this.getComponent('layouts', layout, target), + context, + component + ); + } + + const { mjml, text } = component; + const html = this.mjml2html(mjml); + const rootElement = this.produceRootElement(html); + return { html, text, rootElement }; + } + + protected async getComponent( + type: ComponentType, + name: string, + target: ComponentTarget + ) { + const path = `${this.opts.templates.basePath}/${type}/${name}`; + const [mjml, text] = await Promise.all([ + this.fetchResource(`${path}/${target}.mjml`), + this.fetchResource(`${path}/${target}.txt`), + ]); + return { mjml, text }; + } + + /** + * Renders an EJS template + * @param component Component to render + * @param context Context used to fill template variables. + * @param body Optional body to wrap + */ + protected renderEjsComponent( + component: EjsComponent, + context: TemplateContext, + body?: EjsComponent + ): EjsComponent { + const { mjml, text } = component; + return { + mjml: this.renderEjs(mjml, context, body?.mjml), + text: this.renderEjs(text, context, body?.text), + }; + } + + /** + * Fetches a resource + * @param path Path to resource + */ + abstract fetchResource(path: string): Promise; + + /** + * Renders EJS + * @param ejsTemplate Raw template to render + * @param context Context to fill template with + * @param body Optional body to wrap + * @returns Rendered EJS template + */ + abstract renderEjs( + ejsTemplate: string, + context: TemplateContext, + body?: string + ): string; + + /** + * Renders MJML into HTML + * @param mjml MJML markup + * @returns HTML + */ + protected abstract mjml2html(mjml: string): string; + + /** + * Produces a DOM like element from an html string + * @param html HTML to parse + * @returns DOM like element + */ + protected abstract produceRootElement(html: string): Element; +} diff --git a/libs/accounts/email-renderer/src/css/appBadges/index.css b/libs/accounts/email-renderer/src/css/appBadges/index.css new file mode 100644 index 00000000000..c83dc65482e --- /dev/null +++ b/libs/accounts/email-renderer/src/css/appBadges/index.css @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.text-xs { + font-size: 12px !important; + line-height: 20px !important; +} +.text-sm { + font-size: 14px !important; + line-height: 22px !important; +} +.text-md { + font-size: 16px !important; + line-height: 24px !important; +} +.text-lg { + font-size: 18px !important; + line-height: 26px !important; +} +.text-xl { + font-size: 20px !important; + line-height: 28px !important; +} +.text-2xl { + font-size: 22px !important; + line-height: 30px !important; +} +.text-3xl { + font-size: 32px !important; + line-height: 36px !important; +} + +.font-sans, .text-footer div, .text-footer-appBadges div, .text-footer-automatedEmail div { + font-family: sans-serif !important; +} + +.text-blue-500, .link-blue { + color: #0060df; +} + +.mt-2 { + margin-top: 8px !important; +} +.mt-4 { + margin-top: 16px !important; +} +.mt-5 { + margin-top: 20px !important; +} +.mt-6 { + margin-top: 24px !important; +} +.mt-8 { + margin-top: 32px !important; +} +.mt-10, .text-footer-automatedEmail div { + margin-top: 40px !important; +} + +.mb-0 { + margin-bottom: 0px !important; +} +.mb-2 { + margin-bottom: 8px !important; +} +.mb-3, .text-footer-automatedEmail div { + margin-bottom: 12px !important; +} +.mb-4 { + margin-bottom: 16px !important; +} +.mb-5 { + margin-bottom: 20px !important; +} +.mb-6 { + margin-bottom: 24px !important; +} +.mb-8 { + margin-bottom: 32px !important; +} + +.px-6 { + padding-left: 24px !important; + padding-right: 24px !important; +} + +.p-4 { + padding: 16px !important; +} +.pb-1 { + padding-bottom: 4px !important; +} +.pb-2 { + padding-bottom: 8px !important; +} + +tbody > td:first-child, +tr > td:first-child { + padding: 0px !important; +} + +.link-blue { + text-decoration: underline; + font-family: sans-serif; +} + +.align-left div { + text-align: left !important; +} + +.hidden { + display: none; + width: 0; + height: 0; + max-height: 0; + line-height: 0; + overflow: hidden; +} + +.text-footer div, .text-footer-appBadges div, .text-footer-automatedEmail div { + text-align: center !important; + color: #6d6d6e !important; +} + +.app-badges { + max-width: 310px !important; + text-align: center !important; +} + +.app-badge { + padding: 40px 0px 20px !important; +} +.app-badge img { + height: 44px !important; + margin: 0 auto !important; +} \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/css/bannerWarning/index.css b/libs/accounts/email-renderer/src/css/bannerWarning/index.css new file mode 100644 index 00000000000..69565accb80 --- /dev/null +++ b/libs/accounts/email-renderer/src/css/bannerWarning/index.css @@ -0,0 +1,148 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.text-xs, .banner-warning-text > div { + font-size: 12px !important; + line-height: 20px !important; +} +.text-sm { + font-size: 14px !important; + line-height: 22px !important; +} +.text-md { + font-size: 16px !important; + line-height: 24px !important; +} +.text-lg { + font-size: 18px !important; + line-height: 26px !important; +} +.text-xl { + font-size: 20px !important; + line-height: 28px !important; +} +.text-2xl { + font-size: 22px !important; + line-height: 30px !important; +} +.text-3xl { + font-size: 32px !important; + line-height: 36px !important; +} + +.font-sans, .text-footer div, .text-footer-automatedEmail div, .banner-warning-text > div { + font-family: sans-serif !important; +} + +.text-blue-500, .link-blue { + color: #0060df; +} + +.mt-2 { + margin-top: 8px !important; +} +.mt-4 { + margin-top: 16px !important; +} +.mt-5 { + margin-top: 20px !important; +} +.mt-6 { + margin-top: 24px !important; +} +.mt-8 { + margin-top: 32px !important; +} +.mt-10, .text-footer-automatedEmail div { + margin-top: 40px !important; +} + +.mb-0 { + margin-bottom: 0px !important; +} +.mb-2 { + margin-bottom: 8px !important; +} +.mb-3, .text-footer-automatedEmail div { + margin-bottom: 12px !important; +} +.mb-4 { + margin-bottom: 16px !important; +} +.mb-5 { + margin-bottom: 20px !important; +} +.mb-6 { + margin-bottom: 24px !important; +} +.mb-8 { + margin-bottom: 32px !important; +} + +.px-6 { + padding-left: 24px !important; + padding-right: 24px !important; +} + +.p-4, .banner-warning-container { + padding: 16px !important; +} +.pb-1 { + padding-bottom: 4px !important; +} +.pb-2 { + padding-bottom: 8px !important; +} + +tbody > td:first-child, +tr > td:first-child { + padding: 0px !important; +} + +.link-blue { + text-decoration: underline; + font-family: sans-serif; +} + +.banner-warning-text > div { + text-align: center !important; +} + +.banner-warning-container { + max-width: 640px !important; + width: 100% !important; +} + +.align-left div { + text-align: left !important; +} + +.hidden { + display: none; + width: 0; + height: 0; + max-height: 0; + line-height: 0; + overflow: hidden; +} + +.text-footer div, .text-footer-automatedEmail div { + text-align: center !important; + color: #6d6d6e !important; +} + +.banner-warning-container { + background-color: #7542e5; +} + +.banner-warning-text > div { + color: #fff !important; +} + +.banner-warning-text div span a { + text-decoration: none !important; + color: #fff !important; +} \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/css/brandMessaging/index.css b/libs/accounts/email-renderer/src/css/brandMessaging/index.css new file mode 100644 index 00000000000..c245193e2bc --- /dev/null +++ b/libs/accounts/email-renderer/src/css/brandMessaging/index.css @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.text-xs, .brand-message-text > div { + font-size: 12px !important; + line-height: 20px !important; +} +.text-sm { + font-size: 14px !important; + line-height: 22px !important; +} +.text-md { + font-size: 16px !important; + line-height: 24px !important; +} +.text-lg { + font-size: 18px !important; + line-height: 26px !important; +} +.text-xl { + font-size: 20px !important; + line-height: 28px !important; +} +.text-2xl { + font-size: 22px !important; + line-height: 30px !important; +} +.text-3xl { + font-size: 32px !important; + line-height: 36px !important; +} + +.font-sans, .text-footer div, .text-footer-automatedEmail div, .brand-message-text > div { + font-family: sans-serif !important; +} + +.text-blue-500, .link-blue { + color: #0060df; +} + +.mt-2 { + margin-top: 8px !important; +} +.mt-4 { + margin-top: 16px !important; +} +.mt-5 { + margin-top: 20px !important; +} +.mt-6 { + margin-top: 24px !important; +} +.mt-8 { + margin-top: 32px !important; +} +.mt-10, .text-footer-automatedEmail div { + margin-top: 40px !important; +} + +.mb-0 { + margin-bottom: 0px !important; +} +.mb-2 { + margin-bottom: 8px !important; +} +.mb-3, .text-footer-automatedEmail div { + margin-bottom: 12px !important; +} +.mb-4 { + margin-bottom: 16px !important; +} +.mb-5 { + margin-bottom: 20px !important; +} +.mb-6 { + margin-bottom: 24px !important; +} +.mb-8 { + margin-bottom: 32px !important; +} + +.px-6 { + padding-left: 24px !important; + padding-right: 24px !important; +} + +.p-4, .brand-message-text > div { + padding: 16px !important; +} +.pb-1 { + padding-bottom: 4px !important; +} +.pb-2 { + padding-bottom: 8px !important; +} + +tbody > td:first-child, +tr > td:first-child { + padding: 0px !important; +} + +.link-blue { + text-decoration: underline; + font-family: sans-serif; +} + +.brand-message-text > div { + text-align: center !important; +} + +.brand-message-container { + max-width: 640px !important; + width: 100% !important; +} + +.brand-message-text > div { + color: #000 !important; + font-weight: 700 !important; +} + +.brand-message-text div span a { + color: #000 !important; + text-decoration: underline !important; +} + +.align-left div { + text-align: left !important; +} + +.hidden { + display: none; + width: 0; + height: 0; + max-height: 0; + line-height: 0; + overflow: hidden; +} + +.text-footer div, .text-footer-automatedEmail div { + text-align: center !important; + color: #6d6d6e !important; +} + +.brand-message-container { + background: linear-gradient(88.76deg, #e4eaf6 3.37%, #dbeef8 39.93%, #daf3f4 65.09%, #e3f6ed 102.21%); + background-color: #dbeef8; +} \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/css/button/index.css b/libs/accounts/email-renderer/src/css/button/index.css new file mode 100644 index 00000000000..9c971136ae3 --- /dev/null +++ b/libs/accounts/email-renderer/src/css/button/index.css @@ -0,0 +1,138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.text-xs { + font-size: 12px !important; + line-height: 20px !important; +} +.text-sm { + font-size: 14px !important; + line-height: 22px !important; +} +.text-md { + font-size: 16px !important; + line-height: 24px !important; +} +.text-lg, .primary-button a { + font-size: 18px !important; + line-height: 26px !important; +} +.text-xl { + font-size: 20px !important; + line-height: 28px !important; +} +.text-2xl { + font-size: 22px !important; + line-height: 30px !important; +} +.text-3xl { + font-size: 32px !important; + line-height: 36px !important; +} + +.font-sans, .primary-button a, .text-footer div, .text-footer-automatedEmail div { + font-family: sans-serif !important; +} + +.text-blue-500, .link-blue { + color: #0060df; +} + +.mt-2 { + margin-top: 8px !important; +} +.mt-4 { + margin-top: 16px !important; +} +.mt-5 { + margin-top: 20px !important; +} +.mt-6 { + margin-top: 24px !important; +} +.mt-8 { + margin-top: 32px !important; +} +.mt-10, .text-footer-automatedEmail div { + margin-top: 40px !important; +} + +.mb-0 { + margin-bottom: 0px !important; +} +.mb-2 { + margin-bottom: 8px !important; +} +.mb-3, .text-footer-automatedEmail div { + margin-bottom: 12px !important; +} +.mb-4 { + margin-bottom: 16px !important; +} +.mb-5 { + margin-bottom: 20px !important; +} +.mb-6 { + margin-bottom: 24px !important; +} +.mb-8 { + margin-bottom: 32px !important; +} + +.px-6 { + padding-left: 24px !important; + padding-right: 24px !important; +} + +.p-4 { + padding: 16px !important; +} +.pb-1 { + padding-bottom: 4px !important; +} +.pb-2 { + padding-bottom: 8px !important; +} + +tbody > td:first-child, +tr > td:first-child { + padding: 0px !important; +} + +.link-blue { + text-decoration: underline; + font-family: sans-serif; +} + +.align-left div { + text-align: left !important; +} + +.hidden { + display: none; + width: 0; + height: 0; + max-height: 0; + line-height: 0; + overflow: hidden; +} + +.text-footer div, .text-footer-automatedEmail div { + text-align: center !important; + color: #6d6d6e !important; +} + +.primary-button { + height: 56px !important; + max-width: 310px !important; +} +.primary-button a { + background: #0060df !important; + max-width: 310px !important; + color: #fff; + padding: 8px 16px !important; + border-radius: 4px !important; +} \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/css/fxa/index.css b/libs/accounts/email-renderer/src/css/fxa/index.css new file mode 100644 index 00000000000..da60b93f6f7 --- /dev/null +++ b/libs/accounts/email-renderer/src/css/fxa/index.css @@ -0,0 +1,180 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.text-xs, .text-body-subtext div, .text-footer div, .text-footer-automatedEmail div { + font-size: 12px !important; + line-height: 20px !important; +} +.text-sm, .text-body div, .text-body-no-margin div, .text-body-top-margin div, .text-body-lg-bottom-margin div, .text-sub-body div { + font-size: 14px !important; + line-height: 22px !important; +} +.text-md { + font-size: 16px !important; + line-height: 24px !important; +} +.text-lg, .code-medium div { + font-size: 18px !important; + line-height: 26px !important; +} +.text-xl, .text-header div { + font-size: 20px !important; + line-height: 28px !important; +} +.text-2xl { + font-size: 22px !important; + line-height: 30px !important; +} +.text-3xl, .code-large div { + font-size: 32px !important; + line-height: 36px !important; +} + +.font-sans, .text-footer div, .text-footer-automatedEmail div, .text-body div, .text-body-no-margin div, .text-body-top-margin div, .text-body-lg-bottom-margin div, .text-sub-body div, .text-header div { + font-family: sans-serif !important; +} + +.text-blue-500, .link-blue { + color: #0060df; +} + +.mt-2, .sync-logo img, .graphic-devices img { + margin-top: 8px !important; +} +.mt-4 { + margin-top: 16px !important; +} +.mt-5, .text-body-subtext div, .text-body-top-margin div { + margin-top: 20px !important; +} +.mt-6 { + margin-top: 24px !important; +} +.mt-8 { + margin-top: 32px !important; +} +.mt-10, .text-footer-automatedEmail div { + margin-top: 40px !important; +} + +.mb-0 { + margin-bottom: 0px !important; +} +.mb-2 { + margin-bottom: 8px !important; +} +.mb-3, .text-header div, .text-sub-body div, .text-footer-automatedEmail div { + margin-bottom: 12px !important; +} +.mb-4 { + margin-bottom: 16px !important; +} +.mb-5, .text-body div, .text-body-subtext div, .sync-logo img, .graphic-devices img { + margin-bottom: 20px !important; +} +.mb-6, .code-medium div, .code-large div { + margin-bottom: 24px !important; +} +.mb-8, .text-body-lg-bottom-margin div { + margin-bottom: 32px !important; +} + +.px-6 { + padding-left: 24px !important; + padding-right: 24px !important; +} + +.p-4 { + padding: 16px !important; +} +.pb-1 { + padding-bottom: 4px !important; +} +.pb-2 { + padding-bottom: 8px !important; +} + +tbody > td:first-child, +tr > td:first-child { + padding: 0px !important; +} + +.link-blue { + text-decoration: underline; + font-family: sans-serif; +} + +.text-header div { + text-align: center !important; +} + +.align-left div { + text-align: left !important; +} + +.hidden { + display: none; + width: 0; + height: 0; + max-height: 0; + line-height: 0; + overflow: hidden; +} + +.text-footer div, .text-footer-automatedEmail div { + text-align: center !important; + color: #6d6d6e !important; +} + +.body { + max-width: 310px !important; + margin: 0px auto !important; +} + +.mozilla-logo { + padding: 40px 0px !important; + margin: 0 auto !important; + display: block !important; + align-items: center; +} + +.sync-img { + padding: 20px 0px !important; + height: 137px; + margin: 0 auto !important; + display: block !important; +} + +.text-sub-body div, .text-body-lg-bottom-margin div, .text-body-top-margin div, .text-body-no-margin div, .text-body div { + text-align: center !important; +} + +.text-body-subtext div { + text-align: center !important; +} + +.sync-logo img { + margin: 0 auto; +} + +.graphic-devices img { + margin: 0 auto; +} + +.code-large div, .code-medium div { + text-align: center !important; + font-family: monospace; +} + +.info-block { + margin: 28px 0 0 0; + background-color: #f0f0f4; + border-radius: 10px; +} + +.info-block div { + padding: 20px 32px 0; +} \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/css/global.css b/libs/accounts/email-renderer/src/css/global.css new file mode 100644 index 00000000000..d711dfd1319 --- /dev/null +++ b/libs/accounts/email-renderer/src/css/global.css @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.text-xs { + font-size: 12px !important; + line-height: 20px !important; +} +.text-sm { + font-size: 14px !important; + line-height: 22px !important; +} +.text-md { + font-size: 16px !important; + line-height: 24px !important; +} +.text-lg { + font-size: 18px !important; + line-height: 26px !important; +} +.text-xl { + font-size: 20px !important; + line-height: 28px !important; +} +.text-2xl { + font-size: 22px !important; + line-height: 30px !important; +} +.text-3xl { + font-size: 32px !important; + line-height: 36px !important; +} + +.font-sans, .text-footer div, .text-footer-automatedEmail div { + font-family: sans-serif !important; +} + +.text-blue-500, .link-blue { + color: #0060df; +} + +.mt-2 { + margin-top: 8px !important; +} +.mt-4 { + margin-top: 16px !important; +} +.mt-5 { + margin-top: 20px !important; +} +.mt-6 { + margin-top: 24px !important; +} +.mt-8 { + margin-top: 32px !important; +} +.mt-10, .text-footer-automatedEmail div { + margin-top: 40px !important; +} + +.mb-0 { + margin-bottom: 0px !important; +} +.mb-2 { + margin-bottom: 8px !important; +} +.mb-3, .text-footer-automatedEmail div { + margin-bottom: 12px !important; +} +.mb-4 { + margin-bottom: 16px !important; +} +.mb-5 { + margin-bottom: 20px !important; +} +.mb-6 { + margin-bottom: 24px !important; +} +.mb-8 { + margin-bottom: 32px !important; +} + +.px-6 { + padding-left: 24px !important; + padding-right: 24px !important; +} + +.p-4 { + padding: 16px !important; +} +.pb-1 { + padding-bottom: 4px !important; +} +.pb-2 { + padding-bottom: 8px !important; +} + +tbody > td:first-child, +tr > td:first-child { + padding: 0px !important; +} + +.link-blue { + text-decoration: underline; + font-family: sans-serif; +} + +.align-left div { + text-align: left !important; +} + +.hidden { + display: none; + width: 0; + height: 0; + max-height: 0; + line-height: 0; + overflow: hidden; +} + +.text-footer div, .text-footer-automatedEmail div { + text-align: center !important; + color: #6d6d6e !important; +} \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/css/locale-dir.css b/libs/accounts/email-renderer/src/css/locale-dir.css new file mode 100644 index 00000000000..423c666ebe5 --- /dev/null +++ b/libs/accounts/email-renderer/src/css/locale-dir.css @@ -0,0 +1,13 @@ +.ltr div { + direction: ltr !important; +} +.ltr td { + direction: ltr !important; +} + +.rtl div { + direction: rtl !important; +} +.rtl td { + direction: rtl !important; +} \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/css/subscription/index.css b/libs/accounts/email-renderer/src/css/subscription/index.css new file mode 100644 index 00000000000..267ece64ac6 --- /dev/null +++ b/libs/accounts/email-renderer/src/css/subscription/index.css @@ -0,0 +1,225 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.text-xs { + font-size: 12px !important; + line-height: 20px !important; +} +.text-sm, .footer-text div, .footer-text-bottom-margin div, .text-footer div, .text-footer-automatedEmail div { + font-size: 14px !important; + line-height: 22px !important; +} +.text-md, .text-body div, .text-body-mb-4 div, .text-body-top-margin div, .text-link, .text-body-link div, .text-body-bottom-link div, .text-body-no-bottom-margin div, .text-body-table-no-bottom-margin td { + font-size: 16px !important; + line-height: 24px !important; +} +.text-lg, .primary-button-subplat a { + font-size: 18px !important; + line-height: 26px !important; +} +.text-xl { + font-size: 20px !important; + line-height: 28px !important; +} +.text-2xl, .text-header div { + font-size: 22px !important; + line-height: 30px !important; +} +.text-3xl { + font-size: 32px !important; + line-height: 36px !important; +} + +.font-sans, .text-title, .text-title-table div, .text-title-link div, .footer-text div, .footer-text-bottom-margin div, .primary-button-subplat a, .text-footer div, .text-footer-automatedEmail div, .text-body div, .text-body-mb-4 div, .text-body-top-margin div, .text-link, .text-body-link div, .text-body-bottom-link div, .text-body-no-bottom-margin div, .text-body-table-no-bottom-margin td, .text-header div { + font-family: sans-serif !important; +} + +.text-blue-500, .link-blue { + color: #0060df; +} + +.mt-2, .text-link, .text-body-link div, .text-body-bottom-link div { + margin-top: 8px !important; +} +.mt-4, .text-body-top-margin div { + margin-top: 16px !important; +} +.mt-5 { + margin-top: 20px !important; +} +.mt-6, .footer-container { + margin-top: 24px !important; +} +.mt-8 { + margin-top: 32px !important; +} +.mt-10, .text-footer-automatedEmail div { + margin-top: 40px !important; +} + +.mb-0, .text-body-no-bottom-margin div, .text-body-table-no-bottom-margin td { + margin-bottom: 0px !important; +} +.mb-2 { + margin-bottom: 8px !important; +} +.mb-3, .footer-text-bottom-margin div, .text-footer-automatedEmail div { + margin-bottom: 12px !important; +} +.mb-4, .text-header div, .text-body-mb-4 div, .text-title-table div { + margin-bottom: 16px !important; +} +.mb-5 { + margin-bottom: 20px !important; +} +.mb-6, .text-body div, .text-body-bottom-link div, .header-container { + margin-bottom: 24px !important; +} +.mb-8 { + margin-bottom: 32px !important; +} + +.px-6 { + padding-left: 24px !important; + padding-right: 24px !important; +} + +.p-4 { + padding: 16px !important; +} +.pb-1 { + padding-bottom: 4px !important; +} +.pb-2 { + padding-bottom: 8px !important; +} + +tbody > td:first-child, +tr > td:first-child { + padding: 0px !important; +} + +.link-blue { + text-decoration: underline; + font-family: sans-serif; +} + +.text-header div { + text-align: center !important; +} + +.align-left div { + text-align: left !important; +} + +.hidden { + display: none; + width: 0; + height: 0; + max-height: 0; + line-height: 0; + overflow: hidden; +} + +.text-footer div, .text-footer-automatedEmail div { + text-align: center !important; + color: #6d6d6e !important; +} + +.body { + max-width: 640px !important; + margin: 0px auto !important; +} + +.text-header div { + font-weight: 700; + color: #464646 !important; +} + +.text-body-table-no-bottom-margin td, .text-body-no-bottom-margin div, .text-link, .text-body-bottom-link div, .text-body-link div, .text-body-top-margin div, .text-body-mb-4 div, .text-body div { + color: #4b5563 !important; +} + +.text-title, .text-title-link div, .text-title-table div { + font-size: 18px !important; + font-weight: 600 !important; + margin-top: 48px !important; +} + +.text-title-table div { + color: #4b5563 !important; +} + +.text-body-top-margin div { + color: #4b5563 !important; + padding: 4px 0; +} + +.text-link, .text-body-bottom-link div, .text-body-link div { + color: #0060df !important; + text-decoration: underline !important; +} + +.text-body li { + color: #4b5563 !important; +} + +.text-body-table-no-bottom-margin td { + padding: 4px 0; +} + +.header-container { + max-width: 640px !important; +} + +.subplat-mozilla-logo { + padding: 20px 0px !important; + display: block !important; +} +.subplat-mozilla-logo a, +.subplat-mozilla-logo img { + font-size: 12px !important; +} + +.mozilla-logo-footer { + padding: 32px 0px !important; + height: 34px !important; + display: block !important; + margin: 0 auto !important; +} + +.footer-container { + background-color: #000; + padding: 40px 24px !important; +} + +.footer-text div, .footer-text-bottom-margin div { + color: #fff !important; + text-align: center !important; +} + +.footer-link { + color: #fff !important; +} + +.primary-button-subplat { + height: 56px !important; + max-width: 310px !important; +} +.primary-button-subplat a { + background: #0060df !important; + max-width: 310px !important; + color: #fff; + display: unset !important; + padding: 12px 20px !important; + border-radius: 6px !important; +} + +.product-icon { + display: block; + margin: 0 auto; + height: 58px; +} \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/css/userInfo/index.css b/libs/accounts/email-renderer/src/css/userInfo/index.css new file mode 100644 index 00000000000..a15b4dedfbe --- /dev/null +++ b/libs/accounts/email-renderer/src/css/userInfo/index.css @@ -0,0 +1,190 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.text-xs, .text-body-subtext div, .text-footer div, .text-footer-automatedEmail div { + font-size: 12px !important; + line-height: 20px !important; +} +.text-sm, .text-body div, .text-body-no-margin div, .text-body-top-margin div, .text-body-lg-bottom-margin div, .text-sub-body div, .text-body-grey-no-margin div, .text-body-grey div { + font-size: 14px !important; + line-height: 22px !important; +} +.text-md { + font-size: 16px !important; + line-height: 24px !important; +} +.text-lg, .code-medium div { + font-size: 18px !important; + line-height: 26px !important; +} +.text-xl, .text-header div { + font-size: 20px !important; + line-height: 28px !important; +} +.text-2xl { + font-size: 22px !important; + line-height: 30px !important; +} +.text-3xl, .code-large div { + font-size: 32px !important; + line-height: 36px !important; +} + +.font-sans, .text-footer div, .text-footer-automatedEmail div, .text-body-grey-no-margin div, .text-body-grey div, .text-body div, .text-body-no-margin div, .text-body-top-margin div, .text-body-lg-bottom-margin div, .text-sub-body div, .text-header div { + font-family: sans-serif !important; +} + +.text-blue-500, .link-blue { + color: #0060df; +} + +.mt-2, .sync-logo img, .graphic-devices img { + margin-top: 8px !important; +} +.mt-4 { + margin-top: 16px !important; +} +.mt-5, .text-body-subtext div, .text-body-top-margin div { + margin-top: 20px !important; +} +.mt-6 { + margin-top: 24px !important; +} +.mt-8 { + margin-top: 32px !important; +} +.mt-10, .text-footer-automatedEmail div { + margin-top: 40px !important; +} + +.mb-0 { + margin-bottom: 0px !important; +} +.mb-2 { + margin-bottom: 8px !important; +} +.mb-3, .text-header div, .text-sub-body div, .text-footer-automatedEmail div { + margin-bottom: 12px !important; +} +.mb-4 { + margin-bottom: 16px !important; +} +.mb-5, .text-body div, .text-body-subtext div, .sync-logo img, .graphic-devices img { + margin-bottom: 20px !important; +} +.mb-6, .text-body-grey div, .code-medium div, .code-large div { + margin-bottom: 24px !important; +} +.mb-8, .text-body-lg-bottom-margin div { + margin-bottom: 32px !important; +} + +.px-6 { + padding-left: 24px !important; + padding-right: 24px !important; +} + +.p-4 { + padding: 16px !important; +} +.pb-1 { + padding-bottom: 4px !important; +} +.pb-2 { + padding-bottom: 8px !important; +} + +tbody > td:first-child, +tr > td:first-child { + padding: 0px !important; +} + +.link-blue { + text-decoration: underline; + font-family: sans-serif; +} + +.text-header div { + text-align: center !important; +} + +.align-left div { + text-align: left !important; +} + +.hidden { + display: none; + width: 0; + height: 0; + max-height: 0; + line-height: 0; + overflow: hidden; +} + +.text-footer div, .text-footer-automatedEmail div { + text-align: center !important; + color: #6d6d6e !important; +} + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.body { + max-width: 310px !important; + margin: 0px auto !important; +} + +.mozilla-logo { + padding: 40px 0px !important; + margin: 0 auto !important; + display: block !important; + align-items: center; +} + +.sync-img { + padding: 20px 0px !important; + height: 137px; + margin: 0 auto !important; + display: block !important; +} + +.text-body-grey-no-margin div, .text-body-grey div, .text-sub-body div, .text-body-lg-bottom-margin div, .text-body-top-margin div, .text-body-no-margin div, .text-body div { + text-align: center !important; +} + +.text-body-subtext div { + text-align: center !important; +} + +.sync-logo img { + margin: 0 auto; +} + +.graphic-devices img { + margin: 0 auto; +} + +.code-large div, .code-medium div { + text-align: center !important; + font-family: monospace; +} + +.info-block { + margin: 28px 0 0 0; + background-color: #f0f0f4; + border-radius: 10px; +} + +.info-block div { + padding: 20px 32px 0; +} + +.text-body-grey-no-margin div, .text-body-grey div { + color: #4b5563 !important; +} \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/global.scss b/libs/accounts/email-renderer/src/global.scss new file mode 100644 index 00000000000..9437457d1d5 --- /dev/null +++ b/libs/accounts/email-renderer/src/global.scss @@ -0,0 +1,197 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Fonts +$font-sans: sans-serif; + +// Colors +$blue-500: #0060df; +$white: #fff; +$black: #000; +$grey-50: #f0f0f4; +$grey-100: #e7e7e7; +$grey-400: #6d6d6e; +$grey-500: #4b5563; +$grey-600: #464646; +$purple-600: #7542e5; + +// Spacing values for margin and padding +$s-0: 0px; +$s-1: 4px; +$s-2: 8px; +$s-3: 12px; +$s-4: 16px; +$s-5: 20px; +$s-6: 24px; +$s-8: 32px; +$s-10: 40px; + +// Font-size and line-height +.text { + &-xs { + font-size: 12px !important; + line-height: 20px !important; + } + &-sm { + font-size: 14px !important; + line-height: 22px !important; + } + &-md { + font-size: 16px !important; + line-height: 24px !important; + } + &-lg { + font-size: 18px !important; + line-height: 26px !important; + } + &-xl { + font-size: 20px !important; + line-height: 28px !important; + } + &-2xl { + font-size: 22px !important; + line-height: 30px !important; + } + &-3xl { + font-size: 32px !important; + line-height: 36px !important; + } +} + +// Utility classes +.font-sans { + font-family: $font-sans !important; +} + +.text-blue-500 { + color: $blue-500; +} + +.mt { + &-2 { + margin-top: $s-2 !important; + } + &-4 { + margin-top: $s-4 !important; + } + &-5 { + margin-top: $s-5 !important; + } + &-6 { + margin-top: $s-6 !important; + } + &-8 { + margin-top: $s-8 !important; + } + &-10 { + margin-top: $s-10 !important; + } +} + +.mb { + &-0 { + margin-bottom: $s-0 !important; + } + &-2 { + margin-bottom: $s-2 !important; + } + &-3 { + margin-bottom: $s-3 !important; + } + &-4 { + margin-bottom: $s-4 !important; + } + &-5 { + margin-bottom: $s-5 !important; + } + &-6 { + margin-bottom: $s-6 !important; + } + &-8 { + margin-bottom: $s-8 !important; + } +} + +.px { + &-6 { + padding-left: $s-6 !important; + padding-right: $s-6 !important; + } +} + +.p { + &-4 { + padding: $s-4 !important; + } + &b-1 { + padding-bottom: $s-1 !important; + } + &b-2 { + padding-bottom: $s-2 !important; + } +} +// Global styles +tbody > td:first-child, +tr > td:first-child { + padding: $s-0 !important; +} + +.link-blue { + @extend .text-blue-500; + text-decoration: underline; + font-family: $font-sans; +} + +%text-header-common { + @extend .font-sans; + text-align: center !important; +} + +%text-body-common { + @extend .font-sans; +} + +%banner-common { + max-width: 640px !important; + width: 100% !important; +} + +%text-banner-common { + @extend %text-header-common; + @extend .text-xs; + @extend .p-4; + color: $black !important; + font-weight: 700 !important; +} + +%link-banner-common { + color: $black !important; + text-decoration: underline !important; +} +.align-left div { + text-align: left !important; +} + +.hidden { + display: none; + width: 0; + height: 0; + max-height: 0; + line-height: 0; + overflow: hidden; +} + +.text-footer div { + @extend .font-sans; + text-align: center !important; + color: $grey-400 !important; +} + +.text-footer-automatedEmail { + @extend .text-footer; + div { + @extend .mb-3; + @extend .mt-10; + } +} diff --git a/libs/accounts/email-renderer/src/index.spec.ts b/libs/accounts/email-renderer/src/index.spec.ts new file mode 100644 index 00000000000..69223a612ed --- /dev/null +++ b/libs/accounts/email-renderer/src/index.spec.ts @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { NodeRendererBindings } from './renderer/bindings-node'; +import { FxaEmailRenderer } from './renderer'; + +describe('emails', () => { + it('can render email', async () => { + const r = new FxaEmailRenderer(new NodeRendererBindings()); + const email = await r.renderAdminResetAccounts({ + status: [{ locator: 'foo@mozilla.com', status: 'Success' }], + }, + { + logoAltText: 'mock-logo-alt-text', + logoUrl: 'https://mozilla.org/mock-logo-url', + logoWidth: '100px', + privacyUrl: 'https://mozilla.org/mock-privacy-url', + sync: false, + } + ); + + expect(email).toBeDefined(); + + expect(email.subject).toEqual('Fxa Admin: Accounts Reset'); + expect(email.preview).toEqual(''); + + expect(email.text).toContain('Here\'s the account reset status'); + expect(email.text).toContain('foo@mozilla.com - Success'); + expect(email.text).toContain('Mozilla Accounts Privacy Notice'); + expect(email.text).toContain('https://mozilla.org/mock-privacy-url'); + + expect(email.html).toContain('Fxa Admin: Accounts Reset') + expect(email.html).toContain('Here\'s the account reset status'); + expect(email.html).toContain('foo@mozilla.com'); + expect(email.html).toContain('Success'); + expect(email.html).toContain('Mozilla Accounts Privacy Notice'); + expect(email.html).toContain('https://mozilla.org/mock-privacy-url'); + }); + + // TODO: Port over other tests, FXA-12579 +}); diff --git a/libs/accounts/email-renderer/src/index.ts b/libs/accounts/email-renderer/src/index.ts new file mode 100644 index 00000000000..add301d2d30 --- /dev/null +++ b/libs/accounts/email-renderer/src/index.ts @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export * from './renderer'; +export * from './renderer/bindings-node'; +export * from './bindings'; +export * from './templates'; diff --git a/libs/accounts/email-renderer/src/l10n.ts b/libs/accounts/email-renderer/src/l10n.ts new file mode 100644 index 00000000000..d4db0ef9935 --- /dev/null +++ b/libs/accounts/email-renderer/src/l10n.ts @@ -0,0 +1,169 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + import { DOMLocalization, Localization } from '@fluent/dom'; + import { FluentBundle, FluentResource } from '@fluent/bundle'; + import { determineLocale, parseAcceptLanguage } from '@fxa/shared/l10n'; + + export type LocalizerOpts = { + translations: { + basePath: string; + }; + templates: { + cssPath: string; + }; +}; + +export interface ILocalizerBindings { + opts: LocalizerOpts; + fetchResource(path: string): Promise; + renderEjs(ejsTemplate: string, context: any): string; + renderTemplate(template: string, context: any, layout?: string, target?: ComponentTarget): Promise<{ text:string, rootElement: Element }>; +} + +export type ComponentTarget = 'index' | 'strapi'; + +export interface ILocalizerBindings { + opts: LocalizerOpts; + fetchResource(path: string): Promise; + renderEjs(ejsTemplate: string, context: any): string; + renderTemplate(template: string, context: any, layout?: string, target?: ComponentTarget): Promise<{ text:string, rootElement: Element }>; +} + + /** + * Represents a Fluent (FTL) message + * @param id - unique identifier for the message + * @param message - a fallback message in case the localized string cannot be found + * @param vars - optional arguments to be interpolated into the localized string + */ + export interface FtlIdMsg { + id: string; + message: string; + vars?: Record; + } + + interface LocalizedStrings { + [id: string]: string; + } + + class Localizer { + protected readonly bindings: ILocalizerBindings; + + constructor(bindings: ILocalizerBindings) { + this.bindings = bindings; + } + + protected async fetchMessages(currentLocales: string[]) { + const fetchedPending: Record> = {}; + const fetched: Record = {}; + for (const locale of currentLocales) { + fetchedPending[locale] = this.fetchTranslatedMessages(locale); + } + + // All we're doing here is taking `{ localeName: pendingLocaleMessagesPromise }` objects and + // parallelizing the promise resolutions instead of waiting for them to finish syncronously. We + // then return the result in the same `{ localeName: messages }` format for fulfilled promises. + const fetchedLocales = await Promise.allSettled( + Object.keys(fetchedPending).map(async (locale) => ({ + locale, + fetchedLocale: await fetchedPending[locale], + })) + ); + + fetchedLocales.forEach((fetchedLocale) => { + if (fetchedLocale.status === 'fulfilled') { + fetched[fetchedLocale.value.locale] = fetchedLocale.value.fetchedLocale; + } + }); + return fetched; + } + + protected createBundleGenerator(fetched: Record) { + async function* generateBundles(currentLocales: string[]) { + for (const locale of currentLocales) { + const source = fetched[locale]; + if (source) { + const bundle = new FluentBundle(locale, { + useIsolating: false, + }); + const resource = new FluentResource(source); + bundle.addResource(resource); + yield bundle; + } + } + } + + return generateBundles; + } + + async getLocalizerDeps(acceptLanguage?: string) { + const currentLocales = parseAcceptLanguage(acceptLanguage || ''); + const selectedLocale = determineLocale(acceptLanguage || ''); + const messages = await this.fetchMessages(currentLocales); + const generateBundles = this.createBundleGenerator(messages); + return { currentLocales, messages, generateBundles, selectedLocale }; + } + + async setupDomLocalizer(acceptLanguage?: string) { + const { currentLocales, generateBundles, selectedLocale } = + await this.getLocalizerDeps(acceptLanguage); + const l10n = new DOMLocalization(currentLocales, generateBundles); + return { l10n, selectedLocale }; + } + + async setupLocalizer(acceptLanguage?: string) { + const { currentLocales, generateBundles, selectedLocale } = + await this.getLocalizerDeps(acceptLanguage); + const l10n = new Localization(currentLocales, generateBundles); + return { l10n, selectedLocale }; + } + + /** + * Returns the set of translated strings for the specified locale. + * @param locale Locale to use, defaults to en. + */ + protected async fetchTranslatedMessages(locale?: string) { + const results: string[] = []; + + // Note: 'en' auth.ftl only exists for browser bindings / Storybook. The fallback + // English strings within the templates are tested and are shown in other envs + const authPath = `${this.bindings.opts.translations.basePath}/${ + locale || 'en' + }/auth.ftl`; + results.push(await this.bindings.fetchResource(authPath)); + + const brandingPath = `${this.bindings.opts.translations.basePath}/${ + locale || 'en' + }/branding.ftl`; + results.push(await this.bindings.fetchResource(brandingPath)); + + return results.join('\n\n\n'); + } + + async localizeStrings( + acceptLanguage = 'en', + ftlIdMsgs: FtlIdMsg[] + ): Promise { + const { l10n } = await this.setupLocalizer(acceptLanguage); + + const localizedFtlIdMsgs = await Promise.all( + ftlIdMsgs.map(async (ftlIdMsg) => { + const { id, message, vars } = ftlIdMsg; + let localizedMessage; + try { + localizedMessage = (await l10n.formatValue(id, vars)) || message; + } catch (e) { + localizedMessage = message; + } + return Promise.resolve({ + [id]: localizedMessage, + }); + }) + ); + + return Object.assign({}, ...localizedFtlIdMsgs); + } + } + + export default Localizer; diff --git a/libs/accounts/email-renderer/src/layouts/fxa/en.ftl b/libs/accounts/email-renderer/src/layouts/fxa/en.ftl new file mode 100644 index 00000000000..5d3c1ee6a22 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/en.ftl @@ -0,0 +1,11 @@ + +## Email content +## Emails do not contain buttons, only links. Emails have a rich HTML version and a plaintext +## version. The strings are usually identical but sometimes they differ slightly. + +fxa-header-mozilla-logo = { -brand-mozilla } logo +fxa-header-sync-devices-image = Sync devices +body-devices-image = Devices +fxa-privacy-url = { -brand-mozilla } Privacy Policy +moz-accounts-privacy-url-2 = { -product-mozilla-accounts(capitalization:"uppercase") } Privacy Notice +moz-accounts-terms-url = { -product-mozilla-accounts(capitalization:"uppercase") } Terms of Service diff --git a/libs/accounts/email-renderer/src/layouts/fxa/index.mjml b/libs/accounts/email-renderer/src/layouts/fxa/index.mjml new file mode 100644 index 00000000000..f23b91ce824 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/index.mjml @@ -0,0 +1,66 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + + + <%- locals.subject %> + <% if (locals.preview) { %> + <%= locals.preview %> + <% } %> + + <%- include('/partials/images.mjml') %> + <%- include('/partials/metadata.mjml') %> + + + + + + <%- include('/partials/brandMessaging/index.mjml') %> + <% if (locals.showBannerWarning === true) { %> + <%- include('/partials/bannerWarning/index.mjml') %> + <% } %> + + + + + <% if (!locals.sync) { %> + + + <% } else { %> + + + <% } %> + + + + <%- body %> + + + + + Mozilla. 149 New Montgomery St, 4th Floor, San Francisco, CA 94105 + + Mozilla Accounts Privacy Notice + + + Mozilla Accounts Terms of Service + + + + + + + diff --git a/libs/accounts/email-renderer/src/layouts/fxa/index.scss b/libs/accounts/email-renderer/src/layouts/fxa/index.scss new file mode 100644 index 00000000000..d4b27da1bd7 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/index.scss @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@use '../../global.scss'; + +.body { + max-width: 310px !important; + margin: global.$s-0 auto !important; +} + +.mozilla-logo { + padding: global.$s-10 global.$s-0 !important; + margin: 0 auto !important; + display: block !important; + align-items: center; +} + +.sync-img { + padding: global.$s-5 global.$s-0 !important; + height: 137px; + margin: 0 auto !important; + display: block !important; +} + +.text-header div { + @extend %text-header-common; + @extend .text-xl; + @extend .mb-3; +} + +%text-body-common { + @extend %text-body-common; + @extend .text-sm; + text-align: center !important; +} + +.text-body div { + @extend %text-body-common; + @extend .mb-5; +} + +.text-body-subtext div { + @extend .text-xs; + @extend .mt-5; + @extend .mb-5; + text-align: center !important; +} + +.text-body-no-margin div { + @extend %text-body-common; +} + +.text-body-top-margin div { + @extend %text-body-common; + @extend .mt-5; +} + +.text-body-lg-bottom-margin div { + @extend %text-body-common; + @extend .mb-8; +} + +.text-sub-body div { + @extend %text-body-common; + @extend .mb-3; +} + +.text-footer div { + @extend .text-xs; +} + +.text-footer-automatedEmail { + @extend .text-footer; +} + +.sync-logo img { + margin: 0 auto; + @extend .mb-5; + @extend .mt-2; +} + +.graphic-devices img { + margin: 0 auto; + @extend .mb-5; + @extend .mt-2; +} + +%code-common { + text-align: center !important; + font-family: monospace; + @extend .mb-6; +} + +.code { + &-medium div { + @extend %code-common; + @extend .text-lg; + } + + &-large div { + @extend %code-common; + @extend .text-3xl; + } +} + +.info-block { + margin: 28px 0 0 0; + background-color: global.$grey-50; + border-radius: 10px; +} + +.info-block div { + padding: 20px 32px 0; +} diff --git a/libs/accounts/email-renderer/src/layouts/fxa/index.stories.ts b/libs/accounts/email-renderer/src/layouts/fxa/index.stories.ts new file mode 100644 index 00000000000..216e9fa3697 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/index.stories.ts @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes } from './mocks'; + +export default { + title: 'FxA Emails/Layout', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'The FxA email base layout.', + { + sync: false, + subject: 'N/A', + }, + includes +); + +export const NotThroughSyncFlow = createStory( + {}, + 'Email not triggered through sync flow' +); + +export const ThroughSyncFlow = createStory( + { + sync: true, + }, + 'Email triggered through sync flow' +); + +export const MessagingNotThroughSyncFlowWithBrandMessaging = createStory( + { + brandMessagingMode: 'postlaunch', + }, + 'Email not triggered through sync flow with brand messaging' +); + +export const MessagingThroughSyncFlowWithBrandMessaging = createStory( + { + brandMessagingMode: 'postlaunch', + }, + 'Email triggered through sync flow with brand messaging' +); + +export const FlowWithWarning = createStory( + { + showBannerWarning: true, + }, + 'Email triggered through web or AMO flow with banner warning' +); diff --git a/libs/accounts/email-renderer/src/layouts/fxa/index.ts b/libs/accounts/email-renderer/src/layouts/fxa/index.ts new file mode 100644 index 00000000000..99caa16fce4 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/index.ts @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + + /** An optional logo url override. This will default to the mozilla logo if not provided. */ + logoUrl?: string; + + /** An optional logo alt text. This will default to 'Mozilla logo' if not provided. */ + logoAltText?: string; + + /** An optional logo width. This will dfault to 120px if not provided. */ + logoWidth?: string; + + /** The current privacy url. */ + privacyUrl: string; + + /** Whether or not this is a 'sync' specific email. These emails have a slightly different styling */ + sync: boolean; +}; diff --git a/libs/accounts/email-renderer/src/layouts/fxa/index.txt b/libs/accounts/email-renderer/src/layouts/fxa/index.txt new file mode 100644 index 00000000000..367840c3a9f --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/index.txt @@ -0,0 +1,14 @@ +<%- include('/partials/brandMessaging/index.txt') %> + +<% if (locals.showBannerWarning === true) { %> +<%- include('/partials/bannerWarning/index.txt', { accountsEmail:"accounts@firefox.com" }) %> +<% } %> + +<%- body %> +Mozilla. 149 New Montgomery St, 4th Floor, San Francisco, CA 94105 + +moz-accounts-privacy-url-2 = "Mozilla Accounts Privacy Notice" +<%- privacyUrl %> + +moz-accounts-terms-url = "Mozilla Accounts Terms of Service" +https://www.mozilla.org/about/legal/terms/services/ diff --git a/libs/accounts/email-renderer/src/layouts/fxa/mocks.ts b/libs/accounts/email-renderer/src/layouts/fxa/mocks.ts new file mode 100644 index 00000000000..e9b7e415600 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/mocks.ts @@ -0,0 +1,6 @@ +export const includes = { + subject: { + id: 'mock-fxa-layout-subject', + message: 'Mock Fxa Layout Subject', + }, +}; diff --git a/libs/accounts/email-renderer/src/layouts/fxa/strapi.mjml b/libs/accounts/email-renderer/src/layouts/fxa/strapi.mjml new file mode 100644 index 00000000000..ce5dc4c3a54 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/strapi.mjml @@ -0,0 +1,59 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + + + <%- locals.subject %> + + <%- include('/partials/images.mjml') %> + <%- include('/partials/metadata.mjml') %> + + + + + + + + + + <% if (locals.logoUrl) { %> + + + <% } else { %> + + + <% } %> + + + + <%- body %> + + + + + Mozilla. 149 New Montgomery St, 4th Floor, San Francisco, CA 94105 + + Mozilla Accounts Privacy Notice + + + Mozilla Accounts Terms of Service + + + + + + + diff --git a/libs/accounts/email-renderer/src/layouts/fxa/strapi.stories.ts b/libs/accounts/email-renderer/src/layouts/fxa/strapi.stories.ts new file mode 100644 index 00000000000..a1d335fba2e --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/strapi.stories.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes } from './mocks'; + +export default { + title: 'FxA Emails/Layout', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'The Strapi email layout.', + { + sync: false, + subject: 'N/A', + logoUrl: + 'https://accounts-cdn.stage.mozaws.net/product-icons/monitor-logo-email.png', + logoAltText: 'Monitor logo', + logoWidth: '280px', + }, + includes, + 'fxa', + 'strapi' +); + +export const CMS = createStory({}, 'CMS customized email'); diff --git a/libs/accounts/email-renderer/src/layouts/fxa/strapi.txt b/libs/accounts/email-renderer/src/layouts/fxa/strapi.txt new file mode 100644 index 00000000000..004c5a42634 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/fxa/strapi.txt @@ -0,0 +1,10 @@ + + +<%- body %> +Mozilla. 149 New Montgomery St, 4th Floor, San Francisco, CA 94105 + +moz-accounts-privacy-url-2 = "Mozilla Accounts Privacy Notice" +<%- privacyUrl %> + +moz-accounts-terms-url = "Mozilla Accounts Terms of Service" +https://www.mozilla.org/about/legal/terms/services/ diff --git a/libs/accounts/email-renderer/src/layouts/subscription/en.ftl b/libs/accounts/email-renderer/src/layouts/subscription/en.ftl new file mode 100644 index 00000000000..c017031ea14 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/subscription/en.ftl @@ -0,0 +1,36 @@ +subplat-header-mozilla-logo-2 = { -brand-mozilla } logo +subplat-footer-mozilla-logo-2 = { -brand-mozilla } logo +subplat-automated-email = This is an automated email; if you received it in error, no action is required. +subplat-privacy-notice = Privacy notice +subplat-privacy-plaintext = Privacy notice: +subplat-update-billing-plaintext = { subplat-update-billing }: +# Variables: +# $email (String) - A user's primary email address +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subplat-explainer-specific-2 = You’re receiving this email because { $email } has a { -product-mozilla-account } and you signed up for { $productName }. +# Variables: +# $email (String) - A user's primary email address +subplat-explainer-reminder-form-2 = You’re receiving this email because { $email } has a { -product-mozilla-account }. +subplat-explainer-multiple-2 = You’re receiving this email because { $email } has a { -product-mozilla-account } and you have subscribed to multiple products. +subplat-explainer-was-deleted-2 = You’re receiving this email because { $email } was registered for a { -product-mozilla-account }. +subplat-manage-account-2 = Manage your { -product-mozilla-account } settings by visiting your account page. +# Variables: +# $accountSettingsUrl (String) - URL to Account Settings +subplat-manage-account-plaintext-2 = Manage your { -product-mozilla-account } settings by visiting your account page: { $accountSettingsUrl } +subplat-terms-policy = Terms and cancellation policy +subplat-terms-policy-plaintext = { subplat-terms-policy }: +subplat-cancel = Cancel subscription +subplat-cancel-plaintext = { subplat-cancel }: +subplat-reactivate = Reactivate subscription +subplat-reactivate-plaintext = { subplat-reactivate }: +subplat-update-billing = Update billing information +subplat-privacy-policy = { -brand-mozilla } Privacy Policy +subplat-privacy-policy-2 = { -product-mozilla-accounts(capitalization:"uppercase") } Privacy Notice +subplat-privacy-policy-plaintext = { subplat-privacy-policy }: +subplat-privacy-policy-plaintext-2 = { subplat-privacy-policy-2 }: +subplat-moz-terms = { -product-mozilla-accounts(capitalization:"uppercase") } Terms of Service +subplat-moz-terms-plaintext = { subplat-moz-terms }: +subplat-legal = Legal +subplat-legal-plaintext = { subplat-legal }: +subplat-privacy = Privacy +subplat-privacy-website-plaintext = { subplat-privacy }: diff --git a/libs/accounts/email-renderer/src/layouts/subscription/index.mjml b/libs/accounts/email-renderer/src/layouts/subscription/index.mjml new file mode 100644 index 00000000000..66fd7802f75 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/subscription/index.mjml @@ -0,0 +1,116 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + + <%- locals.subject %> + <%- locals.preview %> + <%- include('/partials/images.mjml') %> + <%- include('/partials/metadata.mjml') %> + + + + + + + <%- include('/partials/brandMessaging/index.mjml') %> + + + + + + + + + + <%- body %> + + + + + <% if (locals.productName) { %> + + You’re receiving this email because <%- email %> has a Mozilla account and you signed up for <%- productName %>. + + <% } else if (locals.reminderShortForm) { %> + + You’re receiving this email because <%- email %> has a Mozilla account. + + <% } else if (locals.wasDeleted) { %> + + You’re receiving this email because <%- email %> was registered for a Mozilla account. + + <% } else { %> + + You’re receiving this email because <%- email %> has a Mozilla account and you have subscribed to multiple products. + + <% } %> + + + <% if (!locals.reminderShortForm && !locals.wasDeleted) { %> + + + Manage your Mozilla account settings by visiting your account page. + + + + + <% if (locals.productName || locals.subscriptions?.length > 0) { %> + Terms and cancellation policy +   •   + + Privacy notice + <% } %> + + <% if (!locals.isFinishSetup) { %> +   •   + <% if (locals.isCancellationEmail) { %> + Reactivate subscription +   •   + <% } else { %> + Cancel subscription +   •   + <% } %> + + Update billing information + <% } %> + + <% } else { %> + + Mozilla Accounts Privacy Notice +   •   + + Mozilla Accounts Terms of Service + + <% } %> + + + + + 149 New Montgomery St, 4th Floor, San Francisco, CA 94105 + + + Legal +   •   + Privacy + + + + + + diff --git a/libs/accounts/email-renderer/src/layouts/subscription/index.scss b/libs/accounts/email-renderer/src/layouts/subscription/index.scss new file mode 100644 index 00000000000..41a2fc3993e --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/subscription/index.scss @@ -0,0 +1,169 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@use '../../global.scss'; + +.body { + max-width: 640px !important; + margin: global.$s-0 auto !important; +} + +.text-header div { + @extend %text-header-common; + @extend .text-2xl; + @extend .mb-4; + font-weight: 700; + color: global.$grey-600 !important; +} + +%text-body-common { + @extend %text-body-common; + @extend .text-md; + color: global.$grey-500 !important; +} + +.text-body div { + @extend %text-body-common; + @extend .mb-6; +} + +.text-body-mb-4 div { + @extend %text-body-common; + @extend .mb-4; +} + +.text-title { + @extend .font-sans; + font-size: 18px !important; + font-weight: 600 !important; + margin-top: 48px !important; +} + +.text-title-table div { + @extend .text-title; + @extend .mb-4; + color: global.$grey-500 !important; +} + +.text-title-link div { + @extend .text-title; +} + +.text-body-top-margin div { + @extend %text-body-common; + @extend .mt-4; + color: global.$grey-500 !important; + padding: global.$s-1 0; +} + +.text-link { + @extend %text-body-common; + @extend .mt-2; + color: global.$blue-500 !important; + text-decoration: underline !important; +} + +.text-body-link div { + @extend .text-link; +} + +.text-body-bottom-link div { + @extend .text-link; + @extend .mb-6; +} + +// NOTE: appeared purple in at least one instance/email client in stage +.text-body li { + color: global.$grey-500 !important; +} + +.text-body-no-bottom-margin div { + @extend %text-body-common; + @extend .mb-0; +} + +.text-body-table-no-bottom-margin td { + @extend %text-body-common; + @extend .mb-0; + padding: global.$s-1 0; +} + +.header-container { + max-width: 640px !important; + @extend .mb-6; +} + +.subplat-mozilla-logo { + padding: global.$s-5 global.$s-0 !important; + display: block !important; + + // overrides the 13px inline font-size default for MJML images + // see https://github.com/mozilla/fxa/pull/12956#discussion_r878556004 + a, + img { + font-size: global.$s-3 !important; + } +} + +.mozilla-logo-footer { + padding: global.$s-8 global.$s-0 !important; + height: 34px !important; + display: block !important; + margin: 0 auto !important; +} + +.footer-container { + background-color: global.$black; + padding: global.$s-10 global.$s-6 !important; + @extend .mt-6; +} + +.footer-text div { + @extend .text-sm; + @extend .font-sans; + color: global.$white !important; + text-align: center !important; +} + +.footer-text-bottom-margin { + @extend .footer-text; + + div { + @extend .mb-3; + } +} + +.footer-link { + color: global.$white !important; +} + +.primary-button-subplat { + height: 56px !important; + max-width: 310px !important; + + a { + @extend .font-sans; + @extend .text-lg; + background: global.$blue-500 !important; + max-width: 310px !important; + color: global.$white; + display: unset !important; + padding: global.$s-3 20px !important; + border-radius: 6px !important; + } +} + +.product-icon { + display: block; + margin: 0 auto; + height: 58px; +} + +.text-footer div { + @extend .text-sm; +} + +.text-footer-automatedEmail { + @extend .text-footer; +} diff --git a/libs/accounts/email-renderer/src/layouts/subscription/index.stories.ts b/libs/accounts/email-renderer/src/layouts/subscription/index.stories.ts new file mode 100644 index 00000000000..37e95b63f42 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/subscription/index.stories.ts @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { includes } from './mocks'; +import { subplatStoryWithProps } from '../../storybook-email'; + +const createStory = subplatStoryWithProps( + '_storybook', + 'The Subscription Platform email base layout.', + { + subject: 'N/A', + brandMessagingMode: 'none', + }, + includes +); + +export const LayoutNoProduct = createStory( + { + reminderShortForm: true, + }, + 'Reminder short form - no specified product' +); + +export const LayoutNoProductWithBrandMessaging = createStory( + { + reminderShortForm: true, + brandMessagingMode: 'postlaunch', + }, + 'Reminder short form - no specified product - with brand messaging' +); + +export const LayoutMultipleProducts = createStory( + { + subscriptions: [ + { + productName: 'Firefox Fortress', + }, + { + productName: 'Mozilla VPN', + }, + ], + }, + 'Multiple products - No brand messaging' +); + +export const LayoutMultipleProductsWithBrandMessaging = createStory( + { + subscriptions: [ + { + productName: 'Firefox Fortress', + }, + { + productName: 'Mozilla VPN', + }, + ], + brandMessagingMode: 'postlaunch', + }, + 'Multiple products - With brand messaging' +); + +export const LayoutWithProduct = createStory( + { + productName: 'Mozilla VPN', + }, + 'Specific product' +); + +export const LayoutWithProductCancellation = createStory( + { + productName: 'Mozilla VPN', + isCancellationEmail: true, + }, + 'Cancellation email' +); + +export const LayoutWithWasDeleted = createStory( + { + wasDeleted: true, + }, + 'Fraudulent account deletion' +); + +export default { + title: 'SubPlat Emails/Layout', + component: LayoutNoProduct, +} as Meta; diff --git a/libs/accounts/email-renderer/src/layouts/subscription/index.ts b/libs/accounts/email-renderer/src/layouts/subscription/index.ts new file mode 100644 index 00000000000..f85b801467b --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/subscription/index.ts @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + email: string; + subscriptionTermsUrl: string; + subscriptionPrivacyUrl: string; + cancelSubscriptionUrl: string; + updateBillingUrl: string; + reactivateSubscriptionUrl: string; + accountSettingsUrl: string; + cancellationSurveyUrl: string; + mozillaSupportUrl: string; +}; diff --git a/libs/accounts/email-renderer/src/layouts/subscription/index.txt b/libs/accounts/email-renderer/src/layouts/subscription/index.txt new file mode 100644 index 00000000000..c5c850618ca --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/subscription/index.txt @@ -0,0 +1,61 @@ +<% if (locals.brandMessagingMode == 'postlaunch') { %> +brand-banner-message = "Did you know we changed our name from Firefox accounts to Mozilla accounts? Learn more" + +https://support.mozilla.org/kb/firefox-accounts-renamed-mozilla-accounts + +<% } %> + +<%- body %> + +<% if (!locals.wasDeleted) { %> +subplat-automated-email = "This is an automated email; if you received it in error, no action is required." +<% } %> + +<% if (locals.productName) { %> +subplat-explainer-specific-2 = "You’re receiving this email because <%- email %> has a Mozilla account and you signed up for <%- productName %>." +<% } else if (locals.reminderShortForm) { %> +subplat-explainer-reminder-form-2 = "You’re receiving this email because <%- email %> has a Mozilla account." +<% } else if (locals.wasDeleted) { %> +subplat-explainer-was-deleted-2 = "You’re receiving this email because <%- email %> was registered for a Mozilla account." +<% } else { %> +subplat-explainer-multiple-2 = "You’re receiving this email because <%- email %> has a Mozilla account and you have subscribed to multiple products." +<% } %> + +<% if (!locals.reminderShortForm && !locals.wasDeleted) { %> +subplat-manage-account-plaintext-2 = "Manage your Mozilla account settings by visiting your account: <%- accountSettingsUrl %>" +<% if (locals.productName || locals.subscriptions?.length > 0) { %> +subplat-terms-policy-plaintext = "Terms and cancellation policy:" +<%- subscriptionTermsUrl %> + +subplat-privacy-plaintext = "Privacy notice:" +<%- subscriptionPrivacyUrl %> +<% } %> + +<% if (!locals.isFinishSetup && !locals.wasDeleted) { %> +<% if (locals.isCancellationEmail) { %> +subplat-reactivate-plaintext = "Reactivate subscription:" +<%- reactivateSubscriptionUrl %> +<% } else { %> +subplat-cancel-plaintext = "Cancel subscription:" +<%- cancelSubscriptionUrl %> +<% } %> + +subplat-update-billing-plaintext = "Update billing information:" +<%- updateBillingUrl %> +<% } %> +<% } else { %> +subplat-privacy-policy-plaintext-2 = "Mozilla Accounts Privacy Notice:" +<%- privacyUrl %> + +subplat-moz-terms-plaintext = "Mozilla Accounts Terms Of Service:" +<%- subscriptionTermsUrl %> +<% } %> + +Mozilla Corporation +149 New Montgomery St, 4th Floor, San Francisco, CA 94105 + +subplat-legal-plaintext = "Legal:" +https://www.mozilla.org/about/legal/terms/services/ + +subplat-privacy-website-plaintext = "Privacy:" +https://www.mozilla.org/privacy/websites/ diff --git a/libs/accounts/email-renderer/src/layouts/subscription/mocks.ts b/libs/accounts/email-renderer/src/layouts/subscription/mocks.ts new file mode 100644 index 00000000000..1c1def8c178 --- /dev/null +++ b/libs/accounts/email-renderer/src/layouts/subscription/mocks.ts @@ -0,0 +1,6 @@ +export const includes = { + subject: { + id: 'mock-subscriptions-layout-subject', + message: 'Mock Subscriptions Layout Subject', + }, +}; diff --git a/libs/accounts/email-renderer/src/locale-dir.scss b/libs/accounts/email-renderer/src/locale-dir.scss new file mode 100644 index 00000000000..53850d17965 --- /dev/null +++ b/libs/accounts/email-renderer/src/locale-dir.scss @@ -0,0 +1,17 @@ +.ltr { + div { + direction: ltr !important; + } + td { + direction: ltr !important; + } +} + +.rtl { + div { + direction: rtl !important; + } + td { + direction: rtl !important; + } +} diff --git a/libs/accounts/email-renderer/src/mjml-browser-helper.ts b/libs/accounts/email-renderer/src/mjml-browser-helper.ts new file mode 100644 index 00000000000..8e2f62aa0a8 --- /dev/null +++ b/libs/accounts/email-renderer/src/mjml-browser-helper.ts @@ -0,0 +1,114 @@ +type MjIncludeTag = { path?: string; inline: boolean; type?: string }; + +/** + * Important! At the current momement, mjml-browser does not support . + * See: https://github.com/mjmlio/mjml/tree/master/packages/mjml-browser#unavailable-features + * + * Until this is supported, we will convert mj-incude tags into mj-style tags with + * ejs includes. The allows parity between mjml and mjml-browser. + * @param mjml mjml document + * @returns mjml document that can be processed by mjml-browser + */ +export function transformMjIncludeTags(mjml: string): string { + // Must be mjml document + const hasOpeningMjmlTag = //.test(mjml); + if (!hasOpeningMjmlTag) { + throw new Error('Missing tag'); + } else if (!hasClosingMjmlTag) { + throw new Error('Missing tag'); + } + + // tags must go in header. Create one if possible, + // otherwise error out. + const hasOpeningMjHeadTag = //.test(mjml); + const hasClosingMjHeadTag = /<\/mj-head>/.test(mjml); + if (!hasOpeningMjHeadTag && !hasClosingMjHeadTag) { + mjml = mjml.replace('', ` `); + } else if (!hasOpeningMjHeadTag) { + throw new Error('Missing tag'); + } else if (!hasClosingMjHeadTag) { + throw new Error('Missing tag'); + } + + // Parse mjml and build style statements using ejs includes + const includes = extractMjIncludeTags(mjml); + + // Append includes to end of header + if (includes.length) { + // Update the header tag, appending the includes + mjml = mjml.replace( + /<\/mj-head>/, + `${includes.map((x) => toMjStyle(x)).join('')}` + ); + } + return mjml; +} + +function extractMjIncludeTags(mjml: string): MjIncludeTag[] { + let chomp = false; + let include = ''; + const includes = new Array(); + mjml + .replace(/ { + if (chomp && / tag'); + } + + // Keep adding text while, chomping + if (chomp) { + include += x; + } + // Find start tag and begin chomping + else if (//.test(include)) { + chomp = false; + } + + // If done chomping and include tag, parse it + if (include && !chomp) { + includes.push(parseMjIncludeTag(include)); + include = ''; + } + }); + + // Inidcates /> is missing + if (chomp) { + throw new Error('Malformed tag'); + } + + return includes; +} + +function parseMjIncludeTag(include: string): MjIncludeTag { + const res = { + path: /path=("[^"]*|'[^']*)/g.exec(include)?.[1]?.substring(1), + inline: /css-inline=("inline"|'inline')/g.test(include), + type: /type=("[^"]*|'[^']*)/g.exec(include)?.[1]?.substring(1), + }; + + // Convert relative paths. The requests will now be made to the root + // of the webserver. + res.path = res.path?.replace(/\.\//, '/'); + + return res; +} + +function toMjStyle(tag: MjIncludeTag) { + const { inline, path, type } = tag; + + if (type !== 'css') return ''; + if (!path) return ''; + + if (inline) { + return ` <%- include('${path}') %> `; + } + return ` <%- include('${path}') %> `; +} diff --git a/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/en.ftl b/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/en.ftl new file mode 100644 index 00000000000..ddcb2e7253d --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/en.ftl @@ -0,0 +1,4 @@ +account-deletion-info-block-communications = If your account is deleted, you’ll still receive emails from Mozilla Corporation and Mozilla Foundation, unless you ask to unsubscribe. +account-deletion-info-block-support = If you have any questions or need assistance, feel free to contact our support team. +account-deletion-info-block-communications-plaintext = If your account is deleted, you’ll still receive emails from Mozilla Corporation and Mozilla Foundation, unless you ask to unsubscribe: +account-deletion-info-block-support-plaintext = If you have any questions or need assistance, feel free to contact our support team: diff --git a/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.mjml b/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.mjml new file mode 100644 index 00000000000..5bafb574f5e --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.mjml @@ -0,0 +1,20 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + If your account is deleted, you’ll still receive emails from Mozilla Corporation and Mozilla Foundation, unless you ask to unsubscribe. + + + <%# css-class is not supported for mj-divider and styles must be applied inline. Border-color is $grey-100 %> + + + + If you have any questions or need assistance, feel free to contact our support team. + + + + diff --git a/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.stories.ts b/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.stories.ts new file mode 100644 index 00000000000..45eb51a34c6 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.stories.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/accountDeletionInfoBlock', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'This partial is used in account deletion emails to provide information and assistance about the deletion process.', + { + layout: null, + subject: 'N/A', + partial: 'accountDeletionInfoBlock', + }, + includes +); + +export const accountDeletionInfoBlock = createStory(); diff --git a/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.txt b/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.txt new file mode 100644 index 00000000000..aa17a2fb6e1 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/accountDeletionInfoBlock/index.txt @@ -0,0 +1,5 @@ +account-deletion-info-block-communications-plaintext = "If your account is deleted, you’ll still receive emails from Mozilla Corporation and Mozilla Foundation, unless you ask to unsubscribe:" +<%- unsubscribeUrl %> + +account-deletion-info-block-support-plaintext = "If you have any questions or need assistance, feel free to contact our support team:" +<%- supportUrl %> diff --git a/libs/accounts/email-renderer/src/partials/appBadges/en.ftl b/libs/accounts/email-renderer/src/partials/appBadges/en.ftl new file mode 100644 index 00000000000..a69dfa1f7ec --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/appBadges/en.ftl @@ -0,0 +1,21 @@ +# Variables: +# $productName (String) - The name of the product to be downloaded, e.g. Mozilla VPN, or Firefox +body-android-badge = Download { $productName } on { -google-play } +# Variables: +# $productName (String) - The name of the product to be downloaded, e.g. Mozilla VPN, or Firefox +body-ios-badge = Download { $productName } on the { -app-store } +# Variables: +# $productName (String) - The name of the product to be downloaded, e.g. Mozilla VPN, or Firefox +another-desktop-device-2 = Install { $productName } on another desktop device. +# Variables: +# $productName (String) - The name of the product to be downloaded, e.g. Mozilla VPN, or Firefox +another-device-2 = Install { $productName } on another device. +# Variables: +# $productName (String) - The name of the product to be downloaded, e.g. Mozilla VPN, or Firefox +android-download-plaintext = Get { $productName } on Google Play: +# Variables: +# $productName (String) - The name of the product to be downloaded, e.g. Mozilla VPN, or Firefox +ios-download-plaintext = Download { $productName } on the App Store: +# Variables: +# $productName (String) - The name of the product to be downloaded, e.g. Mozilla VPN, or Firefox +another-device-plaintext = Install { $productName } on another device: diff --git a/libs/accounts/email-renderer/src/partials/appBadges/index.mjml b/libs/accounts/email-renderer/src/partials/appBadges/index.mjml new file mode 100644 index 00000000000..296348efd1d --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/appBadges/index.mjml @@ -0,0 +1,55 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + +<% locals.iosUrl = locals.iosUrl || locals.appStoreLink %> +<% locals.androidUrl = locals.androidUrl || locals.playStoreLink %> + + + + + <% if (locals.iosUrl) { %> + + + + + <% } %> + <% if (locals.androidUrl) { %> + + + + + <% } %> + + + + +<% if (!locals.hideDeviceLink) { %> + + + <% if (locals.onDesktopOrTabletDevice) { %> + Install <%- locals.productName %> on + another desktop device + + <% } else { %> + Install <%- locals.productName %> + on another device + + <% } %> + + +<% } %> diff --git a/libs/accounts/email-renderer/src/partials/appBadges/index.scss b/libs/accounts/email-renderer/src/partials/appBadges/index.scss new file mode 100644 index 00000000000..623f009e80f --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/appBadges/index.scss @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@use '../../global.scss'; + +.app-badges { + max-width: 310px !important; + text-align: center !important; +} + +.app-badge { + padding: global.$s-10 global.$s-0 global.$s-5 !important; + + img { + height: 44px !important; + margin: 0 auto !important; + } +} + +.text-footer-appBadges { + @extend .text-footer; +} diff --git a/libs/accounts/email-renderer/src/partials/appBadges/index.txt b/libs/accounts/email-renderer/src/partials/appBadges/index.txt new file mode 100644 index 00000000000..291175bd6a4 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/appBadges/index.txt @@ -0,0 +1,8 @@ +ios-download-plaintext = "Download <%- productName %> on the App Store:" +<%- locals.iosUrl %> + +android-download-plaintext = "Get <%- productName %> on Google Play:" +<%- locals.androidUrl %> + +another-device-plaintext = "Install <%- productName %> on another device:" +<%- locals.link %> diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/en.ftl b/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/en.ftl new file mode 100644 index 00000000000..e16d4eaac7d --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/en.ftl @@ -0,0 +1,6 @@ +automated-email-change-2 = If you didn’t take this action, change your password right away. +automated-email-support = For more info, visit { -brand-mozilla } Support. +# After the colon, there's a link to https://accounts.firefox.com/settings/change_password +automated-email-change-plaintext-2 = If you didn’t take this action, change your password right away: +# After the colon, there's a link to https://support.mozilla.org/kb/im-having-problems-my-firefox-account +automated-email-support-plaintext = For more info, visit { -brand-mozilla } Support: diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.mjml b/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.mjml new file mode 100644 index 00000000000..09e11506c16 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.mjml @@ -0,0 +1,16 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + If you didn’t take this action, change your password right away. + + + For more info, visit Mozilla Support. + + + + diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.stories.ts b/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.stories.ts new file mode 100644 index 00000000000..fe559ed058b --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.stories.ts @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/footers/automatedEmailChangePassword', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'This partial is used in footers for automated emails recommending a password change.', + { + layout: null, + subject: 'N/A', + partial: 'automatedEmailChangePassword', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + }, + includes +); + +export const AutomatedEmailChangePassword = createStory(); diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.txt b/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.txt new file mode 100644 index 00000000000..17f43ed9a0c --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailChangePassword/index.txt @@ -0,0 +1,5 @@ +automated-email-change-plaintext-2 = "If you didn’t take this action, change your password right away:" +<%- passwordChangeLink %> + +automated-email-support-plaintext = "For more info, visit Mozilla Support:" +<%- supportUrl %> diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/en.ftl b/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/en.ftl new file mode 100644 index 00000000000..90b3bf00a90 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/en.ftl @@ -0,0 +1 @@ +automated-email-inactive-account = This is an automated email. You are receiving it because you have a { -product-mozilla-account } and it has been 2 years since your last sign-in. diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.mjml b/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.mjml new file mode 100644 index 00000000000..89e20b2facf --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.mjml @@ -0,0 +1,13 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + This is an automated email. You are receiving it because you have a Mozilla account and it has been 2 years since your last sign-in. + + + + diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.stories.ts b/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.stories.ts new file mode 100644 index 00000000000..f207e9eb7f1 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.stories.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/footers/automatedEmailInactiveAccount', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'This partial is used in footers for automated inactive account email notifications.', + { + layout: null, + subject: 'N/A', + partial: 'automatedEmailInactiveAccount', + }, + includes +); + +export const AutomatedEmailInactiveAccount = createStory(); diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.txt b/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.txt new file mode 100644 index 00000000000..815634fd0df --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailInactiveAccount/index.txt @@ -0,0 +1 @@ +automated-email-inactive-account = "This is an automated email. You are receiving it because you have a Mozilla account and it has been 2 years since your last sign-in." diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/en.ftl b/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/en.ftl new file mode 100644 index 00000000000..20bdf38ae9e --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/en.ftl @@ -0,0 +1,3 @@ +# supportLink - https://support.mozilla.org/kb/im-having-problems-my-firefox-account +automated-email-no-action = { automated-email-no-action-plaintext } For more info, visit { -brand-mozilla } Support. +automated-email-no-action-plaintext = This is an automated email. If you received it by mistake, you don’t need to do anything. diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.mjml b/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.mjml new file mode 100644 index 00000000000..c787efa3e45 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.mjml @@ -0,0 +1,14 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + This is an automated email. If you received it by mistake, you don’t need to do anything. For more info, visit + Mozilla Support. + + + + diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.stories.ts b/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.stories.ts new file mode 100644 index 00000000000..15749b9e4db --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.stories.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/footers/automatedEmailNoAction', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'This partial is used in footers for automated emails where no action is recommended.', + { + layout: null, + subject: 'N/A', + partial: 'automatedEmailNoAction', + }, + includes +); + +export const AutomatedEmailNoAction = createStory(); diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.txt b/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.txt new file mode 100644 index 00000000000..95173754104 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailNoAction/index.txt @@ -0,0 +1 @@ +automated-email-no-action-plaintext = "This is an automated email. If you received it by mistake, you don’t need to do anything." diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/en.ftl b/libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/en.ftl new file mode 100644 index 00000000000..8b981139521 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/en.ftl @@ -0,0 +1,2 @@ +# After the colon, there's a link to https://accounts.firefox.com/settings/change_password +automated-email-not-authorized-plaintext = This is an automated email; if you did not authorize this action, then please change your password: diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/index.stories.ts b/libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/index.stories.ts new file mode 100644 index 00000000000..1af3a93a03c --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/index.stories.ts @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/footers/automatedEmailNotAuthorized', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'This partial is used in footers for automated emails - text format only', + { + layout: null, + subject: 'N/A', + partial: 'automatedEmailNotAuthorized', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + }, + includes +); + +export const AutomatedEmailNotAuthorized = createStory(); diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/index.txt b/libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/index.txt new file mode 100644 index 00000000000..5eef4d3b506 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailNotAuthorized/index.txt @@ -0,0 +1,2 @@ +automated-email-not-authorized-plaintext = "This is an automated email; if you did not authorize this action, then please change your password:" +<%- passwordChangeLink %> diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/en.ftl b/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/en.ftl new file mode 100644 index 00000000000..180374a3ca5 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/en.ftl @@ -0,0 +1,44 @@ +# "This request" refers to a modification (addition, change or removal) to the account recovery key. +# Variables: +# - $uaBrowser: the user agent's browser (e.g., Firefox Nightly) +# - $uaOS: the user agent's operating system (e.g, MacOS) +# - $uaOSVersion - the user agent's operating system version +automatedEmailRecoveryKey-origin-device-all = This request came from { $uaBrowser } on { $uaOS } { $uaOSVersion }. + +# "This request" refers to a modification (addition, change or removal) to the account recovery key. +# Variables: +# - $uaBrowser: the user agent's browser (e.g., Firefox Nightly) +# - $uaOS: the user agent's operating system (e.g, MacOS) +automatedEmailRecoveryKey-origin-device-browser-os = This request came from { $uaBrowser } on { $uaOS }. + +# "This request" refers to a modification (addition, change or removal) to the account recovery key. +# Variables: +# - $uaBrowser: the user agent's browser (e.g., Firefox Nightly) +automatedEmailRecoveryKey-origin-device-browser-only = This request came from { $uaBrowser }. + +# "This request" refers to a modification (addition, change or removal) to the account recovery key. +# Variables: +# - $uaOS: the user agent's operating system (e.g, MacOS) +# - $uaOSVersion - the user agent's operating system version +automatedEmailRecoveryKey-origin-device-OS-version-only = This request came from { $uaOS } { $uaOSVersion }. + +# "This request" refers to a modification (addition, change or removal) to the account recovery key. +# Variables: +# - $uaOS: the user agent's operating system (e.g, MacOS) +automatedEmailRecoveryKey-origin-device-OS-only = This request came from { $uaOS }. + +automatedEmailRecoveryKey-delete-key-change-pwd = If this wasn’t you, delete the new key and change your password. +automatedEmailRecoveryKey-change-pwd-only = If this wasn’t you, change your password. +automatedEmailRecoveryKey-more-info = For more info, visit { -brand-mozilla } Support. + +# Colon is followed by user device info on a separate line (e.g., "Firefox Nightly on Mac OSX 10.11") +automatedEmailRecoveryKey-origin-plaintext = This request came from: +# Colon is followed by a URL to the account recovery key section of account settings +automatedEmailRecoveryKey-notyou-delete-key-plaintext = If this wasn’t you, delete the new key: +# Colon is followed by a URL to the change password section of account settings +automatedEmailRecoveryKey-notyou-change-pwd-only-plaintext = If this wasn’t you, change your password: +# This string is shown on its own line, after automatedEmailRecoveryKey-notyou-delete-key-plaintext and its URL +# Colon is followed by a URL to the change password section of account settings +automatedEmailRecoveryKey-notyou-change-pwd-plaintext = and change your password: +# Colon is followed by a URL to Mozilla Support's "I'm having problems with my account" page +automatedEmailRecoveryKey-more-info-plaintext = For more info, visit { -brand-mozilla } Support: diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.mjml b/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.mjml new file mode 100644 index 00000000000..5532740b467 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.mjml @@ -0,0 +1,45 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + <% if (locals.device) { %> + <%# Request origin will only be shown if device info is available - otherwise it will be omitted %> + <% const { uaBrowser, uaOS, uaOSVersion } = device; %> + <% if (uaBrowser) { %> + <% if (uaOS) { %> + <% if (uaOSVersion) { %> + <%- `This request came from ${uaBrowser} on ${uaOS} ${uaOSVersion}.` %> + <% } else { %> + <%- `This request came from ${uaBrowser} on ${uaOS}.` %> + <% } %> + <% } else { %> + <%- `This request came from ${uaBrowser}.` %> + <% } %> + <% } else if (uaOS) { %> + <% if (uaOSVersion) { %> + <%- `This request came from ${uaOS} ${uaOSVersion}.` %> + <% } else { %> + <%- `This request came from ${uaOS}.` %> + <% } %> + <% } %> + <% } %> + + <% if (locals.keyExists === true ) { %> + + If this wasn’t you, delete the new key and change your password. + + <% } else { %> + + If this wasn’t you, change your password. + + <% } %> + + + For more info, visit Mozilla Support. + + + + diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.stories.ts b/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.stories.ts new file mode 100644 index 00000000000..9ed80332f93 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.stories.ts @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { MOCK_DEVICE_ALL } from '../userDevice/mocks'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/footers/automatedEmailRecoveryKey', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'This partial is used in footers for automated emails when the action involved an account recovery key.', + { + layout: null, + subject: 'N/A', + partial: 'automatedEmailRecoveryKey', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + }, + includes +); + +export const AutomatedEmailRecoveryKey = createStory( + {}, + 'When no recovery key exists for the account.' +); + +export const AutomatedEmailRecoveryKeyExists = createStory( + { + keyExists: true, + revokeAccountRecoveryLink: 'http://localhost:3030/settings/#recovery-key', + }, + 'When recovery key exists for the account.' +); + +export const AutomatedEmailRecoveryKeyInclDeviceInfo = createStory( + { + device: MOCK_DEVICE_ALL, + }, + 'With device information.' +); diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.txt b/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.txt new file mode 100644 index 00000000000..04272fb3c1b --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailRecoveryKey/index.txt @@ -0,0 +1,23 @@ +<%# Request origin will only be shown if device info is available - otherwise it will be omitted %> + +<% if (locals.device) { %> + automatedEmailRecoveryKey-origin-plaintext = "This request came from:" + <% const device = include('/partials/userDevice/index.txt') %><%- device.trim() %> +<% } %> + +<% if (locals.keyExists === true) { %> +automatedEmailRecoveryKey-notyou-delete-key-plaintext = "If this wasn’t you, delete the new key:" +<%- revokeAccountRecoveryLink %> + +automatedEmailRecoveryKey-notyou-change-pwd-plaintext = "and change your password:" +<%- passwordChangeLink %> + +<% } else { %> + +automatedEmailRecoveryKey-notyou-change-pwd-only-plaintext = "If this wasn’t you, change your password:" +<%- passwordChangeLink %> + +<% } %> + +automatedEmailRecoveryKey-more-info-plaintext = "For more info, visit Mozilla Support:" +<%- supportUrl %> diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/en.ftl b/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/en.ftl new file mode 100644 index 00000000000..7e78d1c1356 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/en.ftl @@ -0,0 +1,5 @@ +automated-email-reset = This is an automated email; if you did not authorize this action, then please reset your password. + For more information, please visit { -brand-mozilla } Support. +# Variables: +# $resetLink (String) - Link to https://accounts.firefox.com/reset_password +automated-email-reset-plaintext-v2 = If you did not authorize this action, please reset your password now at { $resetLink } diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.mjml b/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.mjml new file mode 100644 index 00000000000..e73aa23699a --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.mjml @@ -0,0 +1,16 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + This is an automated email; if you did not authorize this action, then + please reset your password. + For more information, please visit + Mozilla Support. + + + + diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.stories.ts b/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.stories.ts new file mode 100644 index 00000000000..64aa80f6938 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.stories.ts @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/footers/automatedEmailResetPassword', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'This partial is used in footers for automated emails where password reset is recommended.', + { + layout: null, + subject: 'N/A', + partial: 'automatedEmailResetPassword', + resetLink: 'http://localhost:3030/reset_password', + }, + includes +); + +export const AutomatedEmailResetPassword = createStory(); diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.txt b/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.txt new file mode 100644 index 00000000000..56bf1ca5edc --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailResetPassword/index.txt @@ -0,0 +1,4 @@ +automated-email-reset-plaintext-v2 = "If you did not authorize this action, please reset your password now at <%- resetLink %>" + +automated-email-support-plaintext = "For more info, visit Mozilla Support:" +<%- supportUrl %> diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/en.ftl b/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/en.ftl new file mode 100644 index 00000000000..218d3417f46 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/en.ftl @@ -0,0 +1,8 @@ +# This message is used by multiple automated emails that notify users of security events on their account +# "this action" is meant to be a generic term, and could, for example, refer to using a backup authentication code to confirm a password reset +automated-email-reset-pwd-two-factor = If you didnʼt take this action, then reset your password and reset two-step authentication right away. + For more information, please visit { -brand-mozilla } Support. +# Followed by link to https://accounts.firefox.com/reset_password +automated-email-reset-pwd-plaintext-v3 = If you didnʼt take this action, then reset your password right away at: +# Followed by link to https://accounts.firefox.com/settings#two-step-authentication +automated-email-reset-two-factor-plaintext = Also, reset two-step authentication at: diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.mjml b/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.mjml new file mode 100644 index 00000000000..dcc928b1902 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.mjml @@ -0,0 +1,17 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + If you didnʼt take this action, then + reset your password and + reset two-step authentication right away. + For more information, please visit + Mozilla Support. + + + + diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.stories.ts b/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.stories.ts new file mode 100644 index 00000000000..1fa0d7ec85c --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/footers/automatedEmailResetPasswordTwoFactor', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'This partial is used in footers for automated emails where password reset is recommended.', + { + layout: null, + subject: 'N/A', + partial: 'automatedEmailResetPasswordTwoFactor', + resetLink: 'http://localhost:3030/reset_password', + twoFactorSettingsLink: + 'http://localhost:3030/settings#two-step-authentication', + }, + includes +); + +export const AutomatedEmailResetPassword = createStory(); diff --git a/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.txt b/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.txt new file mode 100644 index 00000000000..d278711f017 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/automatedEmailResetPasswordTwoFactor/index.txt @@ -0,0 +1,8 @@ +automated-email-reset-pwd-plaintext-v3 = "If you didnʼt take this action, then reset your password right away at:" +<%- resetLink %> + +automated-email-reset-two-factor-plaintext = "Also, reset two-step authentication at:" +<%- twoFactorSettingsLink %> + +automated-email-support-plaintext = "For more info, visit Mozilla Support:" +<%- supportUrl %> diff --git a/libs/accounts/email-renderer/src/partials/bannerWarning/en.ftl b/libs/accounts/email-renderer/src/partials/bannerWarning/en.ftl new file mode 100644 index 00000000000..35c65a7050d --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/bannerWarning/en.ftl @@ -0,0 +1,4 @@ +# $accountsEmail is the Mozilla accounts sender email address (e.g. accounts@firefox.com) +banner-warning-message = { -brand-firefox } add-on developers have been targeted by phishing email attacks recently. We’ll only send emails about your { -product-mozilla-account } from { $accountsEmail }. +banner-warning-message-plaintext = { -brand-firefox } add-on developers have been targeted by phishing email attacks recently. We’ll only send emails about your { -product-mozilla-account } from this email address: +banner-warning-check = Check to make sure the device and location you signed in to is correct. diff --git a/libs/accounts/email-renderer/src/partials/bannerWarning/index.mjml b/libs/accounts/email-renderer/src/partials/bannerWarning/index.mjml new file mode 100644 index 00000000000..62f702376ef --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/bannerWarning/index.mjml @@ -0,0 +1,24 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + + + "> + Firefox add-on developers have been targeted by phishing email attacks recently. We’ll only send emails about your Mozilla account from accounts@firefox.com. + + + + + + Check to make sure the device and location you signed in to is correct. + + + + diff --git a/libs/accounts/email-renderer/src/partials/bannerWarning/index.scss b/libs/accounts/email-renderer/src/partials/bannerWarning/index.scss new file mode 100644 index 00000000000..e85f4289437 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/bannerWarning/index.scss @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@use '../../global.scss'; + +.banner-warning-container { + @extend %banner-common; + @extend .p-4; + background-color: global.$purple-600; +} + +.banner-warning-text > div { + @extend %text-header-common; + @extend .text-xs; + color: global.$white !important; +} + +.banner-warning-text div span a { + text-decoration: none !important; + color: global.$white !important; +} diff --git a/libs/accounts/email-renderer/src/partials/bannerWarning/index.txt b/libs/accounts/email-renderer/src/partials/bannerWarning/index.txt new file mode 100644 index 00000000000..023e4fa680b --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/bannerWarning/index.txt @@ -0,0 +1,3 @@ +banner-warning-message-plaintext = "Firefox add-on developers have been targeted by phishing email attacks recently. We’ll only send emails about your Mozilla account from this email address:" +<%- accountsEmail %> +banner-warning-check = "Check to make sure the device and location you signed in to is correct." diff --git a/libs/accounts/email-renderer/src/partials/brandMessaging/en.ftl b/libs/accounts/email-renderer/src/partials/brandMessaging/en.ftl new file mode 100644 index 00000000000..476c18b5b90 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/brandMessaging/en.ftl @@ -0,0 +1 @@ +brand-banner-message = Did you know we changed our name from { -product-firefox-accounts } to { -product-mozilla-accounts }? Learn more diff --git a/libs/accounts/email-renderer/src/partials/brandMessaging/index.mjml b/libs/accounts/email-renderer/src/partials/brandMessaging/index.mjml new file mode 100644 index 00000000000..f79b91394fa --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/brandMessaging/index.mjml @@ -0,0 +1,19 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<% if (locals.brandMessagingMode == 'postlaunch') { %> + + + + + + + Did you know we changed our name from Firefox accounts to Mozilla accounts? + Learn more + + + + + +<% } %> diff --git a/libs/accounts/email-renderer/src/partials/brandMessaging/index.scss b/libs/accounts/email-renderer/src/partials/brandMessaging/index.scss new file mode 100644 index 00000000000..4b7477905e0 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/brandMessaging/index.scss @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@use '../../global.scss'; + +.brand-message-container { + @extend %banner-common; + background: linear-gradient( + 88.76deg, + #e4eaf6 3.37%, + #dbeef8 39.93%, + #daf3f4 65.09%, + #e3f6ed 102.21% + ); + background-color: #dbeef8; +} + +.brand-message-text > div { + @extend %text-banner-common; +} + +.brand-message-text div span a { + @extend %link-banner-common; +} diff --git a/libs/accounts/email-renderer/src/partials/brandMessaging/index.txt b/libs/accounts/email-renderer/src/partials/brandMessaging/index.txt new file mode 100644 index 00000000000..9932f4f3187 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/brandMessaging/index.txt @@ -0,0 +1,6 @@ +<% if (locals.brandMessagingMode == 'postlaunch') { %> +brand-banner-message = "Did you know we changed our name from Firefox accounts to Mozilla accounts? Learn more" + +https://support.mozilla.org/kb/firefox-accounts-renamed-mozilla-accounts + +<% } %> diff --git a/libs/accounts/email-renderer/src/partials/button/index.mjml b/libs/accounts/email-renderer/src/partials/button/index.mjml new file mode 100644 index 00000000000..35e0c3aea1a --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/button/index.mjml @@ -0,0 +1,13 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + + <%- locals.buttonText %> + + + diff --git a/libs/accounts/email-renderer/src/partials/button/index.scss b/libs/accounts/email-renderer/src/partials/button/index.scss new file mode 100644 index 00000000000..611e2431671 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/button/index.scss @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@use '../../global.scss'; + +.primary-button { + height: 56px !important; + max-width: 310px !important; + + a { + @extend .font-sans; + @extend .text-lg; + background: global.$blue-500 !important; + max-width: 310px !important; + color: global.$white; + padding: global.$s-2 global.$s-4 !important; + border-radius: 4px !important; + } +} diff --git a/libs/accounts/email-renderer/src/partials/cancellationSurvey/en.ftl b/libs/accounts/email-renderer/src/partials/cancellationSurvey/en.ftl new file mode 100644 index 00000000000..45dabe4b3df --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/cancellationSurvey/en.ftl @@ -0,0 +1,3 @@ +cancellationSurvey = Please help us improve our services by taking this short survey. +# After the colon, there's a link to https://survey.alchemer.com/s3/6534408/Privacy-Security-Product-Cancellation-of-Service-Q4-21 +cancellationSurvey-plaintext = Please help us improve our services by taking this short survey: diff --git a/libs/accounts/email-renderer/src/partials/cancellationSurvey/index.mjml b/libs/accounts/email-renderer/src/partials/cancellationSurvey/index.mjml new file mode 100644 index 00000000000..b719ea647af --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/cancellationSurvey/index.mjml @@ -0,0 +1,13 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + Please help us improve our services by taking this short survey. + + + + diff --git a/libs/accounts/email-renderer/src/partials/cancellationSurvey/index.txt b/libs/accounts/email-renderer/src/partials/cancellationSurvey/index.txt new file mode 100644 index 00000000000..12c74dcdb5c --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/cancellationSurvey/index.txt @@ -0,0 +1,2 @@ +cancellationSurvey-plaintext = "Please help us improve our services by taking this short survey:" +<%- cancellationSurveyUrl %> diff --git a/libs/accounts/email-renderer/src/partials/changePassword/en.ftl b/libs/accounts/email-renderer/src/partials/changePassword/en.ftl new file mode 100644 index 00000000000..5524f1eefd4 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/changePassword/en.ftl @@ -0,0 +1 @@ +change-password-plaintext = If you suspect that someone is trying to gain access to your account, please change your password. diff --git a/libs/accounts/email-renderer/src/partials/changePassword/index.txt b/libs/accounts/email-renderer/src/partials/changePassword/index.txt new file mode 100644 index 00000000000..8fdf94a87f0 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/changePassword/index.txt @@ -0,0 +1,2 @@ +change-password-plaintext = "If you suspect that someone is trying to gain access to your account, please change your password." +<%- passwordChangeLink %> \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/partials/icon/index.mjml b/libs/accounts/email-renderer/src/partials/icon/index.mjml new file mode 100644 index 00000000000..a824a0d0baa --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/icon/index.mjml @@ -0,0 +1,18 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + <% if (locals.productIconURLNew) { %> + + + + + <% } else { %> + + + + <% } %> + diff --git a/libs/accounts/email-renderer/src/partials/images.mjml b/libs/accounts/email-renderer/src/partials/images.mjml new file mode 100644 index 00000000000..753bd7c57ba --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/images.mjml @@ -0,0 +1,63 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<% if (!locals.productName) { locals.productName = 'Firefox' %><% } %> + + + fxa-header-mozilla-logo + + + mozilla-logo + + + + fxa-header-sync-devices-image + + + sync-devices-image + + + + subplat-header-mozilla-logo-2 + + + subplat-mozilla-logo + + + + subplat-footer-mozilla-logo-2 + + + mozilla-logo-footer + + + + body-devices-image + + + devices-image + + + body-devices-image + + + devices-image + + + + body-android-badge + <%= JSON.stringify({productName}) %> + + + google-play-badge + + + + body-ios-badge + <%= JSON.stringify({productName}) %> + + + apple-app-badge + + diff --git a/libs/accounts/email-renderer/src/partials/manageAccount/en.ftl b/libs/accounts/email-renderer/src/partials/manageAccount/en.ftl new file mode 100644 index 00000000000..c357b97326c --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/manageAccount/en.ftl @@ -0,0 +1,2 @@ +manage-account = Manage account +manage-account-plaintext = { manage-account }: diff --git a/libs/accounts/email-renderer/src/partials/manageAccount/index.txt b/libs/accounts/email-renderer/src/partials/manageAccount/index.txt new file mode 100644 index 00000000000..68374c8ee91 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/manageAccount/index.txt @@ -0,0 +1,2 @@ +manage-account-plaintext = "Manage account:" +<%- link %> diff --git a/libs/accounts/email-renderer/src/partials/metadata.mjml b/libs/accounts/email-renderer/src/partials/metadata.mjml new file mode 100644 index 00000000000..988603e0f94 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/metadata.mjml @@ -0,0 +1,26 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + <% if (locals.oneClickLink) { %> + + <% } %> + diff --git a/libs/accounts/email-renderer/src/partials/mocks.ts b/libs/accounts/email-renderer/src/partials/mocks.ts new file mode 100644 index 00000000000..4a471268e8c --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/mocks.ts @@ -0,0 +1,6 @@ +export const includes = { + subject: { + id: 'mock-id', + message: 'Mock Subject', + }, +}; diff --git a/libs/accounts/email-renderer/src/partials/paymentPlanDetails/en.ftl b/libs/accounts/email-renderer/src/partials/paymentPlanDetails/en.ftl new file mode 100644 index 00000000000..ed44aa67ced --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/paymentPlanDetails/en.ftl @@ -0,0 +1,11 @@ +payment-details = Payment details: +# Variables: +# $invoiceNumber (String) - The invoice number of the subscription invoice, e.g. 8675309 +payment-plan-invoice-number = Invoice Number: { $invoiceNumber } +# Variables: +# $invoiceDateOnly (String) - The date of the invoice, e.g. 01/20/2016 +# $invoiceTotal (String) - The amount of the subscription invoice, including currency, e.g. $10.00 +payment-plan-charged = Charged: { $invoiceTotal } on { $invoiceDateOnly } +# Variables +# $nextInvoiceDateOnly (String) - The date of the next invoice, e.g. 01/20/2016 +payment-plan-next-invoice = Next Invoice: { $nextInvoiceDateOnly } diff --git a/libs/accounts/email-renderer/src/partials/paymentPlanDetails/index.mjml b/libs/accounts/email-renderer/src/partials/paymentPlanDetails/index.mjml new file mode 100644 index 00000000000..8236a410ed5 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/paymentPlanDetails/index.mjml @@ -0,0 +1,30 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + Payment details: +
    +
  • + <%- productName %> +
  • + + <% if (locals.invoiceNumber) { %> +
  • + Invoice Number: <%- invoiceNumber %> +
  • + <% } %> + + <% if (locals.invoiceDateOnly && locals.invoiceTotal) { %> +
  • + Charged: <%- invoiceTotal %> on <%- invoiceDateOnly %> +
  • + <% } %> + + <% if (locals.nextInvoiceDateOnly) { %> +
  • + Next Invoice: <%- nextInvoiceDateOnly %> +
  • + <% } %> +
+
diff --git a/libs/accounts/email-renderer/src/partials/paymentPlanDetails/index.txt b/libs/accounts/email-renderer/src/partials/paymentPlanDetails/index.txt new file mode 100644 index 00000000000..683f720cc86 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/paymentPlanDetails/index.txt @@ -0,0 +1,6 @@ +payment-details = "Payment details:" + +<% if (locals.productName) { %><%- productName %><% } %> +<% if (locals.invoiceNumber) { %>payment-plan-invoice-number = "Invoice Number: <%- invoiceNumber %>"<% } %> +<% if (locals.invoiceDateOnly && locals.invoiceTotal) { %>payment-plan-charged = "Charged: <%- invoiceTotal %> on <%- invoiceDateOnly %>"<% } %> +<% if (locals.nextInvoiceDateOnly) { %>payment-plan-next-invoice = "Next Invoice: <%- nextInvoiceDateOnly %>"<% } %> diff --git a/libs/accounts/email-renderer/src/partials/paymentProvider/en.ftl b/libs/accounts/email-renderer/src/partials/paymentProvider/en.ftl new file mode 100644 index 00000000000..2335b7f5ca9 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/paymentProvider/en.ftl @@ -0,0 +1,14 @@ +## $paymentProviderName (String) - The brand name of the payment method, e.g. PayPal, Apple Pay, Google Pay, Link +payment-method-payment-provider = Payment method: { $paymentProviderName } +payment-method-payment-provider-plaintext = Payment method: { $paymentProviderName } + +## This string displays when the type of credit card is known +## https://stripe.com/docs/payments/cards/supported-card-brands +## Variables: +## $cardName (String) - The brand name of the credit card, e.g. American Express +## $lastFour (String) - The last four digits of the credit card, e.g. 5309 + +payment-provider-card-name-ending-in-plaintext = Payment method: { $cardName } ending in { $lastFour } +payment-provider-card-ending-in-plaintext = Payment method: Card ending in { $lastFour } +payment-provider-card-ending-in = Payment method: Card ending in { $lastFour } +payment-provider-card-ending-in-card-name = Payment method: { $cardName } ending in { $lastFour } diff --git a/libs/accounts/email-renderer/src/partials/paymentProvider/index.mjml b/libs/accounts/email-renderer/src/partials/paymentProvider/index.mjml new file mode 100644 index 00000000000..8e3e6ec8c72 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/paymentProvider/index.mjml @@ -0,0 +1,27 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<% if (invoiceAmountDueInCents > 0) { %> + <% if (paymentProviderName) { %> + + + Payment method: <%- paymentProviderName %> + + + <% } else { %> + <% if (cardName && lastFour) { %> + + + Payment method: <%- cardName %> ending in <%- lastFour %> + + + <% } else { %> + + + Payment method: Card ending in <%- lastFour %> + + + <% } %> + <% } %> +<% } %> diff --git a/libs/accounts/email-renderer/src/partials/paymentProvider/index.txt b/libs/accounts/email-renderer/src/partials/paymentProvider/index.txt new file mode 100644 index 00000000000..3aa2ee9e5f6 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/paymentProvider/index.txt @@ -0,0 +1,11 @@ +<% if (invoiceAmountDueInCents > 0) { %> + <% if (paymentProviderName) { %> + payment-method-payment-provider-plaintext = "Payment method: { $paymentProviderName }" + <% } else { %> + <% if (cardName && lastFour) {%> + payment-provider-card-name-ending-in-plaintext = "Payment method: <%- cardName %> ending in <%- lastFour %>" + <% } else { %> + payment-provider-card-ending-in-plaintext = "Payment method: Card ending in <%- lastFour %>" + <% } %> + <% } %> +<% } %> diff --git a/libs/accounts/email-renderer/src/partials/subscriptionCharges/en.ftl b/libs/accounts/email-renderer/src/partials/subscriptionCharges/en.ftl new file mode 100644 index 00000000000..3cd6a5cd431 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionCharges/en.ftl @@ -0,0 +1,53 @@ +subscription-charges-invoice-summary = Invoice Summary +# Variables: +## $invoiceNumber (String) - The invoice number of the subscription invoice, e.g. 8675309 +## $invoiceDateOnly (String) - The date of the next invoice, e.g. August 28, 2025 + +subscription-charges-invoice-number = Invoice number: { $invoiceNumber } +subscription-charges-invoice-number-plaintext = Invoice number: { $invoiceNumber } +subscription-charges-invoice-date = Date: { $invoiceDateOnly } +subscription-charges-invoice-date-plaintext = Date: { $invoiceDateOnly } +subscription-charges-prorated-price = Prorated price +# $remainingAmountTotal (String) - The prorated amount of the subscription invoice, including currency, e.g. $4.00 +subscription-charges-prorated-price-plaintext = Prorated price: { $remainingAmountTotal } +subscription-charges-list-price = List price +# $offeringPrice (String) - The list price of the subscription offering, including currency, e.g. $10.00 +subscription-charges-list-price-plaintext = List price: { $offeringPrice } +subscription-charges-credit-from-unused-time = Credit from unused time +# $unusedAmountTotal (String) - The credit amount from unused time of the subscription invoice, including currency, e.g. $2.00 +subscription-charges-credit-from-unused-time-plaintext = Credit from unused time: { $unusedAmountTotal } +subscription-charges-subtotal = Subtotal +# $invoiceSubtotal (String) - The amount, before discount, of the subscription invoice, including currency, e.g. $10.00 +subscriptionFirstInvoiceDiscount-content-subtotal = Subtotal: { $invoiceSubtotal } + +## $invoiceDiscountAmount (String) - The amount of the discount of the subscription invoice, including currency, e.g. $2.00 +## $discountDuration - The duration of the discount in number of months, e.g. "3" if the discount is 3-months + +subscription-charges-one-time-discount = One-time discount +subscription-charges-one-time-discount-plaintext = One-time discount: { $invoiceDiscountAmount } +subscription-charges-repeating-discount = + { $discountDuration -> + *[other] { $discountDuration }-month discount + } +subscription-charges-repeating-discount-plaintext = + { $discountDuration -> + *[other] { $discountDuration }-month discount: { $invoiceDiscountAmount } + } +subscription-charges-discount = Discount +subscription-charges-discount-plaintext = Discount: { $invoiceDiscountAmount } +subscription-charges-taxes = Taxes & fees +# $invoiceTaxAmount (String) - The amount of the tax of the subscription invoice, including currency, e.g. $2.00 +subscriptionCharges-content-tax-plaintext = Taxes & fees: { $invoiceTaxAmount } +subscription-charges-total = Total +# $invoiceTotal (String) - The total amount of the subscription invoice, including currency, e.g. $10.00 +subscription-charges-total-plaintext = Total: { $invoiceTotal } +subscription-charges-credit-applied = Credit applied +# $creditApplied (String) - The amount of credit applied to the subscription invoice, including currency, e.g. $2.00 +subscription-charges-credit-applied-plaintext = Credit applied: { $creditApplied } +subscription-charges-amount-paid = Amount paid +# $invoiceAmountDue (String) - The total that the customer owes after all credits, discounts, and taxes have been applied, including currency, e.g. $8.00 +subscription-charges-amount-paid-plaintext = Amount paid: { $invoiceAmountDue } +# $creditReceived (String) - The amount, after discount, of the subscription invoice, including currency, e.g. $8.00 +subscription-charges-credit-received = You have received an account credit of { $creditReceived }, which will be applied to your future invoices. + +## diff --git a/libs/accounts/email-renderer/src/partials/subscriptionCharges/index.mjml b/libs/accounts/email-renderer/src/partials/subscriptionCharges/index.mjml new file mode 100644 index 00000000000..4d179b2df87 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionCharges/index.mjml @@ -0,0 +1,207 @@ + + + Invoice Summary + + + + + + Invoice number: <%- invoiceNumber %> + + + + + + Date: <%- invoiceDateOnly %> + + + +<%- include ('/partials/paymentProvider/index.mjml') %> + +<% if (remainingAmountTotalInCents && offeringPriceInCents !== remainingAmountTotalInCents) { %> + + + + + + +
+ + Prorated price + + + + <%- remainingAmountTotal %> + +
+
+<% } else { %> + + + + + + +
+ + List price + + + + <%- offeringPrice %> + +
+
+<% } %> + +<% if (!!unusedAmountTotalInCents) { %> + + + + + + +
+ + Credit from unused time + + + + <%- unusedAmountTotal %> + +
+
+ + <% if (invoiceSubtotalInCents !== invoiceTotalInCents) { %> + + + + + + +
+ + Subtotal + + + + <%- invoiceSubtotal %> + +
+
+ <% } %> +<% } %> + +<% if (discountType && invoiceDiscountAmount) { %> + + + + + + +
+ <% if (discountType==='once' ) { %> + + One-time discount + + <% } else if (discountType==='repeating' ) { %> + + <%discountDuration%>-month discount + + <% } else { %> + + Discount + + <% } %> + + + <%- invoiceDiscountAmount %> + +
+
+<% } %> + +<% if (showTaxAmount && invoiceTaxAmountInCents > 0 ) { %> + + + + + + +
+ + Taxes & fees + + + + <%- invoiceTaxAmount %> + +
+
+<% } %> + +<% if (invoiceTotalInCents !== invoiceAmountDueInCents) { %> + + + + + + +
+ + Total + + + + <%- invoiceTotal %> + +
+
+<% } %> + +<% if (!!creditAppliedInCents && invoiceStartingBalance < 0) { %> + + + + + + +
+ + Credit applied + + + + <%- creditApplied %> + +
+
+<% } %> + + + + + + + +
+ + Amount paid + + + + <%- invoiceAmountDue %> + +
+
+ +<% if (locals.invoiceTotalInCents < 0) { %> + +

+ You have received an account credit of <%- creditReceived %>, which will be applied to your future invoices. +

+
+<% } %> diff --git a/libs/accounts/email-renderer/src/partials/subscriptionCharges/index.txt b/libs/accounts/email-renderer/src/partials/subscriptionCharges/index.txt new file mode 100644 index 00000000000..b6c711885c1 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionCharges/index.txt @@ -0,0 +1,40 @@ +subscription-charges-invoice-summary = "Invoice Summary" + +subscription-charges-content-invoice-number-plaintext = "Invoice number: <%- invoiceNumber %>" +subscription-charges-invoice-date-plaintext = "Date: <%- invoiceDateOnly %>" +<%- include ('/partials/paymentProvider/index.txt') %> +<% if (remainingAmountTotalInCents && offeringPriceInCents !== remainingAmountTotalInCents) { %> +subscription-charges-prorated-price-plaintext = "Prorated price: <%- remainingAmountTotal %>" +<% } else { %> +subscription-charges-list-price-plaintext = "List price: <%- offeringPrice %>" +<% } %> +<% if (!!unusedAmountTotalInCents) { %> +subscription-charges-credit-from-unused-time-plaintext = "Credit from unused time: <%- unusedAmountTotal %>" +<% if (invoiceSubtotalInCents !== invoiceTotalInCents) { %> +subscriptionFirstInvoiceDiscount-content-subtotal = "Subtotal: <%- invoiceSubtotal %>" +<% } %> +<% } %> +<% if (discountType && invoiceDiscountAmount) { %> +<% if (discountType === 'once') { %> + subscription-charges-one-time-discount-plaintext = "One-time discount: <%- invoiceDiscountAmount %>" +<% } %> +<% if (discountType === 'repeating') { %> + subscription-charges-repeating-discount-plaintext = "<%discountDuration%>-month discount: <%- invoiceDiscountAmount %>" +<% } %> +<% if (discountType === 'forever') { %> + subscription-charges-discount-plaintext = "Discount: <%- invoiceDiscountAmount %>" +<% } %> +<% } %> +<% if (showTaxAmount && invoiceTaxAmountInCents > 0) { %> + subscriptionCharges-content-tax = "Taxes & fees: <%- invoiceTaxAmount %>" +<% } %> +<% if (invoiceTotalInCents !== invoiceAmountDueInCents) { %> +subscription-charges-total-plaintext = "Total: <%- invoiceTotal %>" +<% } %> +<% if (!!creditAppliedInCents && invoiceStartingBalance < 0) { %> +subscription-charges-credit-applied-plaintext = "Credit applied: <%- creditApplied %>" +<% } %> +subscription-charges-amount-paid-plaintext = "Amount paid: <%- invoiceAmountDue %>" +<% if (locals.invoiceTotalInCents < 0) { %> +subscription-charges-credit-received = "You have received an account credit of <%- invoiceTotal %>, which will be applied to your future invoices." +<% } %> diff --git a/libs/accounts/email-renderer/src/partials/subscriptionSupport/en.ftl b/libs/accounts/email-renderer/src/partials/subscriptionSupport/en.ftl new file mode 100644 index 00000000000..40bd7eb548d --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionSupport/en.ftl @@ -0,0 +1,3 @@ +subscriptionSupport = Questions about your subscription? Our support team is here to help you. +# After the colon, there's a link to https://accounts.firefox.com/support +subscriptionSupport-plaintext = Questions about your subscription? Our support team is here to help you: diff --git a/libs/accounts/email-renderer/src/partials/subscriptionSupport/index.mjml b/libs/accounts/email-renderer/src/partials/subscriptionSupport/index.mjml new file mode 100644 index 00000000000..23e0f50fd8c --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionSupport/index.mjml @@ -0,0 +1,13 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + Questions about your subscription? Our support team is here to help you. + + + + diff --git a/libs/accounts/email-renderer/src/partials/subscriptionSupport/index.txt b/libs/accounts/email-renderer/src/partials/subscriptionSupport/index.txt new file mode 100644 index 00000000000..fd409cc0c08 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionSupport/index.txt @@ -0,0 +1,2 @@ +subscriptionSupport-plaintext = "Questions about your subscription? Our support team is here to help you:" +<%- subscriptionSupportUrl %> \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/partials/subscriptionSupportContact/en.ftl b/libs/accounts/email-renderer/src/partials/subscriptionSupportContact/en.ftl new file mode 100644 index 00000000000..c8c60931d61 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionSupportContact/en.ftl @@ -0,0 +1,5 @@ +# Variables +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionSupportContact = Thank you for subscribing to { $productName }. If you have any questions about your subscription or need more information about { $productName }, please contact us. +# After the colon, there's a link to https://accounts.firefox.com/support +subscriptionSupportContact-plaintext = Thank you for subscribing to { $productName }. If you have any questions about your subscription or need more information about { $productName }, please contact us: diff --git a/libs/accounts/email-renderer/src/partials/subscriptionSupportContact/index.mjml b/libs/accounts/email-renderer/src/partials/subscriptionSupportContact/index.mjml new file mode 100644 index 00000000000..b16504fdc8d --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionSupportContact/index.mjml @@ -0,0 +1,9 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + Thank you for subscribing to <%- productName %>. If you have any questions about your subscription or need more information about <%- productName %>, please contact us. + + diff --git a/libs/accounts/email-renderer/src/partials/subscriptionSupportContact/index.txt b/libs/accounts/email-renderer/src/partials/subscriptionSupportContact/index.txt new file mode 100644 index 00000000000..e83caaa8497 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionSupportContact/index.txt @@ -0,0 +1,2 @@ +subscriptionSupportContact-plaintext = "Thank you for subscribing to <%- productName %>. If you have any questions about your subscription or need more information about <%- productName %>, please contact us:" +<%- subscriptionSupportUrl %> diff --git a/libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/en.ftl b/libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/en.ftl new file mode 100644 index 00000000000..9f7c5f0834f --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/en.ftl @@ -0,0 +1,7 @@ +subscription-support-get-help = Get help with your subscription +subscription-support-manage-your-subscription = Manage your subscription +# After the colon, there's a link to https://payments.firefox.com/subscriptions +subscription-support-manage-your-subscription-plaintext = Manage your subscription: +subscription-support-contact-support = Contact support +# After the colon, there's a link to https://support.mozilla.com/products +subscription-support-contact-support-plaintext = Contact support: diff --git a/libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/index.mjml b/libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/index.mjml new file mode 100644 index 00000000000..d851d652580 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/index.mjml @@ -0,0 +1,25 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not + distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + Get help with your subscription + + + + + + Manage your subscription + + + + + + Contact support + + + + diff --git a/libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/index.txt b/libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/index.txt new file mode 100644 index 00000000000..321469fe9e7 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionSupportGetHelp/index.txt @@ -0,0 +1,5 @@ +subscription-support-get-help = "Get help with your subscription" + +subscription-support-manage-your-subscription-plaintext = "Manage your subscription:" <%- manageSubscriptionUrl %> + +subscription-support-contact-support-plaintext = "Contact support:" <%- subscriptionSupportUrl %> diff --git a/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/en.ftl b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/en.ftl new file mode 100644 index 00000000000..c38a71eba28 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/en.ftl @@ -0,0 +1,3 @@ +subscriptionUpdateBillingEnsure = You can ensure that your payment method and account information are up to date here. +# After the colon, there's a link to https://accounts.firefox.com/subscriptions +subscriptionUpdateBillingEnsure-plaintext = You can ensure that your payment method and account information are up to date here: diff --git a/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/index.mjml b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/index.mjml new file mode 100644 index 00000000000..4de3374c18b --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/index.mjml @@ -0,0 +1,9 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + You can ensure that your payment method and account information are up to date here. + + diff --git a/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/index.txt b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/index.txt new file mode 100644 index 00000000000..a16fc2ecf7a --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingEnsure/index.txt @@ -0,0 +1,2 @@ +subscriptionUpdateBillingEnsure-plaintext = "You can ensure that your payment method and account information are up to date here:" +<%- updateBillingUrl %> diff --git a/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/en.ftl b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/en.ftl new file mode 100644 index 00000000000..624b53c0288 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/en.ftl @@ -0,0 +1,3 @@ +subscriptionUpdateBillingTry = We’ll try your payment again over the next few days, but you may need to help us fix it by updating your payment information. +# After the colon, there's a link to https://accounts.firefox.com/subscriptions +subscriptionUpdateBillingTry-plaintext = We’ll try your payment again over the next few days, but you may need to help us fix it by updating your payment information: diff --git a/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/index.mjml b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/index.mjml new file mode 100644 index 00000000000..52c30d1da42 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/index.mjml @@ -0,0 +1,13 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + We’ll try your payment again over the next few days, but you may need to help us fix it by updating your payment information. + + + + diff --git a/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/index.txt b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/index.txt new file mode 100644 index 00000000000..b6379b815f3 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionUpdateBillingTry/index.txt @@ -0,0 +1,2 @@ +subscriptionUpdateBillingTry-plaintext = "We’ll try your payment again over the next few days, but you may need to help us fix it by updating your payment information:" +<%- updateBillingUrl %> diff --git a/libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/en.ftl b/libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/en.ftl new file mode 100644 index 00000000000..215afb7d701 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/en.ftl @@ -0,0 +1,3 @@ +subscriptionUpdatePayment = To prevent any interruption to your service, please update your payment information as soon as possible. +# After the colon, there's a link to https://accounts.firefox.com/subscriptions +subscriptionUpdatePayment-plaintext = To prevent any interruption to your service, please update your payment information as soon as possible: diff --git a/libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/index.mjml b/libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/index.mjml new file mode 100644 index 00000000000..8c00ce45de3 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/index.mjml @@ -0,0 +1,13 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + To prevent any interruption to your service, please update your payment information as soon as possible. + + + + diff --git a/libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/index.txt b/libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/index.txt new file mode 100644 index 00000000000..0f19d7d61d7 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/subscriptionUpdatePayment/index.txt @@ -0,0 +1,2 @@ +subscriptionUpdatePayment-plaintext = "To prevent any interruption to your service, please update your payment information as soon as possible:" +<%- updateBillingUrl %> diff --git a/libs/accounts/email-renderer/src/partials/support/en.ftl b/libs/accounts/email-renderer/src/partials/support/en.ftl new file mode 100644 index 00000000000..9f7f3e57524 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/support/en.ftl @@ -0,0 +1,7 @@ +# Variables: +# $supportUrl (String) - Link to https://support.mozilla.org/kb/im-having-problems-my-firefox-account +support-message-3 = For more info, visit { -brand-mozilla } Support. + +# Variables: +# $supportUrl (String) - Link to https://support.mozilla.org/kb/im-having-problems-my-firefox-account +support-message-plaintext = For more info, visit { -brand-mozilla } Support: { $supportUrl }. diff --git a/libs/accounts/email-renderer/src/partials/support/index.mjml b/libs/accounts/email-renderer/src/partials/support/index.mjml new file mode 100644 index 00000000000..4714d1d565e --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/support/index.mjml @@ -0,0 +1,9 @@ + + + + + For more info, visit Mozilla Support. + + + + diff --git a/libs/accounts/email-renderer/src/partials/support/index.txt b/libs/accounts/email-renderer/src/partials/support/index.txt new file mode 100644 index 00000000000..fdd7ae0e18c --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/support/index.txt @@ -0,0 +1 @@ +support-message-plaintext = "For more info, visit Mozilla Support: <%- supportUrl %>" diff --git a/libs/accounts/email-renderer/src/partials/userDevice/en.ftl b/libs/accounts/email-renderer/src/partials/userDevice/en.ftl new file mode 100644 index 00000000000..f198135e3ce --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userDevice/en.ftl @@ -0,0 +1,9 @@ +# Variables: +# $uaBrowser (String) - User's browser, e.g. Firefox +# $uaOS (String) - User's OS, e.g. Mac OSX +# $uaOSVersion (String) - User's OS version, e.g. 10.11 +device-all = { $uaBrowser } on { $uaOS } { $uaOSVersion } +# Variables: +# $uaBrowser (String) - User's browser, e.g. Firefox +# $uaOS (String) - User's OS, e.g. Mac OSX +device-browser-os = { $uaBrowser } on { $uaOS } diff --git a/libs/accounts/email-renderer/src/partials/userDevice/index.mjml b/libs/accounts/email-renderer/src/partials/userDevice/index.mjml new file mode 100644 index 00000000000..f14a93dfc22 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userDevice/index.mjml @@ -0,0 +1,19 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + <% const { uaBrowser, uaOS, uaOSVersion } = device; %> + <%# uaBrowser, uaOS, or both, must be truthy or this partial won't be included %> + <% if (uaBrowser) { %> + <% if (uaOS) { %> + <% if (uaOSVersion) { %> + <%- `${uaBrowser} on ${uaOS} ${uaOSVersion}` %> + <% } else { %> + <%- `${uaBrowser} on ${uaOS}` %> + <% } %> + <% } else { %> + <%- uaBrowser %> + <% } %> + <% } else { %> + <%- uaOS %><% if (uaOSVersion) { %> <%- uaOSVersion %><% } %> +<% } %> diff --git a/libs/accounts/email-renderer/src/partials/userDevice/index.stories.ts b/libs/accounts/email-renderer/src/partials/userDevice/index.stories.ts new file mode 100644 index 00000000000..a566e4c5b35 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userDevice/index.stories.ts @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { + MOCK_DEVICE_ALL, + MOCK_DEVICE_BROWSER, + MOCK_DEVICE_BROWSER_OS, + MOCK_DEVICE_OS, + MOCK_DEVICE_OS_VERSION, +} from './mocks'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/userInfo/userDevice', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'The userDevice partial within userInfo, with various states depending on what data is available.', + { + subject: 'N/A', + partial: 'userInfo', + layout: null, + }, + includes +); + +export const DeviceAll = createStory( + { + device: MOCK_DEVICE_ALL, + }, + 'All device details: browser, OS, and OS version' +); + +export const DeviceBrowserOs = createStory( + { + device: MOCK_DEVICE_BROWSER_OS, + }, + 'Some device details: browser and OS' +); + +export const DeviceBrowser = createStory( + { + device: MOCK_DEVICE_BROWSER, + }, + 'Some device details: browser' +); + +export const DeviceOS = createStory( + { + device: MOCK_DEVICE_OS, + }, + 'Some device details: OS' +); + +export const DeviceOSVersion = createStory( + { + device: MOCK_DEVICE_OS_VERSION, + }, + 'Some device details: OS and OS version' +); diff --git a/libs/accounts/email-renderer/src/partials/userDevice/index.txt b/libs/accounts/email-renderer/src/partials/userDevice/index.txt new file mode 100644 index 00000000000..fc72f747f90 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userDevice/index.txt @@ -0,0 +1,14 @@ +<% const { uaBrowser, uaOS, uaOSVersion } = device; %> + <% if (uaBrowser) { %> + <% if (uaOS) { %> + <% if (uaOSVersion) { %> + device-all = "<%- `${uaBrowser} on ${uaOS} ${uaOSVersion}` %>" + <% } else { %> + device-browser-os = "<%- `${uaBrowser} on ${uaOS}` %>" + <% } %> + <% } else { %> + <%- uaBrowser %> + <% } %> + <% } else { %> + <%- uaOS %><% if (uaOSVersion) { %> <%- uaOSVersion %><% } %> +<% } %> diff --git a/libs/accounts/email-renderer/src/partials/userDevice/mocks.ts b/libs/accounts/email-renderer/src/partials/userDevice/mocks.ts new file mode 100644 index 00000000000..710bb97d1d6 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userDevice/mocks.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export const MOCK_DEVICE_OS = { + uaOS: 'Mac OSX', +}; + +export const MOCK_DEVICE_OS_VERSION = { + ...MOCK_DEVICE_OS, + uaOSVersion: '10.11', +}; + +export const MOCK_DEVICE_BROWSER = { + uaBrowser: 'Firefox Nightly', +}; + +export const MOCK_DEVICE_BROWSER_OS = { + ...MOCK_DEVICE_BROWSER, + ...MOCK_DEVICE_OS, +}; + +export const MOCK_DEVICE_ALL = { + ...MOCK_DEVICE_OS_VERSION, + ...MOCK_DEVICE_BROWSER, +}; diff --git a/libs/accounts/email-renderer/src/partials/userInfo/index.mjml b/libs/accounts/email-renderer/src/partials/userInfo/index.mjml new file mode 100644 index 00000000000..09b903fd764 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userInfo/index.mjml @@ -0,0 +1,32 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + + <% if (locals.primaryEmail) { %> + <%- primaryEmail %> +
+ <% } %> + <% if (locals.device) { %> + <%- include('/partials/userDevice/index.mjml') %> +
+ <% } %> + <% if (locals.location && locals.location.country) { %> + <%- include('/partials/userLocation/index.mjml') %> +
+ <% } %> + <% if (locals.date) { %> + <%- date %> +
+ <% } %> + <% if (locals.time) { %> + <%- time %> +
+ <% } %> +
+
+
diff --git a/libs/accounts/email-renderer/src/partials/userInfo/index.scss b/libs/accounts/email-renderer/src/partials/userInfo/index.scss new file mode 100644 index 00000000000..dc05cd2fde8 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userInfo/index.scss @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@use '../../global.scss'; +@use '../../layouts/fxa/index.scss'; + +.text-body-grey-no-margin div { + @extend %text-body-common; + color: global.$grey-500 !important; +} + +.text-body-grey { + @extend .text-body-grey-no-margin; + div { + @extend .mb-6; + } +} diff --git a/libs/accounts/email-renderer/src/partials/userInfo/index.stories.ts b/libs/accounts/email-renderer/src/partials/userInfo/index.stories.ts new file mode 100644 index 00000000000..b11afa2110d --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userInfo/index.stories.ts @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { + MOCK_USER_INFO, + MOCK_USER_INFO_ALL, + MOCK_USER_INFO_TRUNCATED, +} from './mocks'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/userInfo', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'This partial displays user information in various states, depending on what we choose to show in each email template. Check "location" and "userDevice" stories for location and device data varieties.', + { + layout: null, + subject: 'N/A', + partial: 'userInfo', + }, + includes +); + +export const UserInfoAll = createStory( + { + ...MOCK_USER_INFO_ALL, + }, + 'All user info: primary email, device, location, date, and time' +); + +export const UserInfo = createStory( + { + ...MOCK_USER_INFO, + }, + 'Some user info (most commonly used): device, location, date, and time' +); + +export const UserInfoTruncated = createStory( + { + ...MOCK_USER_INFO_TRUNCATED, + }, + 'Some user info: device, location, and time' +); diff --git a/libs/accounts/email-renderer/src/partials/userInfo/index.txt b/libs/accounts/email-renderer/src/partials/userInfo/index.txt new file mode 100644 index 00000000000..33badb0b6e5 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userInfo/index.txt @@ -0,0 +1,5 @@ +<% if (locals.primaryEmail) { %><%- primaryEmail %><% } %> +<% if (locals.device) { %><% const device = include('/partials/userDevice/index.txt') %><%- device.trim() %><% } %> +<% if (locals.location && locals.location.country) { %><% const location = include('/partials/userLocation/index.txt') %><%- location.trim() %><% } %> +<% if (locals.date) { %><%- date %><% } %> +<% if (locals.time) { %><%- time %><% } %> diff --git a/libs/accounts/email-renderer/src/partials/userInfo/mocks.ts b/libs/accounts/email-renderer/src/partials/userInfo/mocks.ts new file mode 100644 index 00000000000..ef1713d8c53 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userInfo/mocks.ts @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { MOCK_LOCATION_ALL } from '../userLocation/mocks'; +import { MOCK_DEVICE_ALL } from '../userDevice/mocks'; + +export const MOCK_USER_INFO_TRUNCATED = { + device: MOCK_DEVICE_ALL, + location: MOCK_LOCATION_ALL, +}; + +export const MOCK_USER_INFO = { + date: 'Thursday, Sep 2, 2021', + ...MOCK_USER_INFO_TRUNCATED, + time: '12:26:44 AM (CEST)', +}; + +export const MOCK_USER_INFO_ALL = { + primaryEmail: 'primaryFoo@bar.com', + ...MOCK_USER_INFO, +}; diff --git a/libs/accounts/email-renderer/src/partials/userLocation/en.ftl b/libs/accounts/email-renderer/src/partials/userLocation/en.ftl new file mode 100644 index 00000000000..e539b337899 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userLocation/en.ftl @@ -0,0 +1,16 @@ +# Variables: +# $city (String) - User's city +# $stateCode (String) - User's state +# $country (String) - User's country +location-all = { $city }, { $stateCode }, { $country } (estimated) +# Variables: +# $city (String) - User's city +# $country (String) - User's country +location-city-country = { $city }, { $country } (estimated) +# Variables: +# $stateCode (String) - User's state +# $country (String) - User's country +location-state-country = { $stateCode }, { $country } (estimated) +# Variables: +# $country (stateCode) - User's country +location-country = { $country } (estimated) diff --git a/libs/accounts/email-renderer/src/partials/userLocation/index.mjml b/libs/accounts/email-renderer/src/partials/userLocation/index.mjml new file mode 100644 index 00000000000..f1ff05096dd --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userLocation/index.mjml @@ -0,0 +1,28 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<% const { city, stateCode, country } = location; %> + <% if (city) { %> + <% if (stateCode) { %> + + <%- `${city}, ${stateCode}, ${country} (estimated)` %> + + <% } else { %> + + <%- `${city}, ${country} (estimated)` %> + + <% } %> + <% } else if (stateCode) { %> + + <%- `${stateCode}, ${country} (estimated)` %> + + <% } else { %> + + <%- `${country} (estimated)` %> + +<% } %> diff --git a/libs/accounts/email-renderer/src/partials/userLocation/index.stories.ts b/libs/accounts/email-renderer/src/partials/userLocation/index.stories.ts new file mode 100644 index 00000000000..c614f72023e --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userLocation/index.stories.ts @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { MOCK_USER_INFO_TRUNCATED } from '../userInfo/mocks'; +import { + MOCK_LOCATION_ALL, + MOCK_LOCATION_CITY_COUNTRY, + MOCK_LOCATION_COUNTRY, + MOCK_LOCATION_STATE_COUNTRY, +} from './mocks'; +import { includes } from '../mocks'; + +export default { + title: 'Partials/userInfo/userLocation', +} as Meta; + +const createStory = storyWithProps( + '_storybook', + 'The location partial within userInfo, with various states depending on where the user is located and what data is available.', + { + subject: 'N/A', + partial: 'userInfo', + layout: null, + ...MOCK_USER_INFO_TRUNCATED, + }, + includes +); + +export const LocationAll = createStory( + { + location: MOCK_LOCATION_ALL, + }, + 'All location details: city, state code, and country' +); + +export const LocationDetailsCityCountry = createStory( + { + location: MOCK_LOCATION_CITY_COUNTRY, + }, + 'Some location details: city and country' +); + +export const LocationDetailsStateCountry = createStory( + { + location: MOCK_LOCATION_STATE_COUNTRY, + }, + 'Some location details: state and country' +); + +export const LocationDetailsCountry = createStory( + { + location: MOCK_LOCATION_COUNTRY, + }, + 'Basic location detail: country' +); diff --git a/libs/accounts/email-renderer/src/partials/userLocation/index.txt b/libs/accounts/email-renderer/src/partials/userLocation/index.txt new file mode 100644 index 00000000000..28388f47466 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userLocation/index.txt @@ -0,0 +1,12 @@ +<% const { city, stateCode, country } = location; %> + <% if (city) { %> + <% if (stateCode) { %> + location-all = "<%- city %>, <%- stateCode %> <%- country %> (estimated)" + <% } else { %> + location-city-country = "<%- `${city}, ${country} (estimated)` %>" + <% } %> + <% } else if (stateCode) { %> + location-state-country = "<%- `${stateCode}, ${country} (estimated)` %>" + <% } else { %> + location-country = <%- `${country} (estimated)` %> +<% } %> \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/partials/userLocation/mocks.ts b/libs/accounts/email-renderer/src/partials/userLocation/mocks.ts new file mode 100644 index 00000000000..922eb102eeb --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/userLocation/mocks.ts @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export const MOCK_LOCATION_STATE_COUNTRY = { + stateCode: 'CA', + country: 'United States', +}; + +export const MOCK_LOCATION_ALL = { + city: 'Mountain View', + ...MOCK_LOCATION_STATE_COUNTRY, +}; + +export const MOCK_LOCATION_COUNTRY = { + country: 'Spain', +}; + +export const MOCK_LOCATION_CITY_COUNTRY = { + city: 'Madrid', + ...MOCK_LOCATION_COUNTRY, +}; \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/partials/viewInvoice/en.ftl b/libs/accounts/email-renderer/src/partials/viewInvoice/en.ftl new file mode 100644 index 00000000000..ad96b662c2c --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/viewInvoice/en.ftl @@ -0,0 +1,5 @@ +view-invoice-link-action = View invoice +# Variables: +# $invoiceLink (String) - The link to the invoice +# After the colon, there's a link to https://pay.stripe.com/ +view-invoice-plaintext = View Invoice: { $invoiceLink } diff --git a/libs/accounts/email-renderer/src/partials/viewInvoice/index.mjml b/libs/accounts/email-renderer/src/partials/viewInvoice/index.mjml new file mode 100644 index 00000000000..10ba8985d30 --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/viewInvoice/index.mjml @@ -0,0 +1,7 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + View invoice + diff --git a/libs/accounts/email-renderer/src/partials/viewInvoice/index.txt b/libs/accounts/email-renderer/src/partials/viewInvoice/index.txt new file mode 100644 index 00000000000..ce0f77dc30f --- /dev/null +++ b/libs/accounts/email-renderer/src/partials/viewInvoice/index.txt @@ -0,0 +1 @@ +view-invoice-plaintext = "View Invoice: <%- invoiceLink %>" diff --git a/libs/accounts/email-renderer/src/renderer/bindings-browser.ts b/libs/accounts/email-renderer/src/renderer/bindings-browser.ts new file mode 100644 index 00000000000..b09f4ad27d4 --- /dev/null +++ b/libs/accounts/email-renderer/src/renderer/bindings-browser.ts @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// NOTE: This file handled with browser ESLint bindings +// instead of NodeJS for DOM typings support +/* eslint-env browser */ + +import { RendererBindings, RendererOpts, TemplateContext } from './bindings'; +import { transformMjIncludeTags } from '../mjml-browser-helper'; + +/** + * Unfortunately, importing the ejs package won't work from a browser context, + * so per their docs, we must use the 'official' build. Currently this is + * automically pulled in by the install-ejs.sh script and is invoked before + * storybook starts up. + */ +const ejs = require('./vendor/ejs'); +const mjml2html = require('mjml-browser'); + +/** + * Allows ejs to import requested files. This gets invoked when include() is + * used in a template. Note that this function MUST be synchronous. + */ +ejs.fileLoader = function (filePath: string) { + const request = new XMLHttpRequest(); + + // `false` makes the request synchronous + request.open('GET', './src/' + filePath, false); + request.send(null); + + if (request.status === 200) { + return request.responseText; + } + + return ''; +}; + +/** + * Bindings needed to render in story book. Can be used for browser context. + */ +export class BrowserRendererBindings extends RendererBindings { + readonly opts: RendererOpts; + + constructor(opts?: Partial) { + super(); + + // Backfill partial with defaults + this.opts = Object.assign( + { + templates: { + basePath: './src', + cssPath: './css', + }, + ejs: {}, + mjml: { + validationLevel: 'strict', + }, + translations: { + basePath: './public/locales', + }, + }, + opts + ); + } + + renderEjs( + template: string, + context: Pick< + TemplateContext, + 'acceptLanguage' | 'templateValues' | 'layout' + >, + body?: string + ) { + return ejs.render(template, { ...context, body: body }, this.opts.ejs); + } + + protected produceRootElement(html: string) { + const root = window.document.createElement('html'); + root.innerHTML = html; + return root; + } + + async fetchResource(path: string) { + const resp = await fetch(path); + return await resp.text(); + } + + protected mjml2html(mjml: string, layout: string): string { + // Work around the fact that mjml-browser doesn't support mj-include tags + mjml = transformMjIncludeTags(mjml); + // Re-render to pull in css files + mjml = this.renderEjs(mjml, { layout }); + return mjml2html(mjml, this.opts.mjml).html; + } +} diff --git a/libs/accounts/email-renderer/src/renderer/bindings-node.ts b/libs/accounts/email-renderer/src/renderer/bindings-node.ts new file mode 100644 index 00000000000..9f84e52f8cb --- /dev/null +++ b/libs/accounts/email-renderer/src/renderer/bindings-node.ts @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { readFileSync, existsSync } from 'fs'; + +import { RendererBindings, RendererOpts, TemplateContext } from './bindings'; +import ejs from 'ejs'; +import mjml2html from 'mjml'; +import { join } from 'path'; + +/** + * Represents default set of bindings for fluent localizer. Used for nodejs + * processes. + */ +export class NodeRendererBindings extends RendererBindings { + readonly opts: RendererOpts; + + constructor(opts?: Partial) { + super(); + + // Backfill options with (email) defaults + this.opts = Object.assign( + { + templates: { + basePath: join(__dirname, '..'), + cssPath: join(__dirname, '../css'), + }, + ejs: { + root: join(__dirname, '..'), + }, + mjml: { + validationLevel: 'strict', + filePath: __dirname, + // (#10018) Ignore mj-includes since we don't test template styles + // This is going to cause issues + ignoreIncludes: + typeof global === 'object' && + typeof (global as any).it === 'function', + minifyOptions: { + collapseWhitespace: true, + minifyCSS: true, + removeEmptyAttributes: true, + }, + }, + translations: { + basePath: join(__dirname, '../../public/locales'), + }, + }, + opts + ); + + // Make sure config is legit + this.validateConfig(); + } + + protected validateConfig() { + if (!existsSync(this.opts.translations.basePath)) { + throw new Error('Invalid ftl translations basePath' + this.opts.translations.basePath); + } + } + + async fetchResource(path: string): Promise { + const raw = readFileSync(path, { + encoding: 'utf8', + }); + + return raw; + } + + renderEjs(template: string, context: TemplateContext, body?: string) { + const result = ejs.render( + template, + { ...context, body: body }, + this.opts.ejs + ); + return result; + } + + protected produceRootElement(html: string) { + const { JSDOM } = require('jsdom'); + const document = new JSDOM(html).window.document; + return document.documentElement; + } + + protected mjml2html(mjml: string): string { + return mjml2html(mjml, this.opts.mjml).html; + } +} diff --git a/libs/accounts/email-renderer/src/renderer/bindings.ts b/libs/accounts/email-renderer/src/renderer/bindings.ts new file mode 100644 index 00000000000..0c1089d9a91 --- /dev/null +++ b/libs/accounts/email-renderer/src/renderer/bindings.ts @@ -0,0 +1,196 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// NOTE: This file handled with browser ESLint bindings +// instead of NodeJS for DOM typings support +/* eslint-env browser */ + +import { LocalizerOpts, ILocalizerBindings, FtlIdMsg } from '../l10n'; + +// Supporting Types +export type EjsOpts = { + root?: string; +}; +export type MjmlOpts = { + validationLevel?: 'strict' | 'soft' | 'skip'; + filePath?: string; + ignoreIncludes?: boolean; + minify?: boolean; +}; +export type TemplateOpts = { + basePath: string; + cssPath: string; +}; + +export type RenderOpts = { + templates: TemplateOpts; + ejs: EjsOpts; + mjml: MjmlOpts; +}; + +type TemplateContextValue = + | string + | Record + | number + | Date + | null + | undefined; + +// Eventually we can list all available values here, or separate them by template +export type TemplateValues = { + numberRemaining?: number; + subscriptions?: Record[]; + [key: string]: TemplateContextValue; +}; + +export type Includes = { + subject: FtlIdMsg; + action?: FtlIdMsg; + preview?: FtlIdMsg; +}; + +export type ComponentTarget = 'index' | 'strapi'; + +// TODO: better typing for 'template' with enums? from _versions.json +export interface TemplateContext { + acceptLanguage?: string; + template: string; + version: number; + layout: string; + includes: Includes; + target?: ComponentTarget; + subject?: string; + templateValues?: TemplateValues; +} + +export interface RendererContext extends TemplateContext, TemplateValues { + // cssPath is relative to where rendering occurs + cssPath: string; + subject: string; + action?: string; + preview?: string; + clientName?: string; +} + +export type EjsComponent = { + mjml: string; + text: string; +}; +export type TemplateResult = { + html: string; + text: string; + rootElement: Element; +}; +export type RendererOpts = RenderOpts & LocalizerOpts; +type ComponentType = 'templates' | 'layouts'; + +/** + * Abstraction for binding the renderer to different contexts, e.g. node vs browser. + */ +export abstract class RendererBindings implements ILocalizerBindings { + /** + * Customized options for the renderer + */ + abstract opts: RendererOpts; + + /** + * Renders a mjml template with support for fluent localization. + * @param template Name of template + * @param context Contains either values sent through mailer.send or mock values from Storybook + * @param layout Optional layout, which acts as wrapper for for template + * @returns Rendered template + */ + async renderTemplate( + template: string, + context: TemplateContext, + layout: string, + target: ComponentTarget = 'index' + ): Promise { + context = { ...context, template }; + + let component = this.renderEjsComponent( + await this.getComponent('templates', template, target), + context + ); + + // Wrap component with layout + if (layout) { + component = this.renderEjsComponent( + await this.getComponent('layouts', layout, target), + context, + component + ); + } + + const { mjml, text } = component; + const html = this.mjml2html(mjml, layout); + const rootElement = this.produceRootElement(html); + return { html, text, rootElement }; + } + + protected async getComponent( + type: ComponentType, + name: string, + target: ComponentTarget + ) { + const path = `${this.opts.templates.basePath}/${type}/${name}`; + + const [mjml, text] = await Promise.all([ + this.fetchResource(`${path}/${target}.mjml`), + this.fetchResource(`${path}/${target}.txt`), + ]); + return { mjml, text }; + } + + /** + * Renders an EJS template + * @param component Component to render + * @param context Context used to fill template variables. + * @param body Optional body to wrap + */ + protected renderEjsComponent( + component: EjsComponent, + context: TemplateContext, + body?: EjsComponent + ): EjsComponent { + const { mjml, text } = component; + return { + mjml: this.renderEjs(mjml, context, body?.mjml), + text: this.renderEjs(text, context, body?.text), + }; + } + + /** + * Fetches a resource + * @param path Path to resource + */ + abstract fetchResource(path: string): Promise; + + /** + * Renders EJS + * @param ejsTemplate Raw template to render + * @param context Context to fill template with + * @param body Optional body to wrap + * @returns Rendered EJS template + */ + abstract renderEjs( + ejsTemplate: string, + context: TemplateContext, + body?: string + ): string; + + /** + * Renders MJML into HTML + * @param mjml MJML markup + * @returns HTML + */ + protected abstract mjml2html(mjml: string, layout: string): string; + + /** + * Produces a DOM like element from an html string + * @param html HTML to parse + * @returns DOM like element + */ + protected abstract produceRootElement(html: string): Element; +} diff --git a/libs/accounts/email-renderer/src/renderer/email-link-builder.ts b/libs/accounts/email-renderer/src/renderer/email-link-builder.ts new file mode 100644 index 00000000000..34d0f0f14e5 --- /dev/null +++ b/libs/accounts/email-renderer/src/renderer/email-link-builder.ts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export class EmailLinkBuilder { + + constructor() { } + + buildPasswordChangeRequiredLink(opts: { url: string; email: string; emailToHashWith: string; }) { + const link = new URL(opts.url); + link.searchParams.set('utm_campaign', 'account-locked'); + link.searchParams.set('utm_content', 'fx-account-locked'); + link.searchParams.set('utm_medium', 'email'); + link.searchParams.set('email', opts.email); + link.searchParams.set('email_to_hash_with', opts.emailToHashWith); + return link.toString(); + } + + // TOOD: Port remaining link building logic from auth-server! +} diff --git a/libs/accounts/email-renderer/src/renderer/email-renderer.ts b/libs/accounts/email-renderer/src/renderer/email-renderer.ts new file mode 100644 index 00000000000..1e6d1897756 --- /dev/null +++ b/libs/accounts/email-renderer/src/renderer/email-renderer.ts @@ -0,0 +1,222 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { DOMLocalization, Localization } from '@fluent/dom'; +import { RendererBindings, TemplateContext, RendererContext } from './bindings'; +import Localizer, { FtlIdMsg } from '../l10n'; + +const RTL_LOCALES = [ + 'ar', + 'ckb', + 'dv', + 'he', + 'ks', + 'ps', + 'fa', + 'syr', + 'ur', + 'ug', + 'pa', +]; + +/** + * Base calss for rendering an MJML email template. + * Ported from fxa-auth-server lib/senders/emails + **/ +export class EmailRenderer extends Localizer { + /** + * Regular expression for breaking up lines of text lke KEY = VAL + **/ + protected readonly reSplitLine = /(?[a-zA-Z0-9-_]+)\s*=\s*"(?.*)?"/; + + + + protected override readonly bindings: RendererBindings; + + constructor(bindings: RendererBindings) { + super(bindings); + this.bindings = bindings; + } + + async localizeAndRender( + l10n: DOMLocalization | Localization | undefined, + string: FtlIdMsg, + context: RendererContext + ) { + // l10n will only be undefined in tests + if (!l10n) { + l10n = (await super.setupLocalizer(context.acceptLanguage || '')).l10n; + } + + const localizedString = + (await l10n.formatValue(string.id, this.flattenNestedObjects(context))) || + string.message; + return localizedString.includes('<%') + ? this.bindings.renderEjs(localizedString, context) + : localizedString; + } + + /** + * Renders and localizes an MJML/EJS email. + * @param templateContext Contains either values sent through mailer.send or mock values from Storybook + * @returns html HTML transformed from MJML that is rendered through EJS and localized + * @returns text Plaintext rendered through EJS and localized + * @returns subject Localized subject, for mailer use + */ + async renderEmail(templateContext: TemplateContext) { + const { + acceptLanguage, + template, + version, + layout, + target = 'index', + includes, + } = templateContext; + const { l10n, selectedLocale } = await super.setupDomLocalizer( + acceptLanguage || '' + ); + + const context = { + ...templateContext.templateValues, + ...templateContext, + cssPath: this.bindings.opts.templates.cssPath, + subject: '', + } as RendererContext; + + if (template !== '_storybook') { + /* + * 'Subject' and 'action' must be localized BEFORE the email is rendered because: + * 1) These values are needed in layout files and aren't easily localized, since + * `subject` goes inside `mj-title` and `action` goes in a script in `metadata.mjml` + * 2) We need to return a localized `subject` back to the mailer + */ + const { subject, action, preview } = includes; + const useCms = context.target === 'strapi'; + + const localizeAndRenderSubject = this.localizeAndRender( + l10n, + subject, + context + ); + + context.subject = useCms + ? (templateContext.subject as unknown as string) + : await localizeAndRenderSubject; + + if (action) { + const localizedAction = await this.localizeAndRender( + l10n, + action, + context + ); + context.action = localizedAction; + } + + if (!useCms && preview) { + const localizedPreview = await this.localizeAndRender( + l10n, + preview, + context + ); + context.preview = localizedPreview; + } + } + + const { text, rootElement } = await this.bindings.renderTemplate( + template || '', + context, + layout, + target + ); + + l10n.connectRoot(rootElement); + await l10n.translateRoots(); + + const isLocaleRenderedRtl = RTL_LOCALES.includes(selectedLocale); + if (isLocaleRenderedRtl) { + const body = rootElement.getElementsByTagName('body')[0]; + body.classList.add('rtl'); + } + + const localizedPlaintext = await this.localizePlaintext( + text, + context, + l10n + ); + + return { + langauge: selectedLocale, + html: rootElement.outerHTML, + text: localizedPlaintext, + subject: context.subject, + preview: context.preview || '', + template: template, + version: version + }; + } + + protected async localizePlaintext( + text: string, + context: TemplateContext | RendererContext, + l10n?: DOMLocalization | Localization + ): Promise { + if (!l10n) { + l10n = (await super.setupLocalizer(context.acceptLanguage)).l10n; + } + const ftlContext = this.flattenNestedObjects(context); + + const plainTextArr = text.split('\n'); + for (const i in plainTextArr) { + // match the lines that are of format key = "value" since we will be extracting the key + // to pass down to fluent + const { key, val } = this.splitPlainTextLine(plainTextArr[i]); + + if (key && val) { + plainTextArr[i] = (await l10n.formatValue(key, ftlContext)) || val; + } + } + // convert back to string and strip excessive line breaks + return plainTextArr.join('\n').replace(/(\n){2,}/g, '\n\n'); + } + + /** + * Takes line of text like KEY=VAL entries and splits into key val pair, + * @param plainText + * @returns { key, val } + */ + protected splitPlainTextLine(plainText: string) { + const matches = this.reSplitLine.exec(plainText); + const key = matches?.groups?.['key']; + const val = matches?.groups?.['val']; + + return { key, val }; + } + + /* + * We flatten objects coming from the mailer when localizing because Fluent expects to be passed + * a simple object containing variable names and their values, not an object containing objects + * + * NOTE: if in the future, any template value is an _array_ of objects containing strings needing + * l10n, we will need to account for those variable names differently to ensure the same EJS + * variable matches the variable name we pass to Fluent. Right now `subscriptions` containing + * `productName`s is the only array of objects and since we don't localize `productName` + * that case isn't handled since we don't need to (yet). + */ + protected flattenNestedObjects( + context: RendererContext | Record + ): Record { + const flattenedObj = {} as any; + + for (const templateVar in context) { + const varValue = context[templateVar]; + if (typeof varValue === 'object' && varValue !== null) { + Object.assign(flattenedObj, this.flattenNestedObjects(varValue)); + } else { + flattenedObj[templateVar] = varValue; + } + } + + return flattenedObj; + } +} diff --git a/libs/accounts/email-renderer/src/renderer/fxa-email-renderer.ts b/libs/accounts/email-renderer/src/renderer/fxa-email-renderer.ts new file mode 100644 index 00000000000..f7d19ccfa7f --- /dev/null +++ b/libs/accounts/email-renderer/src/renderer/fxa-email-renderer.ts @@ -0,0 +1,684 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { EmailRenderer } from './email-renderer'; + +import * as FxaLayouts from '../layouts/fxa'; +import * as AdminResetAccounts from '../templates/adminResetAccounts'; +import * as CadReminderFirst from '../templates/cadReminderFirst'; +import * as CadReminderSecond from '../templates/cadReminderSecond'; +import * as InactiveAccountFinalWarning from '../templates/inactiveAccountFinalWarning'; +import * as InactiveAccountFirstWarning from '../templates/inactiveAccountFirstWarning'; +import * as InactiveAccountSecondWarning from '../templates/inactiveAccountSecondWarning'; +import * as LowRecoveryCodes from '../templates/lowRecoveryCodes'; +import * as NewDeviceLogin from '../templates/newDeviceLogin'; +import * as PasswordChanged from '../templates/passwordChanged'; +import * as PasswordChangeRequired from '../templates/passwordChangeRequired'; +import * as PasswordForgotOtp from '../templates/passwordForgotOtp'; +import * as PasswordReset from '../templates/passwordReset'; +import * as PasswordResetAccountRecovery from '../templates/passwordResetAccountRecovery'; +import * as PasswordResetRecoveryPhone from '../templates/passwordResetRecoveryPhone'; +import * as PasswordResetWithRecoveryKeyPrompt from '../templates/passwordResetWithRecoveryKeyPrompt'; +import * as PostAddAccountRecovery from '../templates/postAddAccountRecovery'; +import * as PostAddLinkedAccount from '../templates/postAddLinkedAccount'; +import * as PostAddRecoveryPhone from '../templates/postAddRecoveryPhone'; +import * as PostAddTwoStepAuthentication from '../templates/postAddTwoStepAuthentication'; +import * as PostChangeAccountRecovery from '../templates/postChangeAccountRecovery'; +import * as PostChangePrimary from '../templates/postChangePrimary'; +import * as PostChangeRecoveryPhone from '../templates/postChangeRecoveryPhone'; +import * as PostChangeTwoStepAuthentication from '../templates/postChangeTwoStepAuthentication'; +import * as PostConsumeRecoveryCode from '../templates/postConsumeRecoveryCode'; +import * as PostNewRecoveryCodes from '../templates/postNewRecoveryCodes'; +import * as PostRemoveAccountRecovery from '../templates/postRemoveAccountRecovery'; +import * as PostRemoveRecoveryPhone from '../templates/postRemoveRecoveryPhone'; +import * as PostRemoveSecondary from '../templates/postRemoveSecondary'; +import * as PostRemoveTwoStepAuthentication from '../templates/postRemoveTwoStepAuthentication'; +import * as PostSigninRecoveryCode from '../templates/postSigninRecoveryCode'; +import * as PostSigninRecoveryPhone from '../templates/postSigninRecoveryPhone'; +import * as PostVerify from '../templates/postVerify'; +import * as PostVerifySecondary from '../templates/postVerifySecondary'; +import * as Recovery from '../templates/recovery'; +import * as UnblockCode from '../templates/unblockCode'; +import * as VerificationReminderFinal from '../templates/verificationReminderFinal'; +import * as VerificationReminderFirst from '../templates/verificationReminderFirst'; +import * as VerificationReminderSecond from '../templates/verificationReminderSecond'; +import * as Verify from '../templates/verify'; +import * as VerifyAccountChange from '../templates/verifyAccountChange'; +import * as VerifyLogin from '../templates/verifyLogin'; +import * as VerifyLoginCode from '../templates/verifyLoginCode'; +import * as VerifyPrimary from '../templates/verifyPrimary'; +import * as VerifySecondaryCode from '../templates/verifySecondaryCode'; +import * as VerifyShortCode from '../templates/verifyShortCode'; + +export class FxaEmailRenderer extends EmailRenderer { + renderAdminResetAccounts( + templateValues: AdminResetAccounts.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: AdminResetAccounts.template, + version: AdminResetAccounts.version, + layout: AdminResetAccounts.layout, + includes: AdminResetAccounts.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderCadReminderFirst( + templateValues: CadReminderFirst.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: CadReminderFirst.template, + version: CadReminderFirst.version, + layout: CadReminderFirst.layout, + includes: CadReminderFirst.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderCadReminderSecond( + templateValues: CadReminderSecond.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: CadReminderSecond.template, + version: CadReminderSecond.version, + layout: CadReminderSecond.layout, + includes: CadReminderSecond.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderInactiveAccountFinalWarning( + templateValues: InactiveAccountFinalWarning.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: InactiveAccountFinalWarning.template, + version: InactiveAccountFinalWarning.version, + layout: InactiveAccountFinalWarning.layout, + includes: InactiveAccountFinalWarning.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderInactiveAccountFirstWarning( + templateValues: InactiveAccountFirstWarning.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: InactiveAccountFirstWarning.template, + version: InactiveAccountFirstWarning.version, + layout: InactiveAccountFirstWarning.layout, + includes: InactiveAccountFirstWarning.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderInactiveAccountSecondWarning( + templateValues: InactiveAccountSecondWarning.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: InactiveAccountSecondWarning.template, + version: InactiveAccountSecondWarning.version, + layout: InactiveAccountSecondWarning.layout, + includes: InactiveAccountSecondWarning.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderLowRecoveryCodes( + templateValues: LowRecoveryCodes.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: LowRecoveryCodes.template, + version: LowRecoveryCodes.version, + layout: LowRecoveryCodes.layout, + includes: LowRecoveryCodes.getIncludes(templateValues), + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderNewDeviceLogin( + templateValues: NewDeviceLogin.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: NewDeviceLogin.template, + version: NewDeviceLogin.version, + layout: NewDeviceLogin.layout, + includes: NewDeviceLogin.getIncludes(templateValues), + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPasswordChanged( + templateValues: PasswordChanged.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PasswordChanged.template, + version: PasswordChanged.version, + layout: PasswordChanged.layout, + includes: PasswordChanged.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPasswordChangeRequired( + templateValues: PasswordChangeRequired.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PasswordChangeRequired.template, + version: PasswordChangeRequired.version, + layout: PasswordChangeRequired.layout, + includes: PasswordChangeRequired.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPasswordForgotOtp( + templateValues: PasswordForgotOtp.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PasswordForgotOtp.template, + version: PasswordForgotOtp.version, + layout: PasswordForgotOtp.layout, + includes: PasswordForgotOtp.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPasswordReset( + templateValues: PasswordReset.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PasswordReset.template, + version: PasswordReset.version, + layout: PasswordReset.layout, + includes: PasswordReset.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPasswordResetAccountRecovery( + templateValues: PasswordResetAccountRecovery.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PasswordResetAccountRecovery.template, + version: PasswordResetAccountRecovery.version, + layout: PasswordResetAccountRecovery.layout, + includes: PasswordResetAccountRecovery.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPasswordResetRecoveryPhone( + templateValues: PasswordResetRecoveryPhone.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PasswordResetRecoveryPhone.template, + version: PasswordResetRecoveryPhone.version, + layout: PasswordResetRecoveryPhone.layout, + includes: PasswordResetRecoveryPhone.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPasswordResetWithRecoveryKeyPrompt( + templateValues: PasswordResetWithRecoveryKeyPrompt.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PasswordResetWithRecoveryKeyPrompt.template, + version: PasswordResetWithRecoveryKeyPrompt.version, + layout: PasswordResetWithRecoveryKeyPrompt.layout, + includes: PasswordResetWithRecoveryKeyPrompt.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostAddAccountRecovery( + templateValues: PostAddAccountRecovery.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostAddAccountRecovery.template, + version: PostAddAccountRecovery.version, + layout: PostAddAccountRecovery.layout, + includes: PostAddAccountRecovery.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostAddLinkedAccount( + templateValues: PostAddLinkedAccount.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostAddLinkedAccount.template, + version: PostAddLinkedAccount.version, + layout: PostAddLinkedAccount.layout, + includes: PostAddLinkedAccount.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostAddRecoveryPhone( + templateValues: PostAddRecoveryPhone.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostAddRecoveryPhone.template, + version: PostAddRecoveryPhone.version, + layout: PostAddRecoveryPhone.layout, + includes: PostAddRecoveryPhone.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostAddTwoStepAuthentication( + templateValues: PostAddTwoStepAuthentication.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostAddTwoStepAuthentication.template, + version: PostAddTwoStepAuthentication.version, + layout: PostAddTwoStepAuthentication.layout, + includes: PostAddTwoStepAuthentication.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostChangeAccountRecovery( + templateValues: PostChangeAccountRecovery.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostChangeAccountRecovery.template, + version: PostChangeAccountRecovery.version, + layout: PostChangeAccountRecovery.layout, + includes: PostChangeAccountRecovery.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostChangePrimary( + templateValues: PostChangePrimary.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostChangePrimary.template, + version: PostChangePrimary.version, + layout: PostChangePrimary.layout, + includes: PostChangePrimary.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostChangeRecoveryPhone( + templateValues: PostChangeRecoveryPhone.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostChangeRecoveryPhone.template, + version: PostChangeRecoveryPhone.version, + layout: PostChangeRecoveryPhone.layout, + includes: PostChangeRecoveryPhone.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostChangeTwoStepAuthentication( + templateValues: PostChangeTwoStepAuthentication.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostChangeTwoStepAuthentication.template, + version: PostChangeTwoStepAuthentication.version, + layout: PostChangeTwoStepAuthentication.layout, + includes: PostChangeTwoStepAuthentication.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostConsumeRecoveryCode( + templateValues: PostConsumeRecoveryCode.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostConsumeRecoveryCode.template, + version: PostConsumeRecoveryCode.version, + layout: PostConsumeRecoveryCode.layout, + includes: PostConsumeRecoveryCode.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostNewRecoveryCodes( + templateValues: PostNewRecoveryCodes.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostNewRecoveryCodes.template, + version: PostNewRecoveryCodes.version, + layout: PostNewRecoveryCodes.layout, + includes: PostNewRecoveryCodes.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostRemoveAccountRecovery( + templateValues: PostRemoveAccountRecovery.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostRemoveAccountRecovery.template, + version: PostRemoveAccountRecovery.version, + layout: PostRemoveAccountRecovery.layout, + includes: PostRemoveAccountRecovery.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostRemoveRecoveryPhone( + templateValues: PostRemoveRecoveryPhone.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostRemoveRecoveryPhone.template, + version: PostRemoveRecoveryPhone.version, + layout: PostRemoveRecoveryPhone.layout, + includes: PostRemoveRecoveryPhone.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostRemoveSecondary( + templateValues: PostRemoveSecondary.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostRemoveSecondary.template, + version: PostRemoveSecondary.version, + layout: PostRemoveSecondary.layout, + includes: PostRemoveSecondary.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostRemoveTwoStepAuthentication( + templateValues: PostRemoveTwoStepAuthentication.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostRemoveTwoStepAuthentication.template, + version: PostRemoveTwoStepAuthentication.version, + layout: PostRemoveTwoStepAuthentication.layout, + includes: PostRemoveTwoStepAuthentication.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostSigninRecoveryCode( + templateValues: PostSigninRecoveryCode.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostSigninRecoveryCode.template, + version: PostSigninRecoveryCode.version, + layout: PostSigninRecoveryCode.layout, + includes: PostSigninRecoveryCode.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostSigninRecoveryPhone( + templateValues: PostSigninRecoveryPhone.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostSigninRecoveryPhone.template, + version: PostSigninRecoveryPhone.version, + layout: PostSigninRecoveryPhone.layout, + includes: PostSigninRecoveryPhone.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostVerify( + templateValues: PostVerify.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostVerify.template, + version: PostVerify.version, + layout: PostVerify.layout, + includes: PostVerify.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderPostVerifySecondary( + templateValues: PostVerifySecondary.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: PostVerifySecondary.template, + version: PostVerifySecondary.version, + layout: PostVerifySecondary.layout, + includes: PostVerifySecondary.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderRecovery( + templateValues: Recovery.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: Recovery.template, + version: Recovery.version, + layout: Recovery.layout, + includes: Recovery.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderUnblockCode( + templateValues: UnblockCode.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: UnblockCode.template, + version: UnblockCode.version, + layout: UnblockCode.layout, + includes: UnblockCode.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerificationReminderFinal( + templateValues: VerificationReminderFinal.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: VerificationReminderFinal.template, + version: VerificationReminderFinal.version, + layout: VerificationReminderFinal.layout, + includes: VerificationReminderFinal.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerificationReminderFirst( + templateValues: VerificationReminderFirst.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: VerificationReminderFirst.template, + version: VerificationReminderFirst.version, + layout: VerificationReminderFirst.layout, + includes: VerificationReminderFirst.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerificationReminderSecond( + templateValues: VerificationReminderSecond.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: VerificationReminderSecond.template, + version: VerificationReminderSecond.version, + layout: VerificationReminderSecond.layout, + includes: VerificationReminderSecond.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerify( + templateValues: Verify.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: Verify.template, + version: Verify.version, + layout: Verify.layout, + includes: Verify.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerifyAccountChange( + templateValues: VerifyAccountChange.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: VerifyAccountChange.template, + version: VerifyAccountChange.version, + layout: VerifyAccountChange.layout, + includes: VerifyAccountChange.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerifyLogin( + templateValues: VerifyLogin.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: VerifyLogin.template, + version: VerifyLogin.version, + layout: VerifyLogin.layout, + includes: VerifyLogin.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerifyLoginCode( + templateValues: VerifyLoginCode.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: VerifyLoginCode.template, + version: VerifyLoginCode.version, + layout: VerifyLoginCode.layout, + includes: VerifyLoginCode.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerifyPrimary( + templateValues: VerifyPrimary.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: VerifyPrimary.template, + version: VerifyPrimary.version, + layout: VerifyPrimary.layout, + includes: VerifyPrimary.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerifySecondaryCode( + templateValues: VerifySecondaryCode.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: VerifySecondaryCode.template, + version: VerifySecondaryCode.version, + layout: VerifySecondaryCode.layout, + includes: VerifySecondaryCode.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderVerifyShortCode( + templateValues: VerifyShortCode.TemplateData, + layoutTemplateValues: FxaLayouts.TemplateData + ) { + return this.renderEmail({ + template: VerifyShortCode.template, + version: VerifyShortCode.version, + layout: VerifyShortCode.layout, + includes: VerifyShortCode.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } +} diff --git a/libs/accounts/email-renderer/src/renderer/index.ts b/libs/accounts/email-renderer/src/renderer/index.ts new file mode 100644 index 00000000000..2993382fa24 --- /dev/null +++ b/libs/accounts/email-renderer/src/renderer/index.ts @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export * from './email-renderer'; +export * from './fxa-email-renderer'; +export * from './subplat-email-renderer'; +export * from './email-link-builder'; + +// Important! do not export ./bindings-node. +// Doing so will break storybook, since this file cannot be processed +// in a web browser context. diff --git a/libs/accounts/email-renderer/src/renderer/subplat-email-renderer.ts b/libs/accounts/email-renderer/src/renderer/subplat-email-renderer.ts new file mode 100644 index 00000000000..40f54b640c7 --- /dev/null +++ b/libs/accounts/email-renderer/src/renderer/subplat-email-renderer.ts @@ -0,0 +1,304 @@ +import { EmailRenderer } from './email-renderer'; +import * as SubscriptionLayouts from '../layouts/subscription'; +import * as DownloadSubscription from '../templates/downloadSubscription'; +import * as FraudulentAccountDeletion from '../templates/fraudulentAccountDeletion'; +import * as SubscriptionAccountDeletion from '../templates/subscriptionAccountDeletion'; +import * as SubscriptionAccountFinishSetup from '../templates/subscriptionAccountFinishSetup'; +import * as SubscriptionAccountReminderFirst from '../templates/subscriptionAccountReminderFirst'; +import * as SubscriptionAccountReminderSecond from '../templates/subscriptionAccountReminderSecond'; +import * as SubscriptionCancellation from '../templates/subscriptionCancellation'; +import * as SubscriptionDowngrade from '../templates/subscriptionDowngrade'; +import * as SubscriptionFailedPaymentsCancellation from '../templates/subscriptionFailedPaymentsCancellation'; +import * as SubscriptionFirstInvoice from '../templates/subscriptionFirstInvoice'; +import * as SubscriptionPaymentExpired from '../templates/subscriptionPaymentExpired'; +import * as SubscriptionPaymentFailed from '../templates/subscriptionPaymentFailed'; +import * as SubscriptionPaymentProviderCancelled from '../templates/subscriptionPaymentProviderCancelled'; +import * as SubscriptionReactivation from '../templates/subscriptionReactivation'; +import * as SubscriptionRenewalReminder from '../templates/subscriptionRenewalReminder'; +import * as SubscriptionReplaced from '../templates/subscriptionReplaced'; +import * as SubscriptionsPaymentExpired from '../templates/subscriptionsPaymentExpired'; +import * as SubscriptionsPaymentProviderCancelled from '../templates/subscriptionsPaymentProviderCancelled'; +import * as SubscriptionSubsequentInvoice from '../templates/subscriptionSubsequentInvoice'; +import * as SubscriptionUpgrade from '../templates/subscriptionUpgrade'; + +export class SubplatEmailRender extends EmailRenderer { + async renderDownloadSubscription( + templateValues: DownloadSubscription.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: DownloadSubscription.template, + version: DownloadSubscription.version, + layout: DownloadSubscription.layout, + includes: DownloadSubscription.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderFraudulentAccountDeletion( + templateValues: FraudulentAccountDeletion.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: FraudulentAccountDeletion.template, + version: FraudulentAccountDeletion.version, + layout: FraudulentAccountDeletion.layout, + includes: FraudulentAccountDeletion.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionAccountDeletion( + templateValues: SubscriptionAccountDeletion.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionAccountDeletion.template, + version: SubscriptionAccountDeletion.version, + layout: SubscriptionAccountDeletion.layout, + includes: SubscriptionAccountDeletion.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionAccountFinishSetup( + templateValues: SubscriptionAccountFinishSetup.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionAccountFinishSetup.template, + version: SubscriptionAccountFinishSetup.version, + layout: SubscriptionAccountFinishSetup.layout, + includes: SubscriptionAccountFinishSetup.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionAccountReminderFirst( + templateValues: SubscriptionAccountReminderFirst.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionAccountReminderFirst.template, + version: SubscriptionAccountReminderFirst.version, + layout: SubscriptionAccountReminderFirst.layout, + includes: SubscriptionAccountReminderFirst.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionAccountReminderSecond( + templateValues: SubscriptionAccountReminderSecond.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionAccountReminderSecond.template, + version: SubscriptionAccountReminderSecond.version, + layout: SubscriptionAccountReminderSecond.layout, + includes: SubscriptionAccountReminderSecond.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionCancellation( + templateValues: SubscriptionCancellation.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionCancellation.template, + version: SubscriptionCancellation.version, + layout: SubscriptionCancellation.layout, + includes: SubscriptionCancellation.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionDowngrade( + templateValues: SubscriptionDowngrade.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionDowngrade.template, + version: SubscriptionDowngrade.version, + layout: SubscriptionDowngrade.layout, + includes: SubscriptionDowngrade.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionFailedPaymentsCancellation( + templateValues: SubscriptionFailedPaymentsCancellation.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionFailedPaymentsCancellation.template, + version: SubscriptionFailedPaymentsCancellation.version, + layout: SubscriptionFailedPaymentsCancellation.layout, + includes: SubscriptionFailedPaymentsCancellation.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionFirstInvoice( + templateValues: SubscriptionFirstInvoice.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionFirstInvoice.template, + version: SubscriptionFirstInvoice.version, + layout: SubscriptionFirstInvoice.layout, + includes: SubscriptionFirstInvoice.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionPaymentExpired( + templateValues: SubscriptionPaymentExpired.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionPaymentExpired.template, + version: SubscriptionPaymentExpired.version, + layout: SubscriptionPaymentExpired.layout, + includes: SubscriptionPaymentExpired.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionPaymentFailed( + templateValues: SubscriptionPaymentFailed.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionPaymentFailed.template, + version: SubscriptionPaymentFailed.version, + layout: SubscriptionPaymentFailed.layout, + includes: SubscriptionPaymentFailed.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionPaymentProviderCancelled( + templateValues: SubscriptionPaymentProviderCancelled.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionPaymentProviderCancelled.template, + version: SubscriptionPaymentProviderCancelled.version, + layout: SubscriptionPaymentProviderCancelled.layout, + includes: SubscriptionPaymentProviderCancelled.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionReactivation( + templateValues: SubscriptionReactivation.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionReactivation.template, + version: SubscriptionReactivation.version, + layout: SubscriptionReactivation.layout, + includes: SubscriptionReactivation.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionRenewalReminder( + templateValues: SubscriptionRenewalReminder.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionRenewalReminder.template, + version: SubscriptionRenewalReminder.version, + layout: SubscriptionRenewalReminder.layout, + includes: SubscriptionRenewalReminder.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionReplaced( + templateValues: SubscriptionReplaced.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionReplaced.template, + version: SubscriptionReplaced.version, + layout: SubscriptionReplaced.layout, + includes: SubscriptionReplaced.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionsPaymentExpired( + templateValues: SubscriptionsPaymentExpired.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionsPaymentExpired.template, + version: SubscriptionsPaymentExpired.version, + layout: SubscriptionsPaymentExpired.layout, + includes: SubscriptionsPaymentExpired.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionsPaymentProviderCancelled( + templateValues: SubscriptionsPaymentProviderCancelled.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionsPaymentProviderCancelled.template, + version: SubscriptionsPaymentProviderCancelled. version, + layout: SubscriptionsPaymentProviderCancelled.layout, + includes: SubscriptionsPaymentProviderCancelled.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionSubsequentInvoice( + templateValues: SubscriptionSubsequentInvoice.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionSubsequentInvoice.template, + version: SubscriptionSubsequentInvoice.version, + layout: SubscriptionSubsequentInvoice.layout, + includes: SubscriptionSubsequentInvoice.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } + + async renderSubscriptionUpgrade( + templateValues: SubscriptionUpgrade.TemplateData, + layoutTemplateValues: SubscriptionLayouts.TemplateData + ) { + return this.renderEmail({ + template: SubscriptionUpgrade.template, + version: SubscriptionUpgrade.version, + layout: SubscriptionUpgrade.layout, + includes: SubscriptionUpgrade.includes, + ...templateValues, + ...layoutTemplateValues, + }); + } +} diff --git a/libs/accounts/email-renderer/src/renderer/vendor/ejs.js b/libs/accounts/email-renderer/src/renderer/vendor/ejs.js new file mode 100644 index 00000000000..b58d8ae9b53 --- /dev/null +++ b/libs/accounts/email-renderer/src/renderer/vendor/ejs.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ejs=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i1;if(options.cache){if(!filename){throw new Error("cache option requires a filename")}func=exports.cache.get(filename);if(func){return func}if(!hasTemplate){template=fileLoader(filename).toString().replace(_BOM,"")}}else if(!hasTemplate){if(!filename){throw new Error("Internal EJS error: no file name or template "+"provided")}template=fileLoader(filename).toString().replace(_BOM,"")}func=exports.compile(template,options);if(options.cache){exports.cache.set(filename,func)}return func}function tryHandleCache(options,data,cb){var result;if(!cb){if(typeof exports.promiseImpl=="function"){return new exports.promiseImpl(function(resolve,reject){try{result=handleCache(options)(data);resolve(result)}catch(err){reject(err)}})}else{throw new Error("Please provide a callback function")}}else{try{result=handleCache(options)(data)}catch(err){return cb(err)}cb(null,result)}}function fileLoader(filePath){return exports.fileLoader(filePath)}function includeFile(path,options){var opts=utils.shallowCopy(utils.createNullProtoObjWherePossible(),options);opts.filename=getIncludePath(path,opts);if(typeof options.includer==="function"){var includerResult=options.includer(path,opts.filename);if(includerResult){if(includerResult.filename){opts.filename=includerResult.filename}if(includerResult.template){return handleCache(opts,includerResult.template)}}}return handleCache(opts)}function rethrow(err,str,flnm,lineno,esc){var lines=str.split("\n");var start=Math.max(lineno-3,0);var end=Math.min(lines.length,lineno+3);var filename=esc(flnm);var context=lines.slice(start,end).map(function(line,i){var curr=i+start+1;return(curr==lineno?" >> ":" ")+curr+"| "+line}).join("\n");err.path=filename;err.message=(filename||"ejs")+":"+lineno+"\n"+context+"\n\n"+err.message;throw err}function stripSemi(str){return str.replace(/;(\s*$)/,"$1")}exports.compile=function compile(template,opts){var templ;if(opts&&opts.scope){if(!scopeOptionWarned){console.warn("`scope` option is deprecated and will be removed in EJS 3");scopeOptionWarned=true}if(!opts.context){opts.context=opts.scope}delete opts.scope}templ=new Template(template,opts);return templ.compile()};exports.render=function(template,d,o){var data=d||utils.createNullProtoObjWherePossible();var opts=o||utils.createNullProtoObjWherePossible();if(arguments.length==2){utils.shallowCopyFromList(opts,data,_OPTS_PASSABLE_WITH_DATA)}return handleCache(opts,template)(data)};exports.renderFile=function(){var args=Array.prototype.slice.call(arguments);var filename=args.shift();var cb;var opts={filename:filename};var data;var viewOpts;if(typeof arguments[arguments.length-1]=="function"){cb=args.pop()}if(args.length){data=args.shift();if(args.length){utils.shallowCopy(opts,args.pop())}else{if(data.settings){if(data.settings.views){opts.views=data.settings.views}if(data.settings["view cache"]){opts.cache=true}viewOpts=data.settings["view options"];if(viewOpts){utils.shallowCopy(opts,viewOpts)}}utils.shallowCopyFromList(opts,data,_OPTS_PASSABLE_WITH_DATA_EXPRESS)}opts.filename=filename}else{data=utils.createNullProtoObjWherePossible()}return tryHandleCache(opts,data,cb)};exports.Template=Template;exports.clearCache=function(){exports.cache.reset()};function Template(text,optsParam){var opts=utils.hasOwnOnlyObject(optsParam);var options=utils.createNullProtoObjWherePossible();this.templateText=text;this.mode=null;this.truncate=false;this.currentLine=1;this.source="";options.client=opts.client||false;options.escapeFunction=opts.escape||opts.escapeFunction||utils.escapeXML;options.compileDebug=opts.compileDebug!==false;options.debug=!!opts.debug;options.filename=opts.filename;options.openDelimiter=opts.openDelimiter||exports.openDelimiter||_DEFAULT_OPEN_DELIMITER;options.closeDelimiter=opts.closeDelimiter||exports.closeDelimiter||_DEFAULT_CLOSE_DELIMITER;options.delimiter=opts.delimiter||exports.delimiter||_DEFAULT_DELIMITER;options.strict=opts.strict||false;options.context=opts.context;options.cache=opts.cache||false;options.rmWhitespace=opts.rmWhitespace;options.root=opts.root;options.includer=opts.includer;options.outputFunctionName=opts.outputFunctionName;options.localsName=opts.localsName||exports.localsName||_DEFAULT_LOCALS_NAME;options.views=opts.views;options.async=opts.async;options.destructuredLocals=opts.destructuredLocals;options.legacyInclude=typeof opts.legacyInclude!="undefined"?!!opts.legacyInclude:true;if(options.strict){options._with=false}else{options._with=typeof opts._with!="undefined"?opts._with:true}this.opts=options;this.regex=this.createRegex()}Template.modes={EVAL:"eval",ESCAPED:"escaped",RAW:"raw",COMMENT:"comment",LITERAL:"literal"};Template.prototype={createRegex:function(){var str=_REGEX_STRING;var delim=utils.escapeRegExpChars(this.opts.delimiter);var open=utils.escapeRegExpChars(this.opts.openDelimiter);var close=utils.escapeRegExpChars(this.opts.closeDelimiter);str=str.replace(/%/g,delim).replace(//g,close);return new RegExp(str)},compile:function(){var src;var fn;var opts=this.opts;var prepended="";var appended="";var escapeFn=opts.escapeFunction;var ctor;var sanitizedFilename=opts.filename?JSON.stringify(opts.filename):"undefined";if(!this.source){this.generateSource();prepended+=' var __output = "";\n'+" function __append(s) { if (s !== undefined && s !== null) __output += s }\n";if(opts.outputFunctionName){if(!_JS_IDENTIFIER.test(opts.outputFunctionName)){throw new Error("outputFunctionName is not a valid JS identifier.")}prepended+=" var "+opts.outputFunctionName+" = __append;"+"\n"}if(opts.localsName&&!_JS_IDENTIFIER.test(opts.localsName)){throw new Error("localsName is not a valid JS identifier.")}if(opts.destructuredLocals&&opts.destructuredLocals.length){var destructuring=" var __locals = ("+opts.localsName+" || {}),\n";for(var i=0;i0){destructuring+=",\n "}destructuring+=name+" = __locals."+name}prepended+=destructuring+";\n"}if(opts._with!==false){prepended+=" with ("+opts.localsName+" || {}) {"+"\n";appended+=" }"+"\n"}appended+=" return __output;"+"\n";this.source=prepended+this.source+appended}if(opts.compileDebug){src="var __line = 1"+"\n"+" , __lines = "+JSON.stringify(this.templateText)+"\n"+" , __filename = "+sanitizedFilename+";"+"\n"+"try {"+"\n"+this.source+"} catch (e) {"+"\n"+" rethrow(e, __lines, __filename, __line, escapeFn);"+"\n"+"}"+"\n"}else{src=this.source}if(opts.client){src="escapeFn = escapeFn || "+escapeFn.toString()+";"+"\n"+src;if(opts.compileDebug){src="rethrow = rethrow || "+rethrow.toString()+";"+"\n"+src}}if(opts.strict){src='"use strict";\n'+src}if(opts.debug){console.log(src)}if(opts.compileDebug&&opts.filename){src=src+"\n"+"//# sourceURL="+sanitizedFilename+"\n"}try{if(opts.async){try{ctor=new Function("return (async function(){}).constructor;")()}catch(e){if(e instanceof SyntaxError){throw new Error("This environment does not support async/await")}else{throw e}}}else{ctor=Function}fn=new ctor(opts.localsName+", escapeFn, include, rethrow",src)}catch(e){if(e instanceof SyntaxError){if(opts.filename){e.message+=" in "+opts.filename}e.message+=" while compiling ejs\n\n";e.message+="If the above error is not helpful, you may want to try EJS-Lint:\n";e.message+="https://github.com/RyanZim/EJS-Lint";if(!opts.async){e.message+="\n";e.message+="Or, if you meant to create an async function, pass `async: true` as an option."}}throw e}var returnedFn=opts.client?fn:function anonymous(data){var include=function(path,includeData){var d=utils.shallowCopy(utils.createNullProtoObjWherePossible(),data);if(includeData){d=utils.shallowCopy(d,includeData)}return includeFile(path,opts)(d)};return fn.apply(opts.context,[data||utils.createNullProtoObjWherePossible(),escapeFn,include,rethrow])};if(opts.filename&&typeof Object.defineProperty==="function"){var filename=opts.filename;var basename=path.basename(filename,path.extname(filename));try{Object.defineProperty(returnedFn,"name",{value:basename,writable:false,enumerable:false,configurable:true})}catch(e){}}return returnedFn},generateSource:function(){var opts=this.opts;if(opts.rmWhitespace){this.templateText=this.templateText.replace(/[\r\n]+/g,"\n").replace(/^\s+|\s+$/gm,"")}this.templateText=this.templateText.replace(/[ \t]*<%_/gm,"<%_").replace(/_%>[ \t]*/gm,"_%>");var self=this;var matches=this.parseTemplateText();var d=this.opts.delimiter;var o=this.opts.openDelimiter;var c=this.opts.closeDelimiter;if(matches&&matches.length){matches.forEach(function(line,index){var closing;if(line.indexOf(o+d)===0&&line.indexOf(o+d+d)!==0){closing=matches[index+2];if(!(closing==d+c||closing=="-"+d+c||closing=="_"+d+c)){throw new Error('Could not find matching close tag for "'+line+'".')}}self.scanLine(line)})}},parseTemplateText:function(){var str=this.templateText;var pat=this.regex;var result=pat.exec(str);var arr=[];var firstPos;while(result){firstPos=result.index;if(firstPos!==0){arr.push(str.substring(0,firstPos));str=str.slice(firstPos)}arr.push(result[0]);str=str.slice(result[0].length);result=pat.exec(str)}if(str){arr.push(str)}return arr},_addOutput:function(line){if(this.truncate){line=line.replace(/^(?:\r\n|\r|\n)/,"");this.truncate=false}if(!line){return line}line=line.replace(/\\/g,"\\\\");line=line.replace(/\n/g,"\\n");line=line.replace(/\r/g,"\\r");line=line.replace(/"/g,'\\"');this.source+=' ; __append("'+line+'")'+"\n"},scanLine:function(line){var self=this;var d=this.opts.delimiter;var o=this.opts.openDelimiter;var c=this.opts.closeDelimiter;var newLineCount=0;newLineCount=line.split("\n").length-1;switch(line){case o+d:case o+d+"_":this.mode=Template.modes.EVAL;break;case o+d+"=":this.mode=Template.modes.ESCAPED;break;case o+d+"-":this.mode=Template.modes.RAW;break;case o+d+"#":this.mode=Template.modes.COMMENT;break;case o+d+d:this.mode=Template.modes.LITERAL;this.source+=' ; __append("'+line.replace(o+d+d,o+d)+'")'+"\n";break;case d+d+c:this.mode=Template.modes.LITERAL;this.source+=' ; __append("'+line.replace(d+d+c,d+c)+'")'+"\n";break;case d+c:case"-"+d+c:case"_"+d+c:if(this.mode==Template.modes.LITERAL){this._addOutput(line)}this.mode=null;this.truncate=line.indexOf("-")===0||line.indexOf("_")===0;break;default:if(this.mode){switch(this.mode){case Template.modes.EVAL:case Template.modes.ESCAPED:case Template.modes.RAW:if(line.lastIndexOf("//")>line.lastIndexOf("\n")){line+="\n"}}switch(this.mode){case Template.modes.EVAL:this.source+=" ; "+line+"\n";break;case Template.modes.ESCAPED:this.source+=" ; __append(escapeFn("+stripSemi(line)+"))"+"\n";break;case Template.modes.RAW:this.source+=" ; __append("+stripSemi(line)+")"+"\n";break;case Template.modes.COMMENT:break;case Template.modes.LITERAL:this._addOutput(line);break}}else{this._addOutput(line)}}if(self.opts.compileDebug&&newLineCount){this.currentLine+=newLineCount;this.source+=" ; __line = "+this.currentLine+"\n"}}};exports.escapeXML=utils.escapeXML;exports.__express=exports.renderFile;exports.VERSION=_VERSION_STRING;exports.name=_NAME;if(typeof window!="undefined"){window.ejs=exports}},{"../package.json":6,"./utils":2,fs:3,path:4}],2:[function(require,module,exports){"use strict";var regExpChars=/[|\\{}()[\]^$+*?.]/g;var hasOwnProperty=Object.prototype.hasOwnProperty;var hasOwn=function(obj,key){return hasOwnProperty.apply(obj,[key])};exports.escapeRegExpChars=function(string){if(!string){return""}return String(string).replace(regExpChars,"\\$&")};var _ENCODE_HTML_RULES={"&":"&","<":"<",">":">",'"':""","'":"'"};var _MATCH_HTML=/[&<>'"]/g;function encode_char(c){return _ENCODE_HTML_RULES[c]||c}var escapeFuncStr="var _ENCODE_HTML_RULES = {\n"+' "&": "&"\n'+' , "<": "<"\n'+' , ">": ">"\n'+' , \'"\': """\n'+' , "\'": "'"\n'+" }\n"+" , _MATCH_HTML = /[&<>'\"]/g;\n"+"function encode_char(c) {\n"+" return _ENCODE_HTML_RULES[c] || c;\n"+"};\n";exports.escapeXML=function(markup){return markup==undefined?"":String(markup).replace(_MATCH_HTML,encode_char)};function escapeXMLToString(){return Function.prototype.toString.call(this)+";\n"+escapeFuncStr}try{if(typeof Object.defineProperty==="function"){Object.defineProperty(exports.escapeXML,"toString",{value:escapeXMLToString})}else{exports.escapeXML.toString=escapeXMLToString}}catch(err){console.warn("Unable to set escapeXML.toString (is the Function prototype frozen?)")}exports.shallowCopy=function(to,from){from=from||{};if(to!==null&&to!==undefined){for(var p in from){if(!hasOwn(from,p)){continue}if(p==="__proto__"||p==="constructor"){continue}to[p]=from[p]}}return to};exports.shallowCopyFromList=function(to,from,list){list=list||[];from=from||{};if(to!==null&&to!==undefined){for(var i=0;i=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up--;up){parts.unshift("..")}}return parts}exports.resolve=function(){var resolvedPath="",resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?arguments[i]:process.cwd();if(typeof path!=="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){continue}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=path.charAt(0)==="/"}resolvedPath=normalizeArray(filter(resolvedPath.split("/"),function(p){return!!p}),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."};exports.normalize=function(path){var isAbsolute=exports.isAbsolute(path),trailingSlash=substr(path,-1)==="/";path=normalizeArray(filter(path.split("/"),function(p){return!!p}),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path};exports.isAbsolute=function(path){return path.charAt(0)==="/"};exports.join=function(){var paths=Array.prototype.slice.call(arguments,0);return exports.normalize(filter(paths,function(p,index){if(typeof p!=="string"){throw new TypeError("Arguments to path.join must be strings")}return p}).join("/"))};exports.relative=function(from,to){from=exports.resolve(from).substr(1);to=exports.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i=1;--i){code=path.charCodeAt(i);if(code===47){if(!matchedSlash){end=i;break}}else{matchedSlash=false}}if(end===-1)return hasRoot?"/":".";if(hasRoot&&end===1){return"/"}return path.slice(0,end)};function basename(path){if(typeof path!=="string")path=path+"";var start=0;var end=-1;var matchedSlash=true;var i;for(i=path.length-1;i>=0;--i){if(path.charCodeAt(i)===47){if(!matchedSlash){start=i+1;break}}else if(end===-1){matchedSlash=false;end=i+1}}if(end===-1)return"";return path.slice(start,end)}exports.basename=function(path,ext){var f=basename(path);if(ext&&f.substr(-1*ext.length)===ext){f=f.substr(0,f.length-ext.length)}return f};exports.extname=function(path){if(typeof path!=="string")path=path+"";var startDot=-1;var startPart=0;var end=-1;var matchedSlash=true;var preDotState=0;for(var i=path.length-1;i>=0;--i){var code=path.charCodeAt(i);if(code===47){if(!matchedSlash){startPart=i+1;break}continue}if(end===-1){matchedSlash=false;end=i+1}if(code===46){if(startDot===-1)startDot=i;else if(preDotState!==1)preDotState=1}else if(startDot!==-1){preDotState=-1}}if(startDot===-1||end===-1||preDotState===0||preDotState===1&&startDot===end-1&&startDot===startPart+1){return""}return path.slice(startDot,end)};function filter(xs,f){if(xs.filter)return xs.filter(f);var res=[];for(var i=0;i1){for(var i=1;i (http://fleegix.org)",license:"Apache-2.0",bin:{ejs:"./bin/cli.js"},main:"./lib/ejs.js",jsdelivr:"ejs.min.js",unpkg:"ejs.min.js",repository:{type:"git",url:"git://github.com/mde/ejs.git"},bugs:"https://github.com/mde/ejs/issues",homepage:"https://github.com/mde/ejs",dependencies:{jake:"^10.8.5"},devDependencies:{browserify:"^16.5.1",eslint:"^6.8.0","git-directory-deploy":"^1.5.1",jsdoc:"^4.0.2","lru-cache":"^4.0.1",mocha:"^10.2.0","uglify-js":"^3.3.16"},engines:{node:">=0.10.0"},scripts:{test:"npx jake test"}}},{}]},{},[1])(1)}); diff --git a/libs/accounts/email-renderer/src/sass-compile-files.ts b/libs/accounts/email-renderer/src/sass-compile-files.ts new file mode 100644 index 00000000000..119761441b8 --- /dev/null +++ b/libs/accounts/email-renderer/src/sass-compile-files.ts @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { renderSync } from 'sass'; +import { + writeFileSync, + mkdirSync, + existsSync, + rmdirSync, + readdirSync, +} from 'fs'; +import path from 'path'; + +const getDirectories = (source: string) => + readdirSync(source, { withFileTypes: true }) + .filter((dirent: any) => dirent.isDirectory()) + .map((dirent: any) => dirent.name); + +// construct arrays of partials and templates based on the directories +const partials = getDirectories(path.join(__dirname, 'partials')); +const templates = getDirectories(path.join(__dirname, 'templates')); +const layouts = getDirectories(path.join(__dirname, 'layouts')); + +async function compileSass(dir: string, subdir: string) { + let styleResult: Record = {}; + try { + styleResult = renderSync({ + file: dir, + outFile: subdir, + }); + } catch (e) { + console.log(e); + } + writeFileSync(subdir, styleResult['css'], 'utf8'); +} + +async function main(directories: Record) { + // remove css directory if already present + if (existsSync(path.join(__dirname, 'css'))) { + rmdirSync(path.join(__dirname, 'css'), { recursive: true }); + } + mkdirSync(path.join(__dirname, 'css')); + + // create subdirectories for templates and partials inside the css directory and generate compiled stylesheets into them + for (const dir in directories) { + for (const subdir of directories[`${dir}`]) { + mkdirSync(path.join(__dirname, 'css', subdir)); + const scssPath = path.join(__dirname, dir, subdir, 'index.scss'); + const cssPath = path.join(__dirname, 'css', subdir, 'index.css'); + if (existsSync(scssPath)) { + await compileSass(scssPath, cssPath); + } + } + } + + // compile the global stylesheet + await compileSass( + path.join(__dirname, 'global.scss'), + path.join(__dirname, 'css', 'global.css') + ); + + await compileSass( + path.join(__dirname, 'locale-dir.scss'), + path.join(__dirname, 'css', 'locale-dir.css') + ); +} + +main({ partials, templates, layouts }); diff --git a/libs/accounts/email-renderer/src/storybook-email.tsx b/libs/accounts/email-renderer/src/storybook-email.tsx new file mode 100644 index 00000000000..bce7e6f0d7c --- /dev/null +++ b/libs/accounts/email-renderer/src/storybook-email.tsx @@ -0,0 +1,180 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// NOTE: This file handled with browser ESLint bindings +// instead of NodeJS for DOM typings support +/* eslint-env browser */ + +import { StoryFn } from '@storybook/html'; +import { EmailRenderer } from './renderer'; +import { BrowserRendererBindings } from './renderer/bindings-browser'; +import { ComponentTarget, Includes } from './renderer/bindings'; + +export interface StorybookEmailArgs { + partial?: string; + template: string; + version: number, + layout: string; + includes: Includes; + target?: ComponentTarget; + acceptLanguage: string; + doc?: string; + variables: Record; + direction: 'ltr' | 'rtl'; +} + +/* in production, `utm` parameters may also exist in the urls */ +const commonArgs = { + androidUrl: + 'https://play.google.com/store/apps/details?id=org.mozilla.firefox', + iosUrl: + 'https://apps.apple.com/us/app/firefox-private-safe-browser/id989804926', + supportUrl: + 'https://support.mozilla.org/kb/im-having-problems-my-firefox-account', + privacyUrl: 'https://www.mozilla.org/privacy/mozilla-accounts/', + unsubscribeUrl: + 'https://privacyportal.onetrust.com/webform/1350748f-7139-405c-8188-22740b3b5587/4ba08202-2ede-4934-a89e-f0b0870f95f0', +}; + +const subplatCommonArgs = { + email: 'customer@example.com', + subscriptionTermsUrl: 'http://localhost:3031/legal-docs', + subscriptionPrivacyUrl: 'http://localhost:3031/legal-docs', + cancelSubscriptionUrl: 'http://localhost:3030/subscriptions', + updateBillingUrl: 'http://localhost:3030/subscriptions', + reactivateSubscriptionUrl: 'http://localhost:3030/subscriptions', + accountSettingsUrl: 'http://localhost:3030/settings', + cancellationSurveyUrl: + 'https://survey.alchemer.com/s3/6534408/Privacy-Security-Product-Cancellation-of-Service-Q4-21', + mozillaSupportUrl: 'https://support.mozilla.org', +}; + +const storybookEmail = ({ + partial, + template, + version, + layout = 'fxa', + target = 'index', + includes, + doc, + variables, + direction, +}: StorybookEmailArgs): HTMLDivElement => { + const container = document.createElement('article') as HTMLDivElement; + container.classList.add('email-template'); + container.innerHTML = '

Loading email...

'; + renderUsingMJML({ template, version, layout, target, includes, variables }) + .then(({ html, text, subject }) => { + container.innerHTML = ` +
+

${ + partial ? partial : layout + } / ${ + template === '_storybook' + ? partial + ? 'partial' + : 'layout' + : template + }

+ ${doc ? `

${doc}

` : ''} + +
+ +
+
+ HTML +
${html}
+
+
+ Plaintext +
${text}
+
+
+ `; + }) + .catch((error: Error) => { + container.innerHTML = `

Loading error - ${error.message}

`; + }); + + return container; +}; + +async function renderUsingMJML({ + template, + version, + layout, + includes, + target, + variables, +}: { + template: string; + version: number, + layout: string; + includes: Includes; + target?: ComponentTarget; + variables: Record; +}) { + const renderer = new EmailRenderer(new BrowserRendererBindings()); + const acceptLanguage = navigator.language || 'en'; + + return renderer.renderEmail({ + template, + version, + layout, + target, + acceptLanguage, + includes, + ...variables, + }); +} + +const Template: StoryFn = (args, context) => + storybookEmail({ + ...args, + acceptLanguage: context.globals['acceptLanguage'], + direction: context.globals['direction'], + }); + +export function storyWithProps>( + templateName: string, + templateDoc = '', + defaultArgs: T, + includes: Includes, + layout = 'fxa', + target: ComponentTarget = 'index' +) { + return (overrides: Record = {}, storyName = 'Default') => { + const template = Template.bind({}); + template.args = { + template: templateName, + includes, + layout, + target, + partial: defaultArgs['partial'], + doc: templateDoc, + variables: { + ...commonArgs, + ...defaultArgs, + ...overrides, + }, + }; + template.storyName = storyName; + return template; + }; +} + +export function subplatStoryWithProps>( + templateName: string, + templateDoc = '', + defaultArgs: T, + includes: Includes +) { + return storyWithProps( + templateName, + templateDoc, + { ...defaultArgs, ...subplatCommonArgs }, + includes, + 'subscription' + ); +} diff --git a/libs/accounts/email-renderer/src/storybook.css b/libs/accounts/email-renderer/src/storybook.css new file mode 100644 index 00000000000..05b7caba6f1 --- /dev/null +++ b/libs/accounts/email-renderer/src/storybook.css @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#stateful-partials.sidebar-subheading { + letter-spacing: 2px !important; +} + +.email-template .message { + margin: 0; +} + +.email-template .message, +.email-template header, +.template-container .badge { + font-family: 'Helvetica Neue', Helvetica, arial, sans-serif; +} + +.email-template header { + background: #f2f2f2; + padding: 16px; + border-radius: 4px; +} + +.template-name { + font-size: 22px; + margin: 0 0 10px; +} + +.template-name span { + color: #777; + font-weight: 500; +} + +.template-description { + color: #777; + margin: 0; +} + +.email-subject { + margin: 16px 0 0; + font-family: monospace; + font-size: 14px; +} + +.template-container { + display: flex; + flex-direction: row; + margin-top: 28px; +} + +.template-html { + flex: 0.75 0 0; + border-right: 1px solid #ddd; + margin-right: 28px; +} + +.template-plaintext { + flex: 0.25 0 0; +} + +.template-plaintext div { + padding-top: 24px; + font-family: monospace; + white-space: pre-wrap; +} + +.template-plaintext .rtl { + direction: rtl; +} + +.template-container .badge { + font-size: 12px; + text-transform: uppercase; + background-color: #eee; + padding: 3px 5px; + letter-spacing: 0.05em; + font-weight: 500; + border-radius: 3px; + color: #444; +} diff --git a/libs/accounts/email-renderer/src/templates/_storybook/index.mjml b/libs/accounts/email-renderer/src/templates/_storybook/index.mjml new file mode 100644 index 00000000000..7fe26b72123 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/_storybook/index.mjml @@ -0,0 +1,40 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<%# This file is used for layout-only or partial-only storybook stories. + # It is not used as an email template. %> + +<% if (locals.partial) { %> + + + <%- include(`/partials/${locals.partial}/index.mjml`) %> + + + +<% } else { %> + + + + + Email header + + + + + + + + Email body text, images, and other footers if applicable are displayed here. + + + + Nunc venenatis cursus nulla vel dapibus. Ut nec ullamcorper arcu. Duis nec convallis leo, id condimentum tortor. Ut quis convallis massa. Pellentesque hendrerit velit id nisi malesuada, tincidunt vestibulum diam placerat. Fusce quis dolor enim. Cras eu metus quam. Nullam volutpat consectetur consectetur. + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse laoreet, mauris in tincidunt finibus, mauris odio consequat massa, non porta nunc risus id nunc. Ut sodales finibus magna. + + + +<% } %> diff --git a/libs/accounts/email-renderer/src/templates/_storybook/index.txt b/libs/accounts/email-renderer/src/templates/_storybook/index.txt new file mode 100644 index 00000000000..d5b9ffcfa74 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/_storybook/index.txt @@ -0,0 +1 @@ +<%# This file is hit in all `_storybook` template stories since the renderer expects every template file to have a corresponding txt file, but this is used in partial-only Storybook stories. %><% if (locals.partial) { %><%- include(`/partials/${locals.partial}/index.txt`) %><% } %> diff --git a/libs/accounts/email-renderer/src/templates/_storybook/strapi.mjml b/libs/accounts/email-renderer/src/templates/_storybook/strapi.mjml new file mode 100644 index 00000000000..7fe26b72123 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/_storybook/strapi.mjml @@ -0,0 +1,40 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<%# This file is used for layout-only or partial-only storybook stories. + # It is not used as an email template. %> + +<% if (locals.partial) { %> + + + <%- include(`/partials/${locals.partial}/index.mjml`) %> + + + +<% } else { %> + + + + + Email header + + + + + + + + Email body text, images, and other footers if applicable are displayed here. + + + + Nunc venenatis cursus nulla vel dapibus. Ut nec ullamcorper arcu. Duis nec convallis leo, id condimentum tortor. Ut quis convallis massa. Pellentesque hendrerit velit id nisi malesuada, tincidunt vestibulum diam placerat. Fusce quis dolor enim. Cras eu metus quam. Nullam volutpat consectetur consectetur. + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse laoreet, mauris in tincidunt finibus, mauris odio consequat massa, non porta nunc risus id nunc. Ut sodales finibus magna. + + + +<% } %> diff --git a/libs/accounts/email-renderer/src/templates/_storybook/strapi.txt b/libs/accounts/email-renderer/src/templates/_storybook/strapi.txt new file mode 100644 index 00000000000..d5b9ffcfa74 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/_storybook/strapi.txt @@ -0,0 +1 @@ +<%# This file is hit in all `_storybook` template stories since the renderer expects every template file to have a corresponding txt file, but this is used in partial-only Storybook stories. %><% if (locals.partial) { %><%- include(`/partials/${locals.partial}/index.txt`) %><% } %> diff --git a/libs/accounts/email-renderer/src/templates/adminResetAccounts/en.ftl b/libs/accounts/email-renderer/src/templates/adminResetAccounts/en.ftl new file mode 100644 index 00000000000..accf7fdda43 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/adminResetAccounts/en.ftl @@ -0,0 +1,2 @@ +adminResetAccounts-subject-1 = Fxa Admin: Accounts Reset +adminResetAccounts-title-1 = Here's the account reset status diff --git a/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.mjml b/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.mjml new file mode 100644 index 00000000000..49c217b62e0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.mjml @@ -0,0 +1,25 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Here's the account reset status + + + + <% for (const { locator, status:x } of status) { %> + + + + <% } %> + +
<%- locator %> + <%- x %>
+
+ + + +
+
diff --git a/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.stories.ts b/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.stories.ts new file mode 100644 index 00000000000..293b8e5f8dc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.stories.ts @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/adminResetAccounts', +} as Meta; + +const data = { + status: [ + { + locator: 'foo@mozilla.com', + status: 'Success' + }, + { + locator: 'Bar@mozilla.com', + status: 'Not found' + }, + ] +}; + +const createStory = storyWithProps( + 'adminResetAccounts', + 'Sent after an admin resets a batch of accounts.', + data, + includes +); + +export const CadReminderDefault = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.ts b/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.ts new file mode 100644 index 00000000000..0f05e73f403 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.ts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + status: Array<{ + locator:string, + status:string + }> +}; + +export const template = 'adminResetAccounts'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'adminResetAccounts-subject-1', + message: 'Fxa Admin: Accounts Reset', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.txt b/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.txt new file mode 100644 index 00000000000..eb3933d3c92 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/adminResetAccounts/index.txt @@ -0,0 +1,3 @@ +adminResetAccounts-title-1 = "Here's the account reset status" + +<%- status.map(({ locator, status:x }) => ` ${locator} - ${x}`).join("\r") %> diff --git a/libs/accounts/email-renderer/src/templates/cadReminderFirst/en.ftl b/libs/accounts/email-renderer/src/templates/cadReminderFirst/en.ftl new file mode 100644 index 00000000000..79d154d71ee --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderFirst/en.ftl @@ -0,0 +1,6 @@ +cadReminderFirst-subject-1 = Reminder! Let’s sync { -brand-firefox } +cadReminderFirst-action = Sync another device +cadReminderFirst-action-plaintext = { cadReminderFirst-action }: +# In the title of the email, "It takes two to sync", "two" refers to syncing two devices +cadReminderFirst-title-1 = It takes two to sync +cadReminderFirst-description-v2 = Take your tabs across all your devices. Get your bookmarks, passwords, and other data everywhere you use { -brand-firefox }. diff --git a/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.mjml b/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.mjml new file mode 100644 index 00000000000..b4971afa5c7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.mjml @@ -0,0 +1,24 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + It takes two to sync + + + + + + Take your tabs across all your devices. Get your bookmarks, passwords, and other data everywhere you use Firefox. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "cadReminderFirst-action", + buttonText: "Sync another device" +}) %> +<%- include('/partials/appBadges/index.mjml') %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.stories.ts b/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.stories.ts new file mode 100644 index 00000000000..aaf5d0744b0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/cadReminderFirst', +} as Meta; + +const data = { + oneClickLink: 'http://localhost:3030/connect_another_device?one_click=true', + link: 'http://localhost:3030/connect_another_device', + productName: 'Firefox', +}; + +const createStory = storyWithProps( + 'cadReminderFirst', + 'Sent 8 hours after a user clicks "send me a reminder" on the connect another device page.', + data, + includes +); + +export const CadReminderDefault = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.ts b/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.ts new file mode 100644 index 00000000000..9892dde6f26 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.ts @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + oneClickLink: string; + link: string; + productName: string; +}; + +export const template = 'cadReminderFirst'; +export const version = 3; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'cadReminderFirst-subject-1', + message: 'Reminder! Let’s sync Firefox', + }, + action: { + id: 'cadReminderFirst-action', + message: 'Sync another device', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.txt b/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.txt new file mode 100644 index 00000000000..0a09c1f1fe6 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderFirst/index.txt @@ -0,0 +1,8 @@ +cadReminderFirst-title-1 = "It takes two to sync" + +cadReminderFirst-description-v2 = "Take your tabs across all your devices. Get your bookmarks, passwords, and other data everywhere you use Firefox." + +cadReminderFirst-action-plaintext = "Sync another device:" +<%- link %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/cadReminderSecond/en.ftl b/libs/accounts/email-renderer/src/templates/cadReminderSecond/en.ftl new file mode 100644 index 00000000000..38401f159c1 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderSecond/en.ftl @@ -0,0 +1,5 @@ +cadReminderSecond-subject-2 = Don’t miss out! Let’s finish your sync setup +cadReminderSecond-action = Sync another device +cadReminderSecond-title-2 = Don’t forget to sync! +cadReminderSecond-description-sync = Sync your bookmarks, passwords, open tabs and more — everywhere you use { -brand-firefox }. +cadReminderSecond-description-plus = Plus, your data is always encrypted. Only you and devices you approve can see it. diff --git a/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.mjml b/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.mjml new file mode 100644 index 00000000000..eb131a68ed1 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.mjml @@ -0,0 +1,31 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Don’t forget to sync! + + + + + + + + + Sync your bookmarks, passwords, open tabs and more — everywhere you use Firefox. + + + + Plus, your data is always encrypted. Only you and devices you approve can see it. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "cadReminderSecond-action", + buttonText: "Sync another device" +}) %> +<%- include('/partials/appBadges/index.mjml') %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.stories.ts b/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.stories.ts new file mode 100644 index 00000000000..4012a15ad97 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/cadReminderSecond', +} as Meta; + +const data = { + oneClickLink: 'http://localhost:3030/connect_another_device?one_click=true', + link: 'http://localhost:3030/connect_another_device', + productName: 'Firefox', +}; + +const createStory = storyWithProps( + 'cadReminderSecond', + 'Sent 72 hours after a user clicks "send me a reminder" on the connect another device page.', + data, + includes +); + +export const CadReminderDefault = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.ts b/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.ts new file mode 100644 index 00000000000..f8d80d5ddf0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.ts @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + oneClickLink: string; + link: string; + productName: string; +}; + +export const template = 'cadReminderSecond'; +export const version = 3; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'cadReminderSecond-subject-2', + message: 'Don’t miss out! Let’s finish your sync setup', + }, + action: { + id: 'cadReminderSecond-action', + message: 'Sync another device', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.txt b/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.txt new file mode 100644 index 00000000000..8d3cb8dad1e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/cadReminderSecond/index.txt @@ -0,0 +1,9 @@ +cadReminderSecond-title-2 = "Don’t forget to sync!" + +cadReminderSecond-description-sync = "Sync your bookmarks, passwords, open tabs and more — everywhere you use Firefox." + +cadReminderSecond-description-plus = "Plus, your data is always encrypted. Only you and devices you approve can see it." + +<%- link %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/downloadSubscription/en.ftl b/libs/accounts/email-renderer/src/templates/downloadSubscription/en.ftl new file mode 100644 index 00000000000..e0286db3b26 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/downloadSubscription/en.ftl @@ -0,0 +1,8 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +downloadSubscription-subject = Welcome to { $productName } +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +downloadSubscription-title = Welcome to { $productName } +downloadSubscription-content-2 = Let’s get started using all the features included in your subscription: +downloadSubscription-link-action-2 = Get Started diff --git a/libs/accounts/email-renderer/src/templates/downloadSubscription/index.mjml b/libs/accounts/email-renderer/src/templates/downloadSubscription/index.mjml new file mode 100644 index 00000000000..c26b8b5fc93 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/downloadSubscription/index.mjml @@ -0,0 +1,36 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + +<%- include('/partials/icon/index.mjml') %> + + + + + + Welcome to <%- productName %> + + + + + + + + + + Let’s get started using all the features included in your subscription: + + + + + +<%- include ('/partials/button/index.mjml', { + buttonL10nId: "downloadSubscription-link-action-2", + buttonText: "Get started", + cssClass: 'mb-8' + }) %> + +<%- include('/partials/appBadges/index.mjml', { hideDeviceLink: true }) %> +<%- include('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/downloadSubscription/index.stories.ts b/libs/accounts/email-renderer/src/templates/downloadSubscription/index.stories.ts new file mode 100644 index 00000000000..e80b6540db3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/downloadSubscription/index.stories.ts @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/downloadSubscription', +} as Meta; + +const data = { + icon: 'https://cdn.accounts.firefox.com/product-icons/mozilla-vpn-email.png', + link: 'http://getfirefox.com/', + productName: 'Firefox Fortress', + subscriptionSupportUrl: 'http://localhost:3030/support', + playStoreLink: + 'https://play.google.com/store/apps/details?id=org.mozilla.firefox', + appStoreLink: + 'https://apps.apple.com/us/app/firefox-private-safe-browser/id989804926', + // Forces fallbacks to be used in appBadges partial + iosUrl: undefined, + androidUrl: undefined, +}; + +const createStory = subplatStoryWithProps( + 'downloadSubscription', + 'Sent when a user successfully adds a subscription.', + data, + includes +); + +export const DownloadSubscription = createStory(); + +export const DownloadSubscriptionJustAppleStore = createStory( + { + playStoreLink: undefined, + }, + 'Just Apple App Store' +); + +export const DownloadSubscriptionJustAndroidStore = createStory( + { + appStoreLink: undefined, + }, + 'Just Android App Store' +); + +export const DownloadSubscriptionNoAppStore = createStory( + { + appStoreLink: undefined, + playStoreLink: undefined, + }, + 'No App Store' +); diff --git a/libs/accounts/email-renderer/src/templates/downloadSubscription/index.ts b/libs/accounts/email-renderer/src/templates/downloadSubscription/index.ts new file mode 100644 index 00000000000..b7945986159 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/downloadSubscription/index.ts @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + icon: string; + link: string; + productName: string; + subscriptionSupportUrl: string; + playStoreLink: string; + appStoreLink: string; + iosUrl: undefined; + androidUrl: undefined; +}; + +export const template = 'downloadSubscription'; +export const version = 2; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'downloadSubscription-subject', + message: 'Welcome to <%- productName %>', + }, + action: { + id: 'downloadSubscription-link-action-2', + message: 'Download <%- productName %>', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/downloadSubscription/index.txt b/libs/accounts/email-renderer/src/templates/downloadSubscription/index.txt new file mode 100644 index 00000000000..8c53a888967 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/downloadSubscription/index.txt @@ -0,0 +1,6 @@ +downloadSubscription-title = "Welcome to <%- productName %>" + +downloadSubscription-content-2 = "Let’s get started using all the features included in your subscription:" +<%- link %> + +<%- include('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/en.ftl b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/en.ftl new file mode 100644 index 00000000000..b66088c8db6 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/en.ftl @@ -0,0 +1,8 @@ +fraudulentAccountDeletion-subject-2 = Your { -product-mozilla-account } was deleted +fraudulentAccountDeletion-title = Your account was deleted +fraudulentAccountDeletion-content-part1-v2 = Recently, a { -product-mozilla-account } was created and a subscription was charged using this email address. As we do with all new accounts, we asked that you confirm your account by first validating this email address. +fraudulentAccountDeletion-content-part2-v2 = At present, we see that the account was never confirmed. Since this step was not completed, we are not sure if this was an authorized subscription. As a result, the { -product-mozilla-account } registered to this email address was deleted and your subscription was canceled with all charges reimbursed. +fraudulentAccountDeletion-contact = If you have any questions, please contact our support team. +# Variables: +# $mozillaSupportUrl (String) - Link to https://support.mozilla.org +fraudulentAccountDeletion-contact-plaintext = If you have any questions, please contact our support team: { $mozillaSupportUrl } diff --git a/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.mjml b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.mjml new file mode 100644 index 00000000000..5a7468ed64f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.mjml @@ -0,0 +1,31 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your account was deleted + + + + + Recently, a Mozilla account was created and a subscription was charged using this email address. As we do with all new accounts, we asked that you confirm your account by first validating this email address. + + + + + + At present, we see that the account was never confirmed. Since this step was not completed, we are not sure if this was an authorized subscription. As a result, the Mozilla account registered to this email address was deleted and your subscription was canceled with all charges reimbursed. + + + + + + If you have any questions, please contact our support team. + + + + + +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.stories.ts b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.stories.ts new file mode 100644 index 00000000000..46a8755b6f0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.stories.ts @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/fraudulentAccountDeletion', +} as Meta; + +const data = { + mozillaSupportUrl: 'https://support.mozilla.org', + wasDeleted: true, +}; + +const createStory = subplatStoryWithProps( + 'fraudulentAccountDeletion', + 'Sent to inform user that their unverified account was deleted and transaction was reimbursed.', + data, + includes +); + +export const FraudulentAccountDeletion = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.ts b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.ts new file mode 100644 index 00000000000..d3edce1c7e4 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.ts @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + mozillaSupportUrl: string; + wasDeleted: boolean; +}; + +export const template = 'fraudulentAccountDeletion'; +export const version = 1; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'fraudulentAccountDeletion-subject-2', + message: 'Your Mozilla account was deleted', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.txt b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.txt new file mode 100644 index 00000000000..ec45c3d643c --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/fraudulentAccountDeletion/index.txt @@ -0,0 +1,9 @@ +fraudulentAccountDeletion-title = "Your account was deleted" + +fraudulentAccountDeletion-content-part1-v2 = "Recently, a Mozilla account was created and a subscription was charged using this email address. As we do with all new accounts, we asked that you confirm your account by first validating this email address." + +fraudulentAccountDeletion-content-part2-v2 = "At present, we see that the account was never confirmed. Since this step was not completed, we are not sure if this was an authorized subscription. As a result, the Mozilla account registered to this email address was deleted and your subscription was canceled with all charges reimbursed." + +fraudulentAccountDeletion-contact-plaintext = "If you have any questions, please contact our support team: <%- mozillaSupportUrl %>" + +<%- include('/partials/automatedEmailNoAction/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/en.ftl b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/en.ftl new file mode 100644 index 00000000000..bfe9cd59b70 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/en.ftl @@ -0,0 +1,10 @@ +inactiveAccountFinalWarning-subject = Last chance to keep your { -product-mozilla-account } +inactiveAccountFinalWarning-title = Your { -brand-mozilla } account and data will be deleted +inactiveAccountFinalWarning-preview = Sign in to keep your account +inactiveAccountFinalWarning-account-description = Your { -product-mozilla-account } is used to access free privacy and browsing products like { -brand-firefox } sync, { -product-mozilla-monitor }, { -product-firefox-relay }, and { -product-mdn }. +# $deletionDate - the date when the account will be deleted if the user does not take action to-reactivate their account +# This date will already be formatted with moment.js into Thursday, Jan 9, 2025 format +inactiveAccountFinalWarning-impact = On { $deletionDate }, your account and your personal data will be permanently deleted unless you sign in. +inactiveAccountFinalWarning-action = Sign in to keep your account +# followed by link to sign in +inactiveAccountFinalWarning-action-plaintext = Sign in to keep your account: diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.mjml b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.mjml new file mode 100644 index 00000000000..24bfafbe03f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.mjml @@ -0,0 +1,30 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your Mozilla account and data will be deleted + + + + + + + + Your Mozilla account is used to access free privacy and browsing products like Firefox sync, Mozilla Monitor, Firefox Relay, and MDN. + + + On { $deletionDate }, your account and your personal data will be permanently deleted unless you sign in. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "inactiveAccountFirstWarning-action", + buttonText: "Sign in to keep your account" +}) %> + +<%- include('/partials/accountDeletionInfoBlock/index.mjml') %> +<%- include('/partials/automatedEmailInactiveAccount/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.stories.ts b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.stories.ts new file mode 100644 index 00000000000..bf156560433 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/inactiveAccountFinalWarning', +} as Meta; + +const data = { + // dates will be passed in to the template already localized and formatted by the email handler + deletionDate: 'Thursday, Jan 9, 2025', + link: 'http://localhost:3030/signin', +}; + +const createStory = storyWithProps( + 'inactiveAccountFinalWarning', + 'Final reminder sent to inactive account before account is deleted', + data, + includes +); + +export const inactiveAccountFinalWarning = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.ts b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.ts new file mode 100644 index 00000000000..21809562a94 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + deletionDate: string; + link: string; +}; + +export const template = 'inactiveAccountFinalWarning'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'inactiveAccountFinalWarning-subject', + message: 'Donʼt lose your account', + }, + action: { + id: 'inactiveAccountFinalWarning-action', + message: 'Sign in to keep your account', + }, + preview: { + id: 'inactiveAccountFinalWarning-preview', + message: 'Sign in to keep your account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.txt b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.txt new file mode 100644 index 00000000000..ae2243f2ffc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFinalWarning/index.txt @@ -0,0 +1,12 @@ +inactiveAccountFinalWarning-title = "Your Mozilla account and data will be deleted" + +inactiveAccountFinalWarning-account-description = "Your Mozilla account is used to access free privacy and browsing products like Firefox sync, Mozilla Monitor, Firefox Relay, and MDN." + +inactiveAccountFinalWarning-impact = "On <%- deletionDate %>, your account and your personal data will be permanently deleted unless you sign in." + +inactiveAccountFinalWarning-action-plaintext = "Sign in to keep your account:" +<%- link %> + +<%- include('/partials/accountDeletionInfoBlock/index.txt') %> + +<%- include('/partials/automatedEmailInactiveAccount/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/en.ftl b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/en.ftl new file mode 100644 index 00000000000..dc508c1b422 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/en.ftl @@ -0,0 +1,11 @@ +inactiveAccountFirstWarning-subject = Donʼt lose your account +inactiveAccountFirstWarning-title = Do you want to keep your { -brand-mozilla } account and data? +inactiveAccountFirstWarning-account-description-v2 = Your { -product-mozilla-account } is used to access free privacy and browsing products like { -brand-firefox } sync, { -product-mozilla-monitor }, { -product-firefox-relay }, and { -product-mdn }. +inactiveAccountFirstWarning-inactive-status = We noticed you haven’t signed in for 2 years. +# $deletionDate - the date when the account will be deleted if the user does not take action to-reactivate their account +# This date will already be formatted with moment.js into Thursday, Jan 9, 2025 format +inactiveAccountFirstWarning-impact = Your account and your personal data will be permanently deleted on { $deletionDate } because you haven’t been active. +inactiveAccountFirstWarning-action = Sign in to keep your account +inactiveAccountFirstWarning-preview = Sign in to keep your account +# followed by link to sign in +inactiveAccountFirstWarning-action-plaintext = Sign in to keep your account: diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.mjml b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.mjml new file mode 100644 index 00000000000..48d7bc221a0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.mjml @@ -0,0 +1,33 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Do you want to keep your Mozilla account and data? + + + + + + + + Your Mozilla account is used to access free privacy and browsing products like Firefox sync, Mozilla Monitor, Firefox Relay, and MDN. + + + We noticed you haven’t signed in for 2 years. + + + Your account and your personal data will be permanently deleted on { $deletionDate } because you haven’t been active. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "inactiveAccountFirstWarning-action", + buttonText: "Sign in to keep your account" +}) %> + +<%- include('/partials/accountDeletionInfoBlock/index.mjml') %> +<%- include('/partials/automatedEmailInactiveAccount/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.stories.ts b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.stories.ts new file mode 100644 index 00000000000..81e116bb5e0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/inactiveAccountFirstWarning', +} as Meta; + +const data = { + // dates will be passed in to the template already localized and formatted by the email handler + deletionDate: 'Thursday, Jan 9, 2025', + link: 'http://localhost:3030/signin', +}; + +const createStory = storyWithProps( + 'inactiveAccountFirstWarning', + 'First reminder sent to inactive account before account is deleted', + data, + includes +); + +export const inactiveAccountFirstWarning = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.ts b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.ts new file mode 100644 index 00000000000..cfb142b66bb --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + deletionDate: string; + link: string; +}; + +export const template = 'inactiveAccountFirstWarning'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'inactiveAccountFirstWarning-subject', + message: 'Donʼt lose your account', + }, + action: { + id: 'inactiveAccountFirstWarning-action', + message: 'Sign in to keep your account', + }, + preview: { + id: 'inactiveAccountFirstWarning-preview', + message: 'Sign in to keep your account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.txt b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.txt new file mode 100644 index 00000000000..cf00aaee612 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountFirstWarning/index.txt @@ -0,0 +1,14 @@ +inactiveAccountFirstWarning-title = "Do you want to keep your Mozilla account and data?" + +inactiveAccountFirstWarning-account-description-v2 = "Your Mozilla account is used to access free privacy and browsing products like Firefox sync, Mozilla Monitor, Firefox Relay, and MDN." + +inactiveAccountFirstWarning-inactive-status = "We noticed you haven’t signed in for 2 years." + +inactiveAccountFirstWarning-impact = "Your account and your personal data will be permanently deleted on <%- deletionDate %> because you haven’t been active." + +inactiveAccountFirstWarning-action-plaintext = "Sign in to keep your account:" +<%- link %> + +<%- include('/partials/accountDeletionInfoBlock/index.txt') %> + +<%- include('/partials/automatedEmailInactiveAccount/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/en.ftl b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/en.ftl new file mode 100644 index 00000000000..893b0c90e63 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/en.ftl @@ -0,0 +1,10 @@ +inactiveAccountSecondWarning-subject = Action required: Account deletion in 7 days +inactiveAccountSecondWarning-title = Your { -brand-mozilla } account and data will be deleted in 7 days +inactiveAccountSecondWarning-account-description-v2 = Your { -product-mozilla-account } is used to access free privacy and browsing products like { -brand-firefox } sync, { -product-mozilla-monitor }, { -product-firefox-relay }, and { -product-mdn }. + +# $deletionDate - the date when the account will be deleted if the user does not take action to-reactivate their account +inactiveAccountSecondWarning-impact = Your account and your personal data will be permanently deleted on { $deletionDate } because you haven’t been active. +inactiveAccountSecondWarning-action = Sign in to keep your account +inactiveAccountSecondWarning-preview = Sign in to keep your account +# followed by link to sign in +inactiveAccountSecondWarning-action-plaintext = Sign in to keep your account: diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.mjml b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.mjml new file mode 100644 index 00000000000..a28370c0be0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.mjml @@ -0,0 +1,30 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your Mozilla account and data will be deleted in 7 days + + + + + + + + Your Mozilla account is used to access free privacy and browsing products like Firefox sync, Mozilla Monitor, Firefox Relay, and MDN. + + + Your account and your personal data will be permanently deleted on { $deletionDate } because you haven’t been active. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "inactiveAccountSecondWarning-action", + buttonText: "Sign in to keep your account" +}) %> + +<%- include('/partials/accountDeletionInfoBlock/index.mjml') %> +<%- include('/partials/automatedEmailInactiveAccount/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.stories.ts b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.stories.ts new file mode 100644 index 00000000000..4ad1914e2b8 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.stories.ts @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/inactiveAccountSecondWarning', +} as Meta; + +const data = { + deletionDate: 'Thursday, Jan 9, 2025', + link: 'http://localhost:3030/signin', +}; + +const createStory = storyWithProps( + 'inactiveAccountSecondWarning', + 'Second reminder sent to inactive account before account is deleted', + data, + includes +); + +export const inactiveAccountSecondWarning = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.ts b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.ts new file mode 100644 index 00000000000..86995ffa1e4 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + deletionDate: string; + link: string; +}; + +export const template = 'inactiveAccountSecondWarning'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'inactiveAccountSecondWarning-subject', + message: 'Action required: Account deletion in 7 days', + }, + action: { + id: 'inactiveAccountSecondWarning-action', + message: 'Sign in to keep your account', + }, + preview: { + id: 'inactiveAccountSecondWarning-preview', + message: 'Sign in to keep your account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.txt b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.txt new file mode 100644 index 00000000000..73b655fb9ca --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/inactiveAccountSecondWarning/index.txt @@ -0,0 +1,12 @@ +inactiveAccountSecondWarning-title = "Your Mozilla account and data will be deleted in 7 days" + +inactiveAccountSecondWarning-account-description-v2 = "Your Mozilla account is used to access free privacy and browsing products like Firefox sync, Mozilla Monitor, Firefox Relay, and MDN." + +inactiveAccountSecondWarning-impact = "Your account and your personal data will be permanently deleted on <%- deletionDate %> because you haven’t been active." + +inactiveAccountSecondWarning-action-plaintext = "Sign in to keep your account:" +<%- link %> + +<%- include('/partials/accountDeletionInfoBlock/index.txt') %> + +<%- include('/partials/automatedEmailInactiveAccount/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/index.ts b/libs/accounts/email-renderer/src/templates/index.ts new file mode 100644 index 00000000000..a9559468a2b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/index.ts @@ -0,0 +1 @@ +export * from './cadReminderFirst'; diff --git a/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/en.ftl b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/en.ftl new file mode 100644 index 00000000000..eaf50692da6 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/en.ftl @@ -0,0 +1,18 @@ +# The user has a low number of valid recovery codes remaining for use +codes-reminder-title-zero = You’re out of backup authentication codes! +codes-reminder-title-one = You’re on your last backup authentication code +codes-reminder-title-two = Time to create more backup authentication codes + +codes-reminder-description-part-one = Backup authentication codes help you restore your info when you forget your password. +codes-reminder-description-part-two = Create new codes now so you don’t lose your data later. +codes-reminder-description-two-left = You only have two codes left. +codes-reminder-description-create-codes = Create new backup authentication codes to help you get back into your account if you’re locked out. + +lowRecoveryCodes-action-2 = Create codes +codes-create-plaintext = { lowRecoveryCodes-action-2 }: +lowRecoveryCodes-subject-2 = + { $numberRemaining -> + [0] No backup authentication codes left + [one] Only 1 backup authentication code left + *[other] Only { $numberRemaining } backup authentication codes left! + } diff --git a/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.mjml b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.mjml new file mode 100644 index 00000000000..5d356015a67 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.mjml @@ -0,0 +1,43 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + <% if (numberRemaining === 0 ) { %> + You’re out of backup authentication codes! + <% } else if (numberRemaining === 1) { %> + You’re on your last backup authentication code + <% } else { %> + Time to create more backup authentication codes + <%} %> + + + + + + + <% if (numberRemaining < 2) { %> + + Backup authentication codes help you restore your info when you forget your password. + + + Create new codes now so you don’t lose your data later. + + <% } else { %> + + You only have two codes left. + + + Create new backup authentication codes to help you get back into your account if you’re locked out. + + <%} %> + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "lowRecoveryCodes-action-2", + buttonText: "Create codes" +}) %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.stories.ts b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.stories.ts new file mode 100644 index 00000000000..b586ea08cba --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.stories.ts @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { getIncludes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/lowRecoveryCodes', +} as Meta; + +const data = { + link: 'http://localhost:3030/settings/two_step_authentication/replace_codes?low_recovery_codes=true', + numberRemaining: 1, +}; + +const createStory = storyWithProps( + 'lowRecoveryCodes', + 'Sent when a user has 2 or less backup authentication codes remaining.', + data, + getIncludes(data) +); + +export const LowRecoveryCodesZero = createStory( + { + numberRemaining: 0, + }, + 'User has 0 recovery codes remaining' +); + +export const LowRecoveryCodesOne = createStory( + { + numberRemaining: 1, + }, + 'User has 1 backup authentication code remaining' +); + +export const LowRecoveryCodesMultiple = createStory( + { + numberRemaining: 2, + }, + 'User has 2 backup authentication codes remaining' +); diff --git a/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.ts b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.ts new file mode 100644 index 00000000000..ff2cc5e3e54 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.ts @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + numberRemaining: number; +}; + +export const template = 'lowRecoveryCodes'; +export const version = 7; +export const layout = 'fxa'; + +const SUBJECTS = [ + 'No backup authentication codes left', + 'Only 1 backup authentication code left', +]; +const getSubject = (numberRemaining: number) => + SUBJECTS[numberRemaining] || + 'Only <%= numberRemaining %> backup authentication codes left!'; +export function getIncludes({ numberRemaining }: TemplateData) { + return { + subject: { + id: 'lowRecoveryCodes-subject-2', + message: getSubject(numberRemaining), + }, + action: { + id: 'lowRecoveryCodes-action-2', + message: 'Create codes', + }, + }; +} diff --git a/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.txt b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.txt new file mode 100644 index 00000000000..fcf55ca87a7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/lowRecoveryCodes/index.txt @@ -0,0 +1,29 @@ +<%- body %> + +<% if (locals.numberRemaining === 0) { %> +codes-reminder-title-zero = "You’re out of backup authentication codes!" + +codes-reminder-description-part-one = "Backup authentication codes help you restore your info when you forget your password." + +codes-reminder-description-part-two = "Create new codes now so you don’t lose your data later." + +<% } else if (locals.numberRemaining === 1) { %> +codes-reminder-title-one = "You’re on your last backup authentication code" + +codes-reminder-description-part-one = "Backup authentication codes help you restore your info when you forget your password." + +codes-reminder-description-part-two = "Create new codes now so you don’t lose your data later." + +<% } else { %> +codes-reminder-title-two = "Time to create more backup authentication codes" + +codes-reminder-description-two-left = "You only have two codes left." + +codes-reminder-description-create-codes = "Create new backup authentication codes to help you get back into your account if you’re locked out." + +<% } %> + +codes-create-plaintext = "Create codes:" +<%- link %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/newDeviceLogin/en.ftl b/libs/accounts/email-renderer/src/templates/newDeviceLogin/en.ftl new file mode 100644 index 00000000000..e9ab2c405d8 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/newDeviceLogin/en.ftl @@ -0,0 +1,12 @@ +# Variables: +# $clientName (String) - A client the user hasn't signed into before (e.g. Firefox, Sync) +newDeviceLogin-subject = New sign-in to { $clientName } +newDeviceLogin-subjectForMozillaAccount = New sign-in to your { -product-mozilla-account } +newDeviceLogin-title-3 = Your { -product-mozilla-account } was used to sign in +# The "Not you?" question is asking whether the recipient of the email is the +# person who performed the action that triggered the email. +newDeviceLogin-change-password = Not you? Change your password. +# The "Not you?" question is asking whether the recipient of the email is the +# person who performed the action that triggered the email. +newDeviceLogin-change-password-plain = Not you? Change your password: +newDeviceLogin-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.mjml b/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.mjml new file mode 100644 index 00000000000..c26dff5da0f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.mjml @@ -0,0 +1,29 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your Mozilla account was used to sign in + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + + Not you? Change your password. + + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "newDeviceLogin-action", + buttonText: "Manage account" +}) %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.stories.ts b/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.stories.ts new file mode 100644 index 00000000000..cb6ac9869cf --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.stories.ts @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { getIncludes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/newDeviceLogin', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + clientName: 'Firefox', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + link: 'http://localhost:3030/settings', + mozillaSupportUrl: 'https://support.mozilla.org', + showBannerWarning: false, +}; + +const createStory = storyWithProps( + 'newDeviceLogin', + 'Sent to notify the account that a new device or service has signed in.', + data, + getIncludes(data) +); + +export const NewDeviceLoginFirefox = createStory( + {}, + 'New device login through Firefox' +); + +export const NewDeviceLoginAMO = createStory( + { clientName: 'Add-ons', showBannerWarning: true }, + 'New device login through AMO or web, with warning banner' +); +export const NewDeviceLoginOther = createStory( + { clientName: '123 Done' }, + 'New device login through something else' +); + +export const NewDeviceLoginForMozillaAccount = createStory( + { clientName: 'Mozilla' }, + 'New device login for your Mozilla account' +); diff --git a/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.ts b/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.ts new file mode 100644 index 00000000000..ad586fbad7b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.ts @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; + time: string; + clientName: string; + passwordChangeLink: string; + link: string; + mozillaSupportUrl: string; + showBannerWarning: boolean; + cmsRpClientId?: string; + cmsRpFromName?: string; + entrypoint?: string; + subject?: string; + headline?: string; + description?: string; +}; + +export const template = 'newDeviceLogin'; +export const version = 7; +export const layout = 'fxa'; + +export function getIncludes({ clientName }: TemplateData) { + const subject = + clientName === 'Mozilla' + ? { + id: 'newDeviceLogin-subjectForMozillaAccount', + message: 'New sign-in to your Mozilla account', + } + : { + id: 'newDeviceLogin-subject', + message: 'New sign-in to <%- clientName %>', + }; + + return { + subject, + action: { + id: 'newDeviceLogin-action', + message: 'Manage account', + }, + }; +} diff --git a/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.txt b/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.txt new file mode 100644 index 00000000000..3b8278f2711 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/newDeviceLogin/index.txt @@ -0,0 +1,11 @@ +newDeviceLogin-title-3 = "Your Mozilla account was used to sign in" + +<%- include('/partials/userInfo/index.txt') %> + +newDeviceLogin-change-password-plain = "Not you? Change your password:" +<%- passwordChangeLink %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.mjml b/libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.mjml new file mode 100644 index 00000000000..357af878d7b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.mjml @@ -0,0 +1,32 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + <%- locals.headline %> + + + <%- locals.description %> + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + + Not you? Change your password. + + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "newDeviceLogin-action", + buttonText: "Manage account" +}) %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.stories.ts b/libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.stories.ts new file mode 100644 index 00000000000..c3e99aecc2e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.stories.ts @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { getIncludes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/newDeviceLogin/Strapi', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + cmsRpClientId: '00f00f', + cmsRpFromName: 'Testo Inc.', + entrypoint: 'quux', + subject: 'New Login', + headline: 'You logged into Product!', + description: 'It appears you logged in.', + clientName: 'Firefox', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + link: 'http://localhost:3030/settings', + mozillaSupportUrl: 'https://support.mozilla.org', + showBannerWarning: false, +}; + +const createStory = storyWithProps( + 'newDeviceLogin', + 'Sent to notify the account that a new service has signed in.', + data, + getIncludes(data), + 'fxa', + 'strapi' +); + +export const NewDeviceLoginRp = createStory( + { clientName: '123 Done' }, + 'New device login through RP with CMS Email' +); diff --git a/libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.txt b/libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.txt new file mode 100644 index 00000000000..ab3ac1eec4c --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/newDeviceLogin/strapi.txt @@ -0,0 +1,13 @@ +<%- locals.headline %> + +<%- locals.description %> + +<%- include('/partials/userInfo/index.txt') %> + +newDeviceLogin-change-password-plain = "Not you? Change your password:" +<%- passwordChangeLink %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordChangeRequired/en.ftl b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/en.ftl new file mode 100644 index 00000000000..35dcceeb52e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/en.ftl @@ -0,0 +1,11 @@ +passwordChangeRequired-subject = Suspicious activity detected +passwordChangeRequired-preview = Change your password immediately +passwordChangeRequired-title-2 = Reset your password + +passwordChangeRequired-suspicious-activity-3 = We locked your account to keep it safe from suspicious activity. You’ve been signed out of all your devices and any synced data has been deleted as a precaution. +passwordChangeRequired-sign-in-3 = To sign back in to your account, all you need to do is reset your password. +passwordChangeRequired-different-password-2 = Important: Pick a strong password that’s different from one you’ve used in the past. +passwordChangeRequired-different-password-plaintext-2 = Important: Pick a strong password that’s different from one you’ve used in the past. + +passwordChangeRequired-action = Reset password +passwordChangeRequired-action-plaintext = { passwordChangeRequired-action }: diff --git a/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.mjml b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.mjml new file mode 100644 index 00000000000..a3a6ecc6852 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.mjml @@ -0,0 +1,35 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Reset your password + + + + + + + + We locked your account to keep it safe from suspicious activity. You’ve been signed out of all your devices and any synced data has been deleted as a precaution. + + + + To sign back in to your account, all you need to do is reset your password. + + + + Important: Pick a strong password that’s different from one you’ve used in the past. + + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "passwordChangeRequired-action", + buttonText: "Reset password" +}) %> + +<%- include('/partials/support/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.stories.ts b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.stories.ts new file mode 100644 index 00000000000..82a923d99ca --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.stories.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/passwordChangeRequired', +} as Meta; + +const data = { + link: 'http://localhost:3030/reset_password', +}; + +const createStory = storyWithProps( + 'passwordChangeRequired', + "Sent when an account's devices are disconnected and a password change is required due to suspicious activity. It currently needs Ops to manually trigger via bulk-mailer", + data, + includes +); + +export const PasswordChangeRequired = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.ts b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.ts new file mode 100644 index 00000000000..036c7aa0647 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +export type TemplateData = { + link: string; +}; + +export const template = 'passwordChangeRequired'; +export const version = 4; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'passwordChangeRequired-subject', + message: 'Suspicious activity detected', + }, + preview: { + id: 'passwordChangeRequired-preview', + message: 'Change your password immediately', + }, + action: { + id: 'passwordChangeRequired-action', + message: 'Reset password', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.txt b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.txt new file mode 100644 index 00000000000..8985e9c574d --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChangeRequired/index.txt @@ -0,0 +1,12 @@ +passwordChangeRequired-title-2 = "Reset your password" + +passwordChangeRequired-suspicious-activity-3 = "We locked your account to keep it safe from suspicious activity. You’ve been signed out of all your devices and any synced data has been deleted as a precaution." + +passwordChangeRequired-sign-in-3 = "To sign back in to your account, all you need to do is reset your password." + +passwordChangeRequired-different-password-plaintext-2 = "Important: Pick a strong password that’s different from one you’ve used in the past." + +passwordChangeRequired-action-plaintext = "Reset password:" +<%- link %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordChanged/en.ftl b/libs/accounts/email-renderer/src/templates/passwordChanged/en.ftl new file mode 100644 index 00000000000..b19f1eb052f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChanged/en.ftl @@ -0,0 +1,3 @@ +passwordChanged-subject = Password updated +passwordChanged-title = Password changed successfully +passwordChanged-description-2 = Your { -product-mozilla-account } password was successfully changed from the following device: diff --git a/libs/accounts/email-renderer/src/templates/passwordChanged/index.mjml b/libs/accounts/email-renderer/src/templates/passwordChanged/index.mjml new file mode 100644 index 00000000000..6b7883457fb --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChanged/index.mjml @@ -0,0 +1,18 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Password changed successfully + + + + Your Mozilla account password was successfully changed from the following device: + + + + +<%- include('/partials/userInfo/index.mjml', { cssClass: 'text-body-grey-no-margin' }) %> +<%- include('/partials/automatedEmailResetPassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordChanged/index.stories.ts b/libs/accounts/email-renderer/src/templates/passwordChanged/index.stories.ts new file mode 100644 index 00000000000..4292b7e94bf --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChanged/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/passwordChanged', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + resetLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'passwordChanged', + 'Sent when password has been changed.', + data, + includes +); + +export const PasswordChanged = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/passwordChanged/index.ts b/libs/accounts/email-renderer/src/templates/passwordChanged/index.ts new file mode 100644 index 00000000000..b24828fe362 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChanged/index.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; + resetLink: string; +}; + +export const template = 'passwordChanged'; +export const version = 5; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'passwordChanged-subject', + message: 'Password updated', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/passwordChanged/index.txt b/libs/accounts/email-renderer/src/templates/passwordChanged/index.txt new file mode 100644 index 00000000000..f1b0f5ce9a9 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordChanged/index.txt @@ -0,0 +1,8 @@ +passwordChanged-title = "Password changed successfully" + +passwordChanged-description-2 = "Your Mozilla account password was successfully changed from the following device:" + +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/automatedEmailResetPassword/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordForgotOtp/en.ftl b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/en.ftl new file mode 100644 index 00000000000..389628c4833 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/en.ftl @@ -0,0 +1,8 @@ +# Variables: +# $code (String) - The confirmation code for sign-in +password-forgot-otp-subject-2 = Use { $code } to change your password +password-forgot-otp-preview = This code expires in 10 minutes +password-forgot-otp-title = Forgot your password? +password-forgot-otp-request = We received a request for a password change on your { -product-mozilla-account } from: +password-forgot-otp-code-2 = If this was you, here is your confirmation code to proceed: +password-forgot-otp-expiry-notice = This code expires in 10 minutes. diff --git a/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.mjml b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.mjml new file mode 100644 index 00000000000..a677c78e780 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.mjml @@ -0,0 +1,44 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public # +License, v. 2.0. If a copy of the MPL was not distributed with this # file, You +can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Forgot your password? + + + + + We received a request for a password change on your Mozilla account from: + + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + + + If this was you, here is your confirmation code to proceed: + + + + <%- code %> + + + This code expires in 10 minutes. + + + + + + + + <%- include('/partials/automatedEmailNoAction/index.mjml') %> + + + diff --git a/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.stories.ts b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.stories.ts new file mode 100644 index 00000000000..a3b72de8371 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/passwordForgotOtp', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + code: '96318398', +}; + +const createStory = storyWithProps( + 'passwordForgotOtp', + 'OTP sent to user to start password reset.', + data, + includes +); + +export const PasswordForgotOtp = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.ts b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.ts new file mode 100644 index 00000000000..6c1d3a1a6be --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.ts @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + code: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'passwordForgotOtp'; +export const version = 2; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'password-forgot-otp-subject-2', + message: 'Use <%- code %> to change your password', + }, + preview: { + id: 'password-forgot-otp-preview', + message: 'This code expires in 10 minutes', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.txt b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.txt new file mode 100644 index 00000000000..6131678d96b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordForgotOtp/index.txt @@ -0,0 +1,13 @@ +password-forgot-otp-title = "Forgot your password?" + +password-forgot-otp-request = "We received a request for a password change on your Mozilla account from:" + +<%- include('/partials/userInfo/index.txt') %> + +password-forgot-otp-code-2 = "If this was you, here is your confirmation code to proceed:" + +<%- code %> + +password-forgot-otp-expiry-notice = "This code expires in 10 minutes." + +<%- include('/partials/automatedEmailNoAction/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordReset/en.ftl b/libs/accounts/email-renderer/src/templates/passwordReset/en.ftl new file mode 100644 index 00000000000..4ff5bbf343f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordReset/en.ftl @@ -0,0 +1,4 @@ +passwordReset-subject-2 = Your password has been reset +passwordReset-title-2 = Your password has been reset +# This sentence is followed by information about the device and time of the password reset +passwordReset-description-2 = You reset your { -product-mozilla-account } password on: diff --git a/libs/accounts/email-renderer/src/templates/passwordReset/index.mjml b/libs/accounts/email-renderer/src/templates/passwordReset/index.mjml new file mode 100644 index 00000000000..39be6000800 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordReset/index.mjml @@ -0,0 +1,23 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your password has been updated + + + + + + + + You reset your Mozilla account password on: + + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/automatedEmailResetPassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordReset/index.stories.ts b/libs/accounts/email-renderer/src/templates/passwordReset/index.stories.ts new file mode 100644 index 00000000000..9e3e33aa7f1 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordReset/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; + +export default { + title: 'FxA Emails/Templates/passwordReset', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + resetLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'passwordReset', + 'Sent when password has been reset.', + data, + includes +); + +export const PasswordReset = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/passwordReset/index.ts b/libs/accounts/email-renderer/src/templates/passwordReset/index.ts new file mode 100644 index 00000000000..e14b22933d8 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordReset/index.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + resetLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'passwordReset'; +export const version = 6; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'passwordReset-subject-2', + message: 'Your password has been reset', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/passwordReset/index.txt b/libs/accounts/email-renderer/src/templates/passwordReset/index.txt new file mode 100644 index 00000000000..78f3ff14f27 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordReset/index.txt @@ -0,0 +1,9 @@ +passwordReset-title-2 = "Your password has been reset" + +passwordReset-description-2 = "You reset your Mozilla account password on:" + +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/automatedEmailResetPassword/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/en.ftl b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/en.ftl new file mode 100644 index 00000000000..40906b5d12b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/en.ftl @@ -0,0 +1,8 @@ +passwordResetAccountRecovery-subject-2 = Your password has been reset +passwordResetAccountRecovery-title-3 = Your password has been reset +# Followed by details on the device, location, and date/time of the password reset. +passwordResetAccountRecovery-description-3 = You used your account recovery key to reset your { -product-mozilla-account } password on: +passwordResetAccountRecovery-information = We logged you out of all your synced devices. We created a new account recovery key to replace the one you used. You can change it in your account settings. +# After the colon there is a link to account settings +passwordResetAccountRecovery-information-txt = We logged you out of all your synced devices. We created a new account recovery key to replace the one you used. You can change it in your account settings: +passwordResetAccountRecovery-action-4 = Manage account diff --git a/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.mjml b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.mjml new file mode 100644 index 00000000000..0e460097764 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.mjml @@ -0,0 +1,33 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your password has been reset + + + + You used your account recovery key to update your Mozilla account password on: + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + We logged you out of all your synced devices. We created a new account recovery key to replace the one you used. You can change it in your account settings. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "passwordResetAccountRecovery-action-4", + buttonText: "Manage account" +}) %> + +<%- include('/partials/appBadges/index.mjml') %> +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.stories.ts b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.stories.ts new file mode 100644 index 00000000000..2bf925c1ae8 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/passwordResetAccountRecovery', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + productName: 'Firefox', +}; +const createStory = storyWithProps( + 'passwordResetAccountRecovery', + 'Sent when account recovery key is used.', + data, + includes +); + +export const PasswordResetAccountRecovery = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.ts b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.ts new file mode 100644 index 00000000000..96d8b7b77fd --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.ts @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + productName: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'passwordResetAccountRecovery'; +export const version = 9; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'passwordResetAccountRecovery-subject-2', + message: 'Your password has been reset', + }, + action: { + id: 'passwordResetAccountRecovery-action-4', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.txt b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.txt new file mode 100644 index 00000000000..31d97338601 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetAccountRecovery/index.txt @@ -0,0 +1,13 @@ +passwordResetAccountRecovery-title-3 = "Your password has been reset" + +passwordResetAccountRecovery-description-3 = "You used your account recovery key to update your Mozilla account password on:" + +<%- include('/partials/userInfo/index.txt') %> + +passwordResetAccountRecovery-regen-required-txt-1 = "We logged you out of all your synced devices. We created a new account recovery key to replace the one you used. You can change it in your account settings:" + +<%- link %> + +<%- include('/partials/appBadges/index.txt') %> + +<%- include('/partials/automatedEmailChangePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/en.ftl b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/en.ftl new file mode 100644 index 00000000000..3998e0a9aff --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/en.ftl @@ -0,0 +1,5 @@ +passwordResetRecoveryPhone-subject = Recovery phone used +passwordResetRecoveryPhone-preview = Check to make sure this was you +passwordResetRecoveryPhone-title = Your recovery phone was used to confirm a password reset +passwordResetRecoveryPhone-device = Recovery phone used from: +passwordResetRecoveryPhone-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.mjml b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.mjml new file mode 100644 index 00000000000..f2e17c36611 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.mjml @@ -0,0 +1,23 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your recovery phone was used to confirm a password reset + + + + Recovery phone used from: + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "passwordResetRecoveryPhone-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailResetPasswordTwoFactor/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.stories.ts b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.stories.ts new file mode 100644 index 00000000000..bbdce01ae7e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.stories.ts @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/passwordResetRecoveryPhone', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + resetLink: 'http://localhost:3030/reset_password', + twoFactorSettingsLink: + 'http://localhost:3030/settings#two-step-authentication', + supportUrl: 'http://localhost:3030/support', +}; + +const createStory = storyWithProps( + 'passwordResetRecoveryPhone', + 'Sent when a user uses their recovery phone to reset their password as an alternative to their authenticator app.', + data, + includes +); + +export const passwordResetRecoveryPhone = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.ts b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.ts new file mode 100644 index 00000000000..893a0011062 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.ts @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + resetLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'passwordResetRecoveryPhone'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'passwordResetRecoveryPhone-subject', + message: 'Recovery phone used', + }, + action: { + id: 'passwordResetRecoveryPhone-action', + message: 'Manage account', + }, + preview: { + id: 'passwordResetRecoveryPhone-preview', + message: 'Check to make sure this was you', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.txt b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.txt new file mode 100644 index 00000000000..36f4d6711b3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetRecoveryPhone/index.txt @@ -0,0 +1,10 @@ +passwordResetRecoveryPhone-title = "Your recovery phone was used to confirm a password reset" + +passwordResetRecoveryPhone-device = "Recovery phone used from:" +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailResetPasswordTwoFactor/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/en.ftl b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/en.ftl new file mode 100644 index 00000000000..bf4c9490a47 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/en.ftl @@ -0,0 +1,9 @@ +passwordResetWithRecoveryKeyPrompt-subject = Your password has been reset +passwordResetWithRecoveryKeyPrompt-title = Your password has been reset +# Details of the device and date/time where the password was reset +passwordResetWithRecoveryKeyPrompt-description = You reset your { -product-mozilla-account } password on: +# Text for button action to create a new account recovery key +passwordResetWithRecoveryKeyPrompt-action = Create account recovery key +# colon is followed by a link to create an account recovery key from the account settings page +passwordResetWithRecoveryKeyPrompt-action-txt = Create account recovery key: +passwordResetWithRecoveryKeyPrompt-cta-description = You’ll need to sign in again on all of your synced devices. Keep your data safe next time with an account recovery key. This allows you to recover your data if you forget your password. diff --git a/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.mjml b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.mjml new file mode 100644 index 00000000000..773187047c1 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.mjml @@ -0,0 +1,32 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your password has been reset + + + + You reset your Mozilla account password on: + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + Youʼll need to sign in again on all of your synced devices. Keep your data safe next time with an account recovery key. This allows you to recover your data if you forget your password. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "passwordResetWithRecoveryKeyPrompt-action", + buttonText: "Create account recovery key" +}) %> + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.stories.ts b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.stories.ts new file mode 100644 index 00000000000..e3f575db2ad --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.stories.ts @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/passwordResetWithRecoveryKeyPrompt', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings/account_recovery', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + productName: 'Firefox', +}; + +const createStory = storyWithProps( + 'passwordResetWithRecoveryKeyPrompt', + 'Sent when a sync user resets their password without an account recovery key.', + data, + includes +); + +export const PasswordResetWithRecoveryKeyPrompt = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.ts b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.ts new file mode 100644 index 00000000000..929b7b89ff7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.ts @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + productName: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'passwordResetWithRecoveryKeyPrompt'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'passwordResetWithRecoveryKeyPrompt-subject', + message: 'Your password has been reset', + }, + action: { + id: 'passwordResetWithRecoveryKeyPrompt-action', + message: 'Create account recovery key', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.txt b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.txt new file mode 100644 index 00000000000..27bc9a7838a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/passwordResetWithRecoveryKeyPrompt/index.txt @@ -0,0 +1,13 @@ +passwordResetWithRecoveryKeyPrompt-title = "Your password has been reset" + +passwordResetWithRecoveryKeyPrompt-description-2 = "You reset your Mozilla account password on:" + +<%- include('/partials/userInfo/index.txt') %> + +passwordResetWithRecoveryKeyPrompt-cta-description = "You’ll need to sign in again on all of your synced devices. Keep your data safe next time with an account recovery key. This allows you to recover your data if you forget your password." + +passwordResetWithRecoveryKeyPrompt-action-txt = "Create account recovery key:" + +<%- link %> + +<%- include('/partials/automatedEmailChangePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/en.ftl b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/en.ftl new file mode 100644 index 00000000000..4cc7c0e5e3f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/en.ftl @@ -0,0 +1,7 @@ +postAddAccountRecovery-subject-3 = New account recovery key created +postAddAccountRecovery-title2 = You created a new account recovery key +# Key here refers to account recovery key +postAddAccountRecovery-body-part1 = Save this key in a safe place — you’ll need it to restore your encrypted browsing data if you forget your password. +# Key here refers to account recovery key +postAddAccountRecovery-body-part2 = This key can only be used once. After you use it, we’ll automatically create a new one for you. Or you can create a new one any time from your account settings. +postAddAccountRecovery-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.mjml b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.mjml new file mode 100644 index 00000000000..8a23eba97e6 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.mjml @@ -0,0 +1,35 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + You created a new account recovery key + + + + + + + + + Save this key in a safe place — you’ll need it to restore your encrypted browsing data if you forget your password. + + + + + This key can only be used once. After you use it, we’ll automatically create a new one for you. Or you can create a new one any time from your account settings. + + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postAddAccountRecovery-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailRecoveryKey/index.mjml', { + keyExists: true +}) %> diff --git a/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.stories.ts b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.stories.ts new file mode 100644 index 00000000000..75a03820a97 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.stories.ts @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postAddAccountRecovery', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + revokeAccountRecoveryLink: 'http://localhost:3030/settings/#recovery-key', +}; + +const createStory = storyWithProps( + 'postAddAccountRecovery', + 'Sent when new account recovery key is generated', + data, + includes +); + +export const PostAddAccountRecovery = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.ts b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.ts new file mode 100644 index 00000000000..79ad36931b0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.ts @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + revokeAccountRecoveryLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postAddAccountRecovery'; +export const version = 9; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postAddAccountRecovery-subject-3', + message: 'New account recovery key created', + }, + action: { + id: 'postAddAccountRecovery-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.txt b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.txt new file mode 100644 index 00000000000..bf3f103305d --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddAccountRecovery/index.txt @@ -0,0 +1,9 @@ +postAddAccountRecovery-title2 = "You created a new account recovery key" + +postAddAccountRecovery-body-part1 = "Save this key in a safe place — you’ll need it to restore your encrypted browsing data if you forget your password." + +postAddAccountRecovery-body-part2 = "This key can only be used once. After you use it, we’ll automatically create a new one for you. Or you can create a new one any time from your account settings." + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailRecoveryKey/index.txt', { keyExists:true }) %> diff --git a/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/en.ftl b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/en.ftl new file mode 100644 index 00000000000..70a50ec8b72 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/en.ftl @@ -0,0 +1,5 @@ +postAddLinkedAccount-subject-2 = New account linked to your { -product-mozilla-account } +# Variables: +# $providerName (String) - The name of the provider, e.g. Apple, Google +postAddLinkedAccount-title-2 = Your { $providerName } account has been linked to your { -product-mozilla-account } +postAddLinkedAccount-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.mjml b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.mjml new file mode 100644 index 00000000000..300099a2726 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.mjml @@ -0,0 +1,18 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your <%- providerName %> account has been linked to your Mozilla account + + + + +<%- include('/partials/userInfo/index.mjml') %> +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postAddLinkedAccount-action", + buttonText: "Manage account" +}) %> +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.stories.ts b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.stories.ts new file mode 100644 index 00000000000..170290e32cd --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.stories.ts @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postAddLinkedAccount', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'postAddLinkedAccount', + 'Sent to notify the account is linked to another account.', + data, + includes +); + +export const PostAddLinkedAccountApple = createStory( + { + providerName: 'Apple', + }, + 'Linked to an Apple account' +); + +export const PostAddLinkedAccountGoogle = createStory( + { + providerName: 'Google', + }, + 'Linked to a Google account' +); diff --git a/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.ts b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.ts new file mode 100644 index 00000000000..1bd46078419 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.ts @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postAddLinkedAccount'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postAddLinkedAccount-subject-2', + message: 'New account linked to your Mozilla account', + }, + action: { + id: 'postAddLinkedAccount-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.txt b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.txt new file mode 100644 index 00000000000..e694f3c2ad5 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddLinkedAccount/index.txt @@ -0,0 +1,9 @@ +postAddLinkedAccount-title-2 = "Your <%- providerName %> account has been linked to your Mozilla account" + +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailNotAuthorized/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/en.ftl b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/en.ftl new file mode 100644 index 00000000000..9f3cc6b1e96 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/en.ftl @@ -0,0 +1,11 @@ +postAddRecoveryPhone-subject = Recovery phone added +postAddRecoveryPhone-preview = Account protected by two-step authentication +postAddRecoveryPhone-title-v2 = You added a recovery phone number +# Variables: +# $maskedLastFourPhoneNumber (String) - A bullet point mask with the last four digits of the user's phone number, e.g. ••••••1234 +postAddRecoveryPhone-description-v2 = You added { $maskedLastFourPhoneNumber } as your recovery phone number +# Links out to a support article about two factor authentication +postAddRecoveryPhone-how-protect = How this protects your account +postAddRecoveryPhone-how-protect-plaintext = How this protects your account: +postAddRecoveryPhone-enabled-device = You enabled it from: +postAddRecoveryPhone-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.mjml b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.mjml new file mode 100644 index 00000000000..16ddc5139b0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.mjml @@ -0,0 +1,33 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + You added a recovery phone number + + + + You added <%- maskedLastFourPhoneNumber %> as your recovery phone number + + + + How this protects your account + + + + + + You enabled it from: + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postAddRecoveryPhone-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailResetPassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.stories.ts b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.stories.ts new file mode 100644 index 00000000000..6bca935d085 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.stories.ts @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postAddRecoveryPhone', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + maskedLastFourPhoneNumber: '••••••1234', + link: 'http://localhost:3030/settings', + resetLink: 'http://localhost:3030/reset_password', + twoFactorSupportLink: + 'https://support.mozilla.org/kb/secure-mozilla-account-two-step-authentication', +}; + +const createStory = storyWithProps( + 'postAddRecoveryPhone', + 'Sent when a user adds their phone number as a backup recovery method', + data, + includes +); + +export const PostAddRecoveryPhone = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.ts b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.ts new file mode 100644 index 00000000000..78a6b74ca00 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.ts @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + maskedLastFourPhoneNumber: string; + link: string; + resetLink: string; + twoFactorSupportLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postAddRecoveryPhone'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postAddRecoveryPhone-subject', + message: 'Recovery phone added', + }, + action: { + id: 'postAddRecoveryPhone-action', + message: 'Manage account', + }, + preview: { + id: 'postAddRecoveryPhone-preview', + message: 'Account protected by two-step authentication', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.txt b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.txt new file mode 100644 index 00000000000..e1584c3c9c1 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddRecoveryPhone/index.txt @@ -0,0 +1,15 @@ +postAddRecoveryPhone-title-v2 = "You added a recovery phone number" + +postAddRecoveryPhone-description-v2 = "You added <%- maskedLastFourPhoneNumber %> as your recovery phone number" + +postAddRecoveryPhone-how-protect-plaintext = "How this protects your account:" +<%- twoFactorSupportLink %> + +postAddRecoveryPhone-enabled-device = "You enabled it from:" +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailResetPassword/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/en.ftl b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/en.ftl new file mode 100644 index 00000000000..4b2ca922ab1 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/en.ftl @@ -0,0 +1,13 @@ +postAddTwoStepAuthentication-preview = Your account is protected +postAddTwoStepAuthentication-subject-v3 = Two-step authentication is on +postAddTwoStepAuthentication-title-2 = You turned on two-step authentication +# After the colon, there is a description of the device that the user used to enable two-step authentication +postAddTwoStepAuthentication-from-device-v2 = You requested this from: +postAddTwoStepAuthentication-action = Manage account +postAddTwoStepAuthentication-code-required-v4 = Security codes from your authenticator app are now required every time you sign in. +postAddTwoStepAuthentication-recovery-method-codes = You also added backup authentication codes as your recovery method. +# Variables: +# $maskedPhoneNumber (String) - A bullet point mask with the last four digits of the user's phone number, e.g. ••••••1234 +postAddTwoStepAuthentication-recovery-method-phone = You also added { $maskedPhoneNumber } as your recovery phone number. +postAddTwoStepAuthentication-how-protects-link = How this protects your account +postAddTwoStepAuthentication-how-protects-plaintext = How this protects your account: diff --git a/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.mjml b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.mjml new file mode 100644 index 00000000000..91eed10c3e5 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.mjml @@ -0,0 +1,48 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + You turned on two-step authentication + + + + Security codes from your authenticator app are now required every time you sign in. + + + <% if (locals.recoveryMethod === 'codes') { %> + + + You also added backup authentication codes as your recovery method. + + + <% } %> + + <% if (locals.recoveryMethod === 'phone' && locals.maskedPhoneNumber) { %> + + You also added <%- maskedPhoneNumber %> as your recovery phone number. + + <% } %> + + + + How this protects your account + + + + + You requested this from: + + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postAddTwoStepAuthentication-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.stories.ts b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.stories.ts new file mode 100644 index 00000000000..865312e47ed --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.stories.ts @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postAddTwoStepAuthentication', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + twoFactorSupportLink: + 'https://support.mozilla.org/kb/secure-mozilla-account-two-step-authentication', +}; + +const createStory = storyWithProps( + 'postAddTwoStepAuthentication', + 'Sent to notify that two step authentication was enabled.', + data, + includes +); + +export const postAddTwoStepAuthenticationWithCodes = createStory( + { + recoveryMethod: 'codes', + }, + 'With backup authentication codes' +); + +export const postAddTwoStepAuthenticationWithPhone = createStory( + { + recoveryMethod: 'phone', + maskedPhoneNumber: '••••••1234', + }, + 'With recovery phone' +); diff --git a/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.ts b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.ts new file mode 100644 index 00000000000..e2c733bb45a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.ts @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + twoFactorSupportLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postAddTwoStepAuthentication'; +export const version = 11; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postAddTwoStepAuthentication-subject-v3', + message: 'Two-step authentication is on', + }, + preview: { + id: 'postAddTwoStepAuthentication-preview', + message: 'Your account is protected', + }, + action: { + id: 'postAddTwoStepAuthentication-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.txt b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.txt new file mode 100644 index 00000000000..f9ac3456a91 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postAddTwoStepAuthentication/index.txt @@ -0,0 +1,21 @@ +postAddTwoStepAuthentication-title-2 = "You turned on two-step authentication" + +postAddTwoStepAuthentication-code-required-v4 = "Security codes from your authenticator app are now required every time you sign in." + +<% if (locals.recoveryMethod === 'codes') { %> +postAddTwoStepAuthentication-recovery-method-codes = "You also added backup authentication codes as your recovery method." +<% } %> + +<% if (locals.recoveryMethod === 'phone' && locals.maskedPhoneNumber) { %> +postAddTwoStepAuthentication-recovery-method-phone = "You also added <%- maskedPhoneNumber %> as your recovery phone number." +<% } %> + +postAddTwoStepAuthentication-how-protects-plaintext = "How this protects your account:" +<%- twoFactorSupportLink %> + +postAddTwoStepAuthentication-from-device-v2 = "You requested this from:" +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailChangePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/en.ftl b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/en.ftl new file mode 100644 index 00000000000..5b30c275f0b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/en.ftl @@ -0,0 +1,5 @@ +postChangeAccountRecovery-subject = Account recovery key changed +postChangeAccountRecovery-title = You changed your account recovery key +postChangeAccountRecovery-body-part1 = You now have a new account recovery key. Your previous key was deleted. +postChangeAccountRecovery-body-part2 = Save this new key in a safe place — you’ll need it to restore your encrypted browsing data if you forget your password. +postChangeAccountRecovery-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.mjml b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.mjml new file mode 100644 index 00000000000..6d746bc5a3b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.mjml @@ -0,0 +1,38 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<% const { uaBrowser, uaOS, uaOSVersion } = device; %> + + + + + You changed your account recovery key + + + + + + + + + + You now have a new account recovery key. Your previous key was deleted. + + + + + Save this new key in a safe place — you’ll need it to restore your encrypted browsing data if you forget your password. + + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postChangeAccountRecovery-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailRecoveryKey/index.mjml', { + keyExists:true +}) %> diff --git a/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.stories.ts b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.stories.ts new file mode 100644 index 00000000000..1f04abd7b9a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.stories.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postChangeAccountRecovery', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + revokeAccountRecoveryLink: 'http://localhost:3030/settings/#recovery-key', + supportLink: '', +}; + +const createStory = storyWithProps( + 'postChangeAccountRecovery', + 'Sent when the user changes their account recovery key', + data, + includes +); + +export const PostAddAccountRecovery = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.ts b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.ts new file mode 100644 index 00000000000..82f8b7d929b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.ts @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + revokeAccountRecoveryLink: string; + supportLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postChangeAccountRecovery'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postChangeAccountRecovery-subject', + message: 'Account recovery key changed', + }, + action: { + id: 'postChangeAccountRecovery-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.txt b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.txt new file mode 100644 index 00000000000..a06c8dab89a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeAccountRecovery/index.txt @@ -0,0 +1,9 @@ +postChangeAccountRecovery-title = "You changed your account recovery key" + +postChangeAccountRecovery-body-part1 = "You now have a new account recovery key. Your previous key was deleted." + +postChangeAccountRecovery-body-part2 = "Save this new key in a safe place — you’ll need it to restore your encrypted browsing data if you forget your password." + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailRecoveryKey/index.txt', { keyExists: true}) %> diff --git a/libs/accounts/email-renderer/src/templates/postChangePrimary/en.ftl b/libs/accounts/email-renderer/src/templates/postChangePrimary/en.ftl new file mode 100644 index 00000000000..b253377f8f3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangePrimary/en.ftl @@ -0,0 +1,6 @@ +postChangePrimary-subject = Primary email updated +postChangePrimary-title = New primary email +# Variables: +# $email (String) - A user's email address +postChangePrimary-description-2 = You have successfully changed your primary email to { $email }. This address is now your username for signing in to your { -product-mozilla-account }, as well as receiving security notifications and sign-in confirmations. +postChangePrimary-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postChangePrimary/index.mjml b/libs/accounts/email-renderer/src/templates/postChangePrimary/index.mjml new file mode 100644 index 00000000000..126d83540bb --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangePrimary/index.mjml @@ -0,0 +1,22 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + New primary email + + + + You have successfully changed your primary email to <%- email %>. This address is now your username for signing in to your Mozilla account, as well as receiving security notifications and sign-in confirmations. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postChangePrimary-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postChangePrimary/index.stories.ts b/libs/accounts/email-renderer/src/templates/postChangePrimary/index.stories.ts new file mode 100644 index 00000000000..85c4a83ca1d --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangePrimary/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postChangePrimary', +} as Meta; + +const data = { + email: 'foo@bar.com', + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'postChangePrimary', + 'Sent to new primary email when it has been updated', + data, + includes +); + +export const PostChangePrimary = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postChangePrimary/index.ts b/libs/accounts/email-renderer/src/templates/postChangePrimary/index.ts new file mode 100644 index 00000000000..1c100862de4 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangePrimary/index.ts @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + email: string; + link: string; + passwordChangeLink: string; +}; + +export const template = 'postChangePrimary'; +export const version = 5; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postChangePrimary-subject', + message: 'Primary email updated', + }, + action: { + id: 'postChangePrimary-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postChangePrimary/index.txt b/libs/accounts/email-renderer/src/templates/postChangePrimary/index.txt new file mode 100644 index 00000000000..0a32cafc1a2 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangePrimary/index.txt @@ -0,0 +1,9 @@ +postChangePrimary-title = "New primary email" + +postChangePrimary-description-2 = "You have successfully changed your primary email to <%- email %>. This address is now your username for signing in to your Mozilla account, as well as receiving security notifications and sign-in confirmations." + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/changePassword/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/en.ftl b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/en.ftl new file mode 100644 index 00000000000..7efee668be7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/en.ftl @@ -0,0 +1,5 @@ +postChangeRecoveryPhone-subject = Recovery phone updated +postChangeRecoveryPhone-preview = Account protected by two-step authentication +postChangeRecoveryPhone-title = You changed your recovery phone +postChangeRecoveryPhone-description = You now have a new recovery phone. Your previous phone number was deleted. +postChangeRecoveryPhone-requested-device = You requested it from: diff --git a/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.mjml b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.mjml new file mode 100644 index 00000000000..8e311f62c2d --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.mjml @@ -0,0 +1,25 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + You changed your recovery phone + + + + + + + + You now have a new recovery phone. Your previous phone number was deleted. + + + You requested it from: + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/automatedEmailResetPassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.stories.ts b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.stories.ts new file mode 100644 index 00000000000..cbe31b154fa --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postChangeRecoveryPhone', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + resetLink: 'http://localhost:3030/reset_password', +}; + +const createStory = storyWithProps( + 'postChangeRecoveryPhone', + 'Sent when a user updates their recovery phone number.', + data, + includes +); + +export const PostChangeRecoveryPhone = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.ts b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.ts new file mode 100644 index 00000000000..67659e28eae --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.ts @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + resetLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postChangeRecoveryPhone'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postChangeRecoveryPhone-subject', + message: 'Recovery phone updated', + }, + preview: { + id: 'postChangeRecoveryPhone-preview', + message: 'Account protected by two-step authentication', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.txt b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.txt new file mode 100644 index 00000000000..86679d3286b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeRecoveryPhone/index.txt @@ -0,0 +1,10 @@ +postChangeRecoveryPhone-title = "You changed your recovery phone" + +postChangeRecoveryPhone-description = "You now have a new recovery phone. Your previous phone number was deleted." + +postChangeRecoveryPhone-requested-device = "You requested it from:" +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/automatedEmailResetPassword/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/en.ftl b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/en.ftl new file mode 100644 index 00000000000..d6197d0b947 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/en.ftl @@ -0,0 +1,9 @@ +postChangeTwoStepAuthentication-preview = Your account is protected +postChangeTwoStepAuthentication-subject = Two-step authentication updated +postChangeTwoStepAuthentication-title = Two-step authentication has been updated +postChangeTwoStepAuthentication-use-new-account = You now need to use the new { -product-mozilla-account } entry in your authenticator app. The older one will no longer work and you can remove it. +# After the colon, there is a description of the device that the user used to enable two-step authentication +postChangeTwoStepAuthentication-from-device = You requested this from: +postChangeTwoStepAuthentication-action = Manage account +postChangeTwoStepAuthentication-how-protects-link = How this protects your account +postChangeTwoStepAuthentication-how-protects-plaintext = How this protects your account: diff --git a/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.mjml b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.mjml new file mode 100644 index 00000000000..f0fc5dfc809 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.mjml @@ -0,0 +1,34 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Two-step authentication has been updated + + + + You now need to use the new Mozilla account entry in your authenticator app. The older one will no longer work and you can remove it. + + + + + How this protects your account + + + + + You requested this from: + + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postChangeTwoStepAuthentication-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.stories.ts b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.stories.ts new file mode 100644 index 00000000000..03686711dc2 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.stories.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postChangeTwoStepAuthentication', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + twoFactorSupportLink: + 'https://support.mozilla.org/kb/secure-mozilla-account-two-step-authentication', +}; + +const createStory = storyWithProps( + 'postChangeTwoStepAuthentication', + 'Sent to notify that two step authentication was updated.', + data, + includes +); + +export const Default = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.ts b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.ts new file mode 100644 index 00000000000..969267ae8ba --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.ts @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + twoFactorSupportLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postChangeTwoStepAuthentication'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postChangeTwoStepAuthentication-subject', + message: 'Two-step authentication updated', + }, + preview: { + id: 'postChangeTwoStepAuthentication-preview', + message: 'Your account is protected', + }, + action: { + id: 'postChangeTwoStepAuthentication-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.txt b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.txt new file mode 100644 index 00000000000..0cde8224afd --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postChangeTwoStepAuthentication/index.txt @@ -0,0 +1,13 @@ +postChangeTwoStepAuthentication-title-2 = "Two-step authentication has been updated" + +postChangeTwoStepAuthentication-use-new-account = "You now need to use the new Mozilla account entry in your authenticator app. The older one will no longer work and you can remove it." + +postChangeTwoStepAuthentication-how-protects-plaintext = "How this protects your account:" +<%- twoFactorSupportLink %> + +postChangeTwoStepAuthentication-from-device-v2 = "You requested this from:" +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailChangePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/en.ftl b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/en.ftl new file mode 100644 index 00000000000..a9730c806a3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/en.ftl @@ -0,0 +1,7 @@ +postConsumeRecoveryCode-title-3 = Your backup authentication code was used to confirm a password reset +# After the colon, there is description of the device that the backup authentication code was used on +# E.g., Firefox Nightly on Mac OSX, Thursday Sept 2, 2024 +postConsumeRecoveryCode-description-3 = Code used from: +postConsumeRecoveryCode-action = Manage account +postConsumeRecoveryCode-subject-v3 = Backup authentication code used +postConsumeRecoveryCode-preview = Check to make sure this was you diff --git a/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.mjml b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.mjml new file mode 100644 index 00000000000..ea3fb4f7344 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.mjml @@ -0,0 +1,24 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your backup authentication code was used to confirm a password reset + + + + Code used from: + + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postConsumeRecoveryCode-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailResetPasswordTwoFactor/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.stories.ts b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.stories.ts new file mode 100644 index 00000000000..468ad7ff105 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.stories.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postConsumeRecoveryCode', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + resetLink: 'http://localhost:3030/reset_password', + twoFactorSettingsLink: + 'http://localhost:3030/settings#two-step-authentication', +}; + +const createStory = storyWithProps( + 'postConsumeRecoveryCode', + 'Sent when user has used a backup authentication code to authorize a password reset', + data, + includes +); + +export const PostConsumeRecoveryCode = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.ts b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.ts new file mode 100644 index 00000000000..df3daea68b9 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.ts @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + resetLink: string; + twoFactorSettingsLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postConsumeRecoveryCode'; +export const version = 8; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postConsumeRecoveryCode-subject-v3', + message: 'Backup authentication code used', + }, + action: { + id: 'postConsumeRecoveryCode-action', + message: 'Manage account', + }, + preview: { + id: 'postConsumeRecoveryCode-preview', + message: 'Check to make sure this was you', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.txt b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.txt new file mode 100644 index 00000000000..b1b4741a5c3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postConsumeRecoveryCode/index.txt @@ -0,0 +1,9 @@ +postConsumeRecoveryCode-title-3 = "Your backup authentication code was used to confirm a password reset" + +postConsumeRecoveryCode-description-3 = "Code used from:" + +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailResetPasswordTwoFactor/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/en.ftl b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/en.ftl new file mode 100644 index 00000000000..ce8e07bc1ac --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/en.ftl @@ -0,0 +1,5 @@ +postNewRecoveryCodes-subject-2 = New backup authentication codes created +postNewRecoveryCodes-title-2 = You created new backup authentication codes +# After the colon, there is information about the device that the authentication codes were created on +postNewRecoveryCodes-description-2 = They were created on: +postNewRecoveryCodes-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.mjml b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.mjml new file mode 100644 index 00000000000..1cbdd4ace30 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.mjml @@ -0,0 +1,24 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + You created new backup authentication codes + + + + They were created on: + + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postNewRecoveryCodes-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.stories.ts b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.stories.ts new file mode 100644 index 00000000000..315ecbc39a9 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postNewRecoveryCodes', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'postNewRecoveryCodes', + 'Sent when user has created new backup authentication codes, resetting their existing ones', + data, + includes +); + +export const PostNewRecoveryCodes = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.ts b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.ts new file mode 100644 index 00000000000..5ab5b5cfefc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.ts @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postNewRecoveryCodes'; +export const version = 7; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postNewRecoveryCodes-subject-2', + message: 'New backup authentication codes created', + }, + action: { + id: 'postNewRecoveryCodes-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.txt b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.txt new file mode 100644 index 00000000000..285ca422d7f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postNewRecoveryCodes/index.txt @@ -0,0 +1,9 @@ +postNewRecoveryCodes-title-2 = "You created new backup authentication codes" + +postNewRecoveryCodes-description-2 = "They were created on:" + +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailChangePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/en.ftl b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/en.ftl new file mode 100644 index 00000000000..047c62dded3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/en.ftl @@ -0,0 +1,5 @@ +postRemoveAccountRecovery-subject-2 = Account recovery key deleted +postRemoveAccountRecovery-title-3 = You deleted your account recovery key +postRemoveAccountRecovery-body-part1 = Your account recovery key is required to restore your encrypted browsing data if you forget your password. +postRemoveAccountRecovery-body-part2 = If you haven’t already, create a new account recovery key in your account settings to prevent losing your saved passwords, bookmarks, browsing history, and more. +postRemoveAccountRecovery-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/includes.json b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/includes.json new file mode 100644 index 00000000000..ff58792d658 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/includes.json @@ -0,0 +1,10 @@ +{ + "subject": { + "id": "postRemoveAccountRecovery-subject-2", + "message": "Account recovery key deleted" + }, + "action": { + "id": "postRemoveAccountRecovery-action", + "message": "Manage account" + } +} diff --git a/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.mjml b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.mjml new file mode 100644 index 00000000000..4eb393f086a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.mjml @@ -0,0 +1,35 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + You deleted your account recovery key + + + + + + + + + Your account recovery key is required to restore your encrypted browsing data if you forget your password. + + + + + If you haven’t already, create a new account recovery key in your account settings to prevent losing your saved passwords, bookmarks, browsing history, and more. + + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postRemoveAccountRecovery-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailRecoveryKey/index.mjml', { + keyExists: false +}) %> diff --git a/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.stories.ts b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.stories.ts new file mode 100644 index 00000000000..fcc5674b0a4 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postRemoveAccountRecovery', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'postRemoveAccountRecovery', + 'Sent when an account recovery key is removed.', + data, + includes +); + +export const PostRemoveAccountRecovery = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.ts b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.ts new file mode 100644 index 00000000000..3c02cb16f8e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.ts @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postRemoveAccountRecovery'; +export const version = 8; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postRemoveAccountRecovery-subject-2', + message: 'Account recovery key deleted', + }, + action: { + id: 'postRemoveAccountRecovery-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.txt b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.txt new file mode 100644 index 00000000000..84343f46934 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveAccountRecovery/index.txt @@ -0,0 +1,9 @@ +postRemoveAccountRecovery-title-3 = "You deleted your account recovery key" + +postRemoveAccountRecovery-body-part1 = "Your account recovery key is required to restore your encrypted browsing data if you forget your password." + +postRemoveAccountRecovery-body-part2 = "If you haven’t already, create a new account recovery key in your account settings to prevent losing your saved passwords, bookmarks, browsing history, and more." + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailRecoveryKey/index.txt', {keyExists:false}) %> diff --git a/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/en.ftl b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/en.ftl new file mode 100644 index 00000000000..95cdf7dd674 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/en.ftl @@ -0,0 +1,6 @@ +postRemoveRecoveryPhone-subject = Recovery phone removed +postRemoveRecoveryPhone-preview = Account protected by two-step authentication +postRemoveRecoveryPhone-title = Recovery phone removed +postRemoveRecoveryPhone-description-v2 = Your recovery phone has been removed from your two-step authentication settings. +postRemoveRecoveryPhone-description-extra = You can still use your backup authentication codes to sign in if you arenʼt able to use your authenticator app. +postRemoveRecoveryPhone-requested-device = You requested it from: diff --git a/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.mjml b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.mjml new file mode 100644 index 00000000000..54ea5b46b1b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.mjml @@ -0,0 +1,28 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Recovery phone removed + + + + + + + + Your recovery phone has been removed from your two-step authentication settings. + + + You can still use your backup authentication codes to sign in if you arenʼt able to use your authenticator app. + + + You requested it from: + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/automatedEmailResetPassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.stories.ts b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.stories.ts new file mode 100644 index 00000000000..5b6d525c685 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; + +export default { + title: 'FxA Emails/Templates/postRemoveRecoveryPhone', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + resetLink: 'http://localhost:3030/reset_password', +}; + +const createStory = storyWithProps( + 'postRemoveRecoveryPhone', + 'Sent when a user removes their recovery phone number.', + data, + includes +); + +export const postRemoveRecoveryPhone = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.ts b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.ts new file mode 100644 index 00000000000..6a8c1f6d6bc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.ts @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +export type TemplateData = { + resetLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postRemoveRecoveryPhone'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postRemoveRecoveryPhone-subject', + message: 'Recovery phone removed', + }, + preview: { + id: 'postRemoveRecoveryPhone-preview', + message: 'Account protected by two-step authentication', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.txt b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.txt new file mode 100644 index 00000000000..ff1b5aa1ad5 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveRecoveryPhone/index.txt @@ -0,0 +1,11 @@ +postRemoveRecoveryPhone-title = "Recovery phone removed" + +postRemoveRecoveryPhone-description-v2 = "Your recovery phone has been removed from your two-step authentication settings." +postRemoveRecoveryPhone-description-extra = "You can still use your backup authentication codes to sign in if you arenʼt able to use your authenticator app." + +postRemoveRecoveryPhone-requested-device = "You requested it from:" +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/automatedEmailResetPassword/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postRemoveSecondary/en.ftl b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/en.ftl new file mode 100644 index 00000000000..9a77b840a64 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/en.ftl @@ -0,0 +1,6 @@ +postRemoveSecondary-subject = Secondary email removed +postRemoveSecondary-title = Secondary email removed +# Variables: +# $secondaryEmail (String) - A user's email address +postRemoveSecondary-description-2 = You have successfully removed { $secondaryEmail } as a secondary email from your { -product-mozilla-account }. Security notifications and sign-in confirmations will no longer be delivered to this address. +postRemoveSecondary-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.mjml b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.mjml new file mode 100644 index 00000000000..ee31337f0e5 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.mjml @@ -0,0 +1,31 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Secondary email removed + + + + + + + + + You have successfully removed <%- secondaryEmail %> as a secondary + email from your Mozilla account. Security notifications and sign-in + confirmations will no longer be delivered to this address. + + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postRemoveSecondary-action", + buttonText: "Manage account" +}) %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.stories.ts b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.stories.ts new file mode 100644 index 00000000000..a3d27e08633 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.stories.ts @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postRemoveSecondary', +} as Meta; + +const data = { + link: 'http://localhost:3030/settings', + secondaryEmail: 'secondary@email.com', +}; + +const createStory = storyWithProps( + 'postRemoveSecondary', + 'Sent to primary email after secondary email is removed.', + data, + includes +); + +export const PostRemoveSecondary = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.ts b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.ts new file mode 100644 index 00000000000..84bf3a56481 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.ts @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + secondaryEmail: string; +}; + +export const template = 'postRemoveSecondary'; +export const version = 5; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postRemoveSecondary-subject', + message: 'Secondary email removed', + }, + action: { + id: 'postRemoveSecondary-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.txt b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.txt new file mode 100644 index 00000000000..66afed74f01 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveSecondary/index.txt @@ -0,0 +1,9 @@ +postRemoveSecondary-title = "Secondary email removed" + +postRemoveSecondary-description-2 = "You have successfully removed <%- secondaryEmail %> as a secondary email from your Mozilla account. Security notifications and sign-in confirmations will no longer be delivered to this address." + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/en.ftl b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/en.ftl new file mode 100644 index 00000000000..f16687adeb3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/en.ftl @@ -0,0 +1,6 @@ +postRemoveTwoStepAuthentication-subject-line-2 = Two-step authentication turned off +postRemoveTwoStepAuthentication-title-2 = You turned off two-step authentication +# After the colon is a description of the device the user used to disable two-step authentication +postRemoveTwoStepAuthentication-from-device = You disabled it from: +postRemoveTwoStepAuthentication-action = Manage account +postRemoveTwoStepAuthentication-not-required-2 = You no longer need security codes from your authentication app when you sign in. diff --git a/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/includes.json b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/includes.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.mjml b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.mjml new file mode 100644 index 00000000000..7cbb64cfecb --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.mjml @@ -0,0 +1,26 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + You turned off two-step authentication + + + You no longer need security codes from your authentication app when you sign in. + + + You disabled it from: + + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postRemoveTwoStepAuthentication-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.stories.ts b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.stories.ts new file mode 100644 index 00000000000..bebd10165ee --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postRemoveTwoStepAuthentication', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + passwordChangeLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'postRemoveTwoStepAuthentication', + 'Sent when a user disables two-step authentication.', + data, + includes +); + +export const PostRemoveAccountRecovery = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.ts b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.ts new file mode 100644 index 00000000000..b25fe253215 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.ts @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + passwordChangeLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postRemoveTwoStepAuthentication'; +export const version = 9; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postRemoveTwoStepAuthentication-subject-line-2', + message: 'Two-step authentication turned off', + }, + action: { + id: 'postRemoveTwoStepAuthentication-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.txt b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.txt new file mode 100644 index 00000000000..5b066096ff7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postRemoveTwoStepAuthentication/index.txt @@ -0,0 +1,11 @@ +postRemoveTwoStepAuthentication-title-2 = "You turned off two-step authentication" + +postRemoveTwoStepAuthentication-not-required-2 = "You no longer need security codes from your authentication app when you sign in." + +postRemoveTwoStepAuthentication-from-device = "You disabled it from:" + +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailChangePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/en.ftl b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/en.ftl new file mode 100644 index 00000000000..694d7cb288f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/en.ftl @@ -0,0 +1,6 @@ +postSigninRecoveryCode-subject = Backup authentication code used to sign in +postSigninRecoveryCode-preview = Confirm account activity +postSigninRecoveryCode-title = Your backup authentication code was used to sign in +postSigninRecoveryCode-description = If you didn’t do this, you should change your password immediately to keep your account safe. +postSigninRecoveryCode-device = You signed in from: +postSigninRecoveryCode-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.mjml b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.mjml new file mode 100644 index 00000000000..1292f526f6b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.mjml @@ -0,0 +1,27 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your backup authentication code was used to sign in + + + + If you didn’t do this, you should change your password immediately to keep your account safe. + + + + You signed in from: + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postSigninRecoveryCode-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailResetPassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.stories.ts b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.stories.ts new file mode 100644 index 00000000000..d907e78fc91 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; + +export default { + title: 'FxA Emails/Templates/postSigninRecoveryCode', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + resetLink: 'http://localhost:3030/reset_password', +}; + +const createStory = storyWithProps( + 'postSigninRecoveryCode', + 'Sent when a user uses one of their backup authentication codes to sign in as an alternative to their authenticator app.', + data, + includes +); + +export const postSigninRecoveryCode = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.ts b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.ts new file mode 100644 index 00000000000..25113770d0e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.ts @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + resetLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postSigninRecoveryCode'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postSigninRecoveryCode-subject', + message: 'Backup authentication code used to sign in', + }, + action: { + id: 'postSigninRecoveryCode-action', + message: 'Manage account', + }, + preview: { + id: 'postSigninRecoveryCode-preview', + message: 'Confirm account activity', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.txt b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.txt new file mode 100644 index 00000000000..9ae60f4983a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryCode/index.txt @@ -0,0 +1,12 @@ +postSigninRecoveryCode-title = "Your backup authentication code was used to sign in" + +postSigninRecoveryCode-description = "If you didn’t do this, you should change your password immediately to keep your account safe." + +postSigninRecoveryCode-device = "You signed in from:" +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailResetPassword/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/en.ftl b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/en.ftl new file mode 100644 index 00000000000..c7e3923c52c --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/en.ftl @@ -0,0 +1,6 @@ +postSigninRecoveryPhone-subject = Recovery phone used to sign in +postSigninRecoveryPhone-preview = Confirm account activity +postSigninRecoveryPhone-title = Your recovery phone was used to sign in +postSigninRecoveryPhone-description = If you didn’t do this, you should change your password immediately to keep your account safe. +postSigninRecoveryPhone-device = You signed in from: +postSigninRecoveryPhone-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.mjml b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.mjml new file mode 100644 index 00000000000..e72ccf2afca --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.mjml @@ -0,0 +1,27 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your recovery phone was used to sign in + + + + If you didn’t do this, you should change your password immediately to keep your account safe. + + + + You signed in from: + + + +<%- include('/partials/userInfo/index.mjml') %> + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postSigninRecoveryPhone-action", + buttonText: "Manage account" +}) %> + +<%- include('/partials/automatedEmailResetPassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.stories.ts b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.stories.ts new file mode 100644 index 00000000000..f7f6c9a0e78 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; + +export default { + title: 'FxA Emails/Templates/postSigninRecoveryPhone', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/settings', + resetLink: 'http://localhost:3030/reset_password', +}; + +const createStory = storyWithProps( + 'postSigninRecoveryPhone', + 'Sent when a user uses their recovery phone to sign in as an alternative to their authenticator app.', + data, + includes +); + +export const postSigninRecoveryPhone = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.ts b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.ts new file mode 100644 index 00000000000..123bb27f5d3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.ts @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + resetLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'postSigninRecoveryPhone'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postSigninRecoveryPhone-subject', + message: 'Recovery phone used to sign in', + }, + action: { + id: 'postSigninRecoveryPhone-action', + message: 'Manage account', + }, + preview: { + id: 'postSigninRecoveryPhone-preview', + message: 'Confirm account activity', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.txt b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.txt new file mode 100644 index 00000000000..7ffc3578dcd --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postSigninRecoveryPhone/index.txt @@ -0,0 +1,12 @@ +postSigninRecoveryPhone-title = "Your recovery phone was used to sign in" + +postSigninRecoveryPhone-description = "If you didn’t do this, you should change your password immediately to keep your account safe." + +postSigninRecoveryPhone-device = "You signed in from:" +<%- include('/partials/userInfo/index.txt') %> + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/automatedEmailResetPassword/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postVerify/en.ftl b/libs/accounts/email-renderer/src/templates/postVerify/en.ftl new file mode 100644 index 00000000000..6319f8eeb1a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerify/en.ftl @@ -0,0 +1,7 @@ +postVerify-sub-title-3 = We’re delighted to see you! +postVerify-title-2 = Want to see the same tab on two devices? +postVerify-description-2 = It’s easy! Just install { -brand-firefox } on another device and log in to sync. It’s like magic! +postVerify-sub-description = (Psst… It also means you can get your bookmarks, passwords, and other { -brand-firefox } data everywhere you’re signed in.) +postVerify-subject-4 = Welcome to { -brand-mozilla }! +postVerify-setup-2 = Connect another device: +postVerify-action-2 = Connect another device diff --git a/libs/accounts/email-renderer/src/templates/postVerify/index.mjml b/libs/accounts/email-renderer/src/templates/postVerify/index.mjml new file mode 100644 index 00000000000..b75e6107e8f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerify/index.mjml @@ -0,0 +1,33 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + We’re delighted to see you! + + + Want to see the same tab on two devices? + + + + + + + + + It’s easy! Just install Firefox on another device and log in to sync. It’s like magic! + + + (Psst… It also means you can get your bookmarks, passwords, and other Firefox data everywhere you’re signed in.) + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postVerify-action-2", + buttonText: "Connect another device" +}) %> +<%- include('/partials/appBadges/index.mjml') %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postVerify/index.stories.ts b/libs/accounts/email-renderer/src/templates/postVerify/index.stories.ts new file mode 100644 index 00000000000..4ea250d7715 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerify/index.stories.ts @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postVerify', +} as Meta; + +const data = { + link: 'http://localhost:3030/connect_another_device', + desktopLink: 'https://firefox.com', + onDesktopOrTabletDevice: true, + productName: 'Firefox', +}; + +const createStory = storyWithProps( + 'postVerify', + 'Sent after account is confirmed during Sync registration on non-mobile and mobile devices.', + data, + includes +); + +export const PostVerifyDesktopTablet = createStory( + { + onDesktopOrTabletDevice: true, + }, + 'User is on desktop or tablet device' +); + +export const PostVerifyMobile = createStory( + { + onDesktopOrTabletDevice: false, + }, + 'User is on mobile device' +); diff --git a/libs/accounts/email-renderer/src/templates/postVerify/index.ts b/libs/accounts/email-renderer/src/templates/postVerify/index.ts new file mode 100644 index 00000000000..261a05316dc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerify/index.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + desktopLink: string; + onDesktopOrTabletDevice: boolean; + productName: string; +}; + +export const template = 'postVerify'; +export const version = 8; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postVerify-subject-4', + message: 'Welcome to Mozilla!', + }, + action: { + id: 'postVerify-action-2', + message: 'Connect another device', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postVerify/index.txt b/libs/accounts/email-renderer/src/templates/postVerify/index.txt new file mode 100644 index 00000000000..c2266352577 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerify/index.txt @@ -0,0 +1,13 @@ +postVerify-sub-title-3 = "We’re delighted to see you!" + +postVerify-title-2 = "Want to see the same tab on two devices?" + +postVerify-description-2 = "It’s easy! Just install Firefox on another device and log in to sync. It’s like magic!" + +postVerify-sub-description = "(Psst… It also means you can get your bookmarks, passwords, and other Firefox data everywhere you’re signed in.)" + +postVerify-setup-2 = "Connect another device:" +<%- link %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/postVerifySecondary/en.ftl b/libs/accounts/email-renderer/src/templates/postVerifySecondary/en.ftl new file mode 100644 index 00000000000..2a390c3601a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerifySecondary/en.ftl @@ -0,0 +1,6 @@ +postVerifySecondary-subject = Secondary email added +postVerifySecondary-title = Secondary email added +# Variables: +# $secondaryEmail (String) - A user's secondary email address +postVerifySecondary-content-3 = You have successfully confirmed { $secondaryEmail } as a secondary email for your { -product-mozilla-account }. Security notifications and sign-in confirmations will now be delivered to both email addresses. +postVerifySecondary-action = Manage account diff --git a/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.mjml b/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.mjml new file mode 100644 index 00000000000..bac57556d70 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.mjml @@ -0,0 +1,33 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + Secondary email added + + + + + + + + + + You have successfully confirmed <%- secondaryEmail %> as a secondary + email for your Mozilla account. Security notifications and sign-in + confirmations will now be delivered to both email addresses. + + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "postVerifySecondary-action", + buttonText: "Manage account" +}) %> +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.stories.ts b/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.stories.ts new file mode 100644 index 00000000000..b32223f272f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/postVerifySecondary', +} as Meta; + +const data = { + link: 'http://localhost:3030/settings', + secondaryEmail: 'secondary@email.com', + passwordChangeLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'postVerifySecondary', + 'Sent to primary email after secondary email is verified.', + data, + includes +); + +export const PostVerifySecondary = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.ts b/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.ts new file mode 100644 index 00000000000..92c3196993d --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.ts @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + secondaryEmail: string; + passwordChangeLink: string; +}; +export const template = 'postVerifySecondary'; +export const version = 6; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'postVerifySecondary-subject', + message: 'Secondary email added', + }, + action: { + id: 'postVerifySecondary-action', + message: 'Manage account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.txt b/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.txt new file mode 100644 index 00000000000..a1dac2d1934 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/postVerifySecondary/index.txt @@ -0,0 +1,7 @@ +postVerifySecondary-title = "Secondary email added" + +postVerifySecondary-content-3 = "You have successfully confirmed <%- secondaryEmail %> as a secondary email for your Mozilla account. Security notifications and sign-in confirmations will now be delivered to both email addresses." + +<%- include('/partials/manageAccount/index.txt') %> + +<%- include('/partials/changePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/recovery/en.ftl b/libs/accounts/email-renderer/src/templates/recovery/en.ftl new file mode 100644 index 00000000000..3055d9e5fd9 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/recovery/en.ftl @@ -0,0 +1,7 @@ +recovery-subject = Reset your password +recovery-title-2 = Forgot your password? +# Information on the device, location, and date and time of the request that triggered the email follows. +recovery-request-origin-2 = We received a request for a password change on your { -product-mozilla-account } from: +recovery-new-password-button = Create a new password by clicking the button below. This link will expire within the next hour. +recovery-copy-paste = Create a new password by copying and pasting the URL below into your browser. This link will expire within the next hour. +recovery-action = Create new password diff --git a/libs/accounts/email-renderer/src/templates/recovery/index.mjml b/libs/accounts/email-renderer/src/templates/recovery/index.mjml new file mode 100644 index 00000000000..e2451e53629 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/recovery/index.mjml @@ -0,0 +1,35 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public # +License, v. 2.0. If a copy of the MPL was not distributed with this # file, You +can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Forgot your password? + + + + + We received a request for a password change on your Mozilla account from: + + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + + Create a new password by clicking the button below. This link will expire within the next hour. + + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "recovery-action", + buttonText: "Create new password" +}) %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/recovery/index.stories.ts b/libs/accounts/email-renderer/src/templates/recovery/index.stories.ts new file mode 100644 index 00000000000..79c397c70a6 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/recovery/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/recovery', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/complete_reset_password', +}; + +const createStory = storyWithProps( + 'recovery', + 'Sent when user begins password reset flow', + data, + includes +); + +export const Recovery = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/recovery/index.ts b/libs/accounts/email-renderer/src/templates/recovery/index.ts new file mode 100644 index 00000000000..8d6848f7cad --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/recovery/index.ts @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; +export const template = 'recovery'; +export const version = 6; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'recovery-subject', + message: 'Reset your password', + }, + action: { + id: 'recovery-action', + message: 'Create new password', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/recovery/index.txt b/libs/accounts/email-renderer/src/templates/recovery/index.txt new file mode 100644 index 00000000000..cfe6f694d70 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/recovery/index.txt @@ -0,0 +1,12 @@ +recovery-title-2 = "Forgot your password?" + +recovery-request-origin-2 = "We received a request for a password change on your Mozilla account from:" + +<%- include('/partials/userInfo/index.txt') %> + +recovery-copy-paste = "Create a new password by copying and pasting the URL below into your browser. This link will expire within the next hour." + +<%- link %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/en.ftl new file mode 100644 index 00000000000..95e7f1bb18f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/en.ftl @@ -0,0 +1,9 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionAccountDeletion-subject = Your { $productName } subscription has been cancelled +subscriptionAccountDeletion-title = Sorry to see you go +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +# $invoiceTotal (String) - The amount of the subscription invoice, including currency, e.g. $10.00 +# $invoiceDateOnly (String) - The date of the next invoice, e.g. 01/20/2016 +subscriptionAccountDeletion-content-cancelled-2 = You recently deleted your { -product-mozilla-account }. As a result, we’ve cancelled your { $productName } subscription. Your final payment of { $invoiceTotal } was paid on { $invoiceDateOnly }. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.mjml new file mode 100644 index 00000000000..ddfd3e62c2b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.mjml @@ -0,0 +1,23 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Sorry to see you go + + + + + + + + + You recently deleted your Mozilla account. As a result, we’ve cancelled your <%- productName %> subscription. Your final payment of <%- invoiceTotal %> was paid on <%- invoiceDateOnly %>. + + + + + +<%- include ('/partials/cancellationSurvey/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.stories.ts new file mode 100644 index 00000000000..14b73ce8064 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.stories.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionAccountDeletion', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + isCancellationEmail: true, + invoiceTotal: '$20', + invoiceDateOnly: '11/13/2021', + cancellationSurveryUrl: + 'https://survey.alchemer.com/s3/6534408/Privacy-Security-Product-Cancellation-of-Service-Q4-21', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionAccountDeletion', + 'Sent when a user with an active subscription deletes their Mozilla account.', + data, + includes +); + +export const SubscriptionAccountDeletion = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.ts new file mode 100644 index 00000000000..f8218816c32 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.ts @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; + isCancellationEmail: boolean; + invoiceTotal: string; + invoiceDateOnly: string; + cancellationSurveryUrl: string; +}; + +export const template = 'subscriptionAccountDeletion'; +export const version = 2; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionAccountDeletion-subject', + message: 'Your <%- productName %> subscription has been cancelled', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.txt new file mode 100644 index 00000000000..41886fd79f3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountDeletion/index.txt @@ -0,0 +1,7 @@ +subscriptionAccountDeletion-subject = "Your <%- productName %> subscription has been cancelled" + +subscriptionAccountDeletion-title = "Sorry to see you go" + +subscriptionAccountDeletion-content-cancelled-2 = "You recently deleted your Mozilla account. As a result, we’ve cancelled your <%- productName %> subscription. Your final payment of <%- invoiceTotal %> was paid on <%- invoiceDateOnly %>." + +<%- include ('/partials/cancellationSurvey/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/en.ftl new file mode 100644 index 00000000000..8098ee61539 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/en.ftl @@ -0,0 +1,9 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionAccountFinishSetup-subject = Welcome to { $productName }: Please set your password. +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionAccountFinishSetup-title = Welcome to { $productName } +subscriptionAccountFinishSetup-content-processing = Your payment is processing and may take up to four business days to complete. Your subscription will renew automatically each billing period unless you choose to cancel. +subscriptionAccountFinishSetup-content-create-3 = Next, you’ll create a { -product-mozilla-account } password to start using your new subscription. +subscriptionAccountFinishSetup-action-2 = Get started diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/includes.json b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/includes.json new file mode 100644 index 00000000000..e0b49380625 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/includes.json @@ -0,0 +1,10 @@ +{ + "subject": { + "id": "subscriptionAccountFinishSetup-subject", + "message": "Welcome to <%- productName %>: Please set your password." + }, + "action": { + "id": "subscriptionAccountFinishSetup-action-2", + "message": "Get started" + } +} diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.mjml new file mode 100644 index 00000000000..364f06c2311 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.mjml @@ -0,0 +1,34 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<%- include ('/partials/icon/index.mjml') %> + + + + + Welcome to <%- productName %> + + + + + Your payment is processing and may take up to four business days to complete. Your subscription will renew automatically each billing period unless you choose to cancel. + + + + <%- include ('/partials/paymentPlanDetails/index.mjml') %> + + + + Next, you’ll create a Mozilla account password to start using your new subscription. + + + + + +<%- include ('/partials/button/index.mjml', { + buttonL10nId: "subscriptionAccountFinishSetup-action-2", + buttonText: "Get started", + cssClass: 'mb-8' + }) %> +<%- include ('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.stories.ts new file mode 100644 index 00000000000..e8bf6321f2b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.stories.ts @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionAccountFinishSetup', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + icon: 'https://cdn.accounts.firefox.com/product-icons/mozilla-vpn-email.png', + link: 'http://localhost:3030/post_verify/finish_account_setup/set_password', + subscriptionSupportUrl: 'http://localhost:3030/support', + isFinishSetup: true, +}; + +const createStory = subplatStoryWithProps( + 'subscriptionAccountFinishSetup', + 'Sent to a user after they purchased the product through the password-less flow without an existing Mozilla account.', + data, + includes +); + +export const SubscriptionFirstInvoiceNumberOnly = createStory( + { + invoiceNumber: '8675309', + }, + 'Missing Details - Invoice Number Only' +); + +export const SubscriptionFirstInvoiceTotalOnly = createStory( + { + invoiceDateOnly: '10/13/2021', + invoiceTotal: '$20.00', + }, + 'Missing Details - Invoice Date & Total Only' +); + +export const SubscriptionFirstInvoiceNextOnly = createStory( + { + nextInvoiceDateOnly: '11/13/2021', + }, + 'Missing Details - Next Invoice Only' +); + +export const SubscriptionAccountFinishSetupFullDetails = createStory( + { + invoiceDateOnly: '10/13/2021', + invoiceNumber: '8675309', + invoiceTotal: '$20.00', + nextInvoiceDateOnly: '11/13/2021', + }, + 'Full Details' +); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.ts new file mode 100644 index 00000000000..0b86719ae6c --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.ts @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; + icon: string; + link: string; + subscriptionSupportUrl: string; + isFinishSetup: boolean; +}; + +export const template = 'subscriptionAccountFinishSetup'; +export const version = 2; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionAccountFinishSetup-subject', + message: 'Welcome to <%- productName %>: Please set your password.', + }, + action: { + id: 'subscriptionAccountFinishSetup-action-2', + message: 'Get started', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.txt new file mode 100644 index 00000000000..3a2408a8266 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountFinishSetup/index.txt @@ -0,0 +1,13 @@ +subscriptionAccountFinishSetup-subject = "Welcome to <%- productName %>: Please set your password." + +subscriptionAccountFinishSetup-title = "Welcome to <%- productName %>" + +subscriptionAccountFinishSetup-content-processing = "Your payment is processing and may take up to four business days to complete. Your subscription will renew automatically each billing period unless you choose to cancel." + +<%- include ('/partials/paymentPlanDetails/index.txt') %> + +subscriptionAccountFinishSetup-content-create-3 = "Next, you’ll create a Mozilla account password to start using your new subscription." + +<%- link %> + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/en.ftl new file mode 100644 index 00000000000..e7b680446b8 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/en.ftl @@ -0,0 +1,6 @@ +subscriptionAccountReminderFirst-subject = Reminder: Finish setting up your account +subscriptionAccountReminderFirst-title = You can’t access your subscription yet +subscriptionAccountReminderFirst-content-info-3 = A few days ago you created a { -product-mozilla-account } but never confirmed it. We hope you’ll finish setting up your account, so you can use your new subscription. +subscriptionAccountReminderFirst-content-select-2 = Select “Create Password” to set up a new password and finish confirming your account. +subscriptionAccountReminderFirst-action = Create Password +subscriptionAccountReminderFirst-action-plaintext = { subscriptionAccountReminderFirst-action }: diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.mjml new file mode 100644 index 00000000000..0c1c8d6cbbe --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.mjml @@ -0,0 +1,31 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + You can’t access your subscription yet + + + + + A few days ago you created a Mozilla account but never confirmed it. We hope you’ll finish setting up your account, so you can use your new subscription. + + + + + + Select “Create Password” to set up a new password and finish confirming your account. + + + + + +<%- include ('/partials/button/index.mjml', { + buttonL10nId: "subscriptionAccountReminderFirst-action", + buttonText: "Create Password", + cssClass: 'mb-8' + }) %> +<%- include ('/partials/subscriptionSupport/index.mjml') %> +<%- include ('/partials/automatedEmailResetPassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.stories.ts new file mode 100644 index 00000000000..6d499e5980e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionAccountReminderFirst', +} as Meta; + +const data = { + link: 'http://localhost:3030/post_verify/finish_account_setup/set_password', + reminderShortForm: true, + resetLink: 'http://localhost:3030/settings/change_password', + subscriptionSupportUrl: 'http://localhost:3030/support', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionAccountReminderFirst', + 'Sent to a user to remind them to finish setting up a Mozilla account as they signed up through the password-less flow without an existing account.', + data, + includes +); + +export const SubscriptionAccountReminderFirst = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.ts new file mode 100644 index 00000000000..d6b90af5513 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + reminderShortForm: boolean; + resetLink: string; + subscriptionSupportUrl: string; +}; + +export const template = 'subscriptionAccountReminderFirst'; +export const version = 3; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionAccountReminderFirst-subject', + message: 'Reminder: Finish setting up your account', + }, + action: { + id: 'subscriptionAccountReminderFirst-action', + message: 'Create Password', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.txt new file mode 100644 index 00000000000..b6920ab535a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderFirst/index.txt @@ -0,0 +1,12 @@ +subscriptionAccountReminderFirst-subject = "Reminder: Finish setting up your account" + +subscriptionAccountReminderFirst-title = "You can’t access your subscription yet" + +subscriptionAccountReminderFirst-content-info-3 = "A few days ago you created a Mozilla account but never confirmed it. We hope you’ll finish setting up your account, so you can use your new subscription." + +subscriptionAccountReminderFirst-content-select-2 = "Select “Create Password” to set up a new password and finish confirming your account." + +subscriptionAccountReminderFirst-action-plaintext = "Create Password:" +<%- link %> + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/en.ftl new file mode 100644 index 00000000000..642d2e73820 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/en.ftl @@ -0,0 +1,6 @@ +subscriptionAccountReminderSecond-subject = Final reminder: Setup your account +subscriptionAccountReminderSecond-title-2 = Welcome to { -brand-mozilla }! +subscriptionAccountReminderSecond-content-info-3 = A few days ago you created a { -product-mozilla-account } but never confirmed it. We hope you’ll finish setting up your account, so you can use your new subscription. +subscriptionAccountReminderSecond-content-select-2 = Select “Create Password” to set up a new password and finish confirming your account. +subscriptionAccountReminderSecond-action = Create Password +subscriptionAccountReminderSecond-action-plaintext = { subscriptionAccountReminderSecond-action }: diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.mjml new file mode 100644 index 00000000000..95addfa903f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.mjml @@ -0,0 +1,31 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Welcome to Mozilla! + + + + + A few days ago you created a Mozilla account but never confirmed it. We hope you’ll finish setting up your account, so you can use your new subscription. + + + + + + Select “Create Password” to set up a new password and finish confirming your account. + + + + + +<%- include ('/partials/button/index.mjml', { + buttonL10nId: "subscriptionAccountReminderSecond-action", + buttonText: "Create Password", + cssClass: 'mb-8' +}) %> +<%- include ('/partials/subscriptionSupport/index.mjml') %> +<%- include ('/partials/automatedEmailResetPassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.stories.ts new file mode 100644 index 00000000000..84f63c8373b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionAccountReminderSecond', +} as Meta; + +const data = { + link: 'http://localhost:3030/post_verify/finish_account_setup/set_password', + reminderShortForm: true, + resetLink: 'http://localhost:3030/settings/change_password', + subscriptionSupportUrl: 'http://localhost:3030/support', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionAccountReminderSecond', + 'Sent as a final reminder to a user to remind them to finish setting up a Mozilla account as they signed up through the password-less flow without an existing account.', + data, + includes +); + +export const SubscriptionAccountReminderSecond = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.ts new file mode 100644 index 00000000000..3153b50cccc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + reminderShortForm: boolean; + resetLink: string; + subscriptionSupportUrl: string; +}; + +export const template = 'subscriptionAccountReminderSecond'; +export const version = 3; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionAccountReminderSecond-subject', + message: 'Final reminder: Setup your account', + }, + action: { + id: 'subscriptionAccountReminderSecond-action', + message: 'Create Password', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.txt new file mode 100644 index 00000000000..025a7e08502 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionAccountReminderSecond/index.txt @@ -0,0 +1,12 @@ +subscriptionAccountReminderSecond-subject = "Final reminder: Setup your account" + +subscriptionAccountReminderSecond-title-2 = "Welcome to Mozilla!" + +subscriptionAccountReminderSecond-content-info-3 = "A few days ago you created a Mozilla account but never confirmed it. We hope you’ll finish setting up your account, so you can use your new subscription." + +subscriptionAccountReminderSecond-content-select-2 = "Select “Create Password” to set up a new password and finish confirming your account." + +subscriptionAccountReminderSecond-action-plaintext = "Create Password:" +<%- link %> + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionCancellation/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/en.ftl new file mode 100644 index 00000000000..b1b1e03deb5 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/en.ftl @@ -0,0 +1,16 @@ +# Variables +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionCancellation-subject = Your { $productName } subscription has been canceled +subscriptionCancellation-title = Sorry to see you go + +## Variables +## $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +## $invoiceTotal (String) - The amount of the subscription invoice, including currency, e.g. $10.00 +## $invoiceDateOnly (String) - The date of the invoice, e.g. 01/20/2016 + +subscriptionCancellation-content-2 = We’ve canceled your { $productName } subscription. Your final payment of { $invoiceTotal } was paid on { $invoiceDateOnly }. +subscriptionCancellation-outstanding-content-2 = We’ve canceled your { $productName } subscription. Your final payment of { $invoiceTotal } will be paid on { $invoiceDateOnly }. + +# Variables +# $serviceLastActiveDateOnly (String) - The date of last active service, e.g. 01/20/2016 +subscriptionCancellation-content-continue = Your service will continue until the end of your current billing period, which is { $serviceLastActiveDateOnly }. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.mjml new file mode 100644 index 00000000000..99685953543 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.mjml @@ -0,0 +1,34 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Sorry to see you go + + + + + + + + <% if (!showOutstandingBalance) { %> + + We’ve cancelled your <%- productName %> subscription. Your final payment of <%- invoiceTotal %> was paid on <%- invoiceDateOnly %>. + + <% } else { %> + + We’ve cancelled your <%- productName %> subscription. Your final payment of <%- invoiceTotal %> will be paid on <%- invoiceDateOnly %>. + + <% } %> + <% if (cancelAtEnd) { %> + + Your service will continue until the end of your current billing period, which is <%- serviceLastActiveDateOnly %>. + + <% } %> + + + + +<%- include ('/partials/cancellationSurvey/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.stories.ts new file mode 100644 index 00000000000..14f42dea153 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.stories.ts @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionCancellation', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + isCancellationEmail: true, + invoiceTotal: '$2,000.00', + invoiceDateOnly: '11/13/2021', + serviceLastActiveDateOnly: '12/13/2021', + cancellationSurveryUrl: + 'https://survey.alchemer.com/s3/6534408/Privacy-Security-Product-Cancellation-of-Service-Q4-21', + showOutstandingBalance: true, + cancelAtEnd: false, +}; + +const createStory = subplatStoryWithProps( + 'subscriptionCancellation', + 'Sent when a user cancels their subscription.', + data, + includes +); + +export const SubscriptionCancellation = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.ts new file mode 100644 index 00000000000..fb70759e92f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; + isCancellationEmail: boolean; + invoiceTotal: string; + invoiceDateOnly: string; + serviceLastActiveDateOnly: string; + cancellationSurveryUrl: string; + showOutstandingBalance: boolean; + cancelAtEnd: boolean; +}; + +export const template = 'subscriptionCancellation'; +export const version = 3; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionCancellation-subject', + message: 'Your <%- productName %> subscription has been cancelled', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.txt new file mode 100644 index 00000000000..ec7a7c71125 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionCancellation/index.txt @@ -0,0 +1,14 @@ +subscriptionCancellation-subject = "Your <%- productName %> subscription has been cancelled" + +subscriptionCancellation-title = "Sorry to see you go" + +<% if (!showOutstandingBalance) { %> +subscriptionCancellation-content-2 = "We’ve cancelled your <%- productName %> subscription. Your final payment of <%- invoiceTotal %> was paid on <%- invoiceDateOnly %>." +<% } else { %> +subscriptionCancellation-outstanding-content-2 = "We’ve cancelled your <%- productName %> subscription. Your final payment of <%- invoiceTotal %> will be paid on <%- invoiceDateOnly %>." +<% } %> +<% if (cancelAtEnd) { %> +subscriptionCancellation-content-continue = "Your service will continue until the end of your current billing period, which is <%- serviceLastActiveDateOnly %>." +<% } %> + +<%- include ('/partials/cancellationSurvey/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/en.ftl new file mode 100644 index 00000000000..a9706051c92 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/en.ftl @@ -0,0 +1,18 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionDowngrade-subject = You have switched to { $productName } +# Variables: +# $productNameOld (String) - The name of the previously subscribed product, e.g. Mozilla VPN +# $productName (String) - The name of the new subscribed product, e.g. Mozilla VPN +subscriptionDowngrade-content-switch = You have successfully switched from { $productNameOld } to { $productName }. +# Variables: +# $paymentAmountOld (String) - The amount of the previous subscription payment, including currency, e.g. $10.00 +# $paymentAmountNew (String) - The amount of the new subscription payment, including currency, e.g. $10.00 +# $productPaymentCycleNew (String) - The interval of time from the end of one payment statement date to the next payment statement date of the new subscription, e.g. month +# $productPaymentCycleOld (String) - The interval of time from the end of one payment statement date to the next payment statement date of the old subscription, e.g. month +# $paymentProrated (String) - The one time fee to reflect the higher charge for the remainder of the payment cycle, including currency, e.g. $10.00 +subscriptionDowngrade-content-charge-info = Starting with your next bill, your charge will change from { $paymentAmountOld } per { $productPaymentCycleOld } to { $paymentAmountNew } per { $productPaymentCycleNew }. At that time you will also be given a one-time credit of { $paymentProrated } to reflect the lower charge for the remainder of this { $productPaymentCycleOld }. +# Variables: +# $productName (String) - The name of the new subscribed product, e.g. Mozilla VPN +subscriptionDowngrade-content-install = If there is new software for you to install in order to use { $productName }, you will receive a separate email with download instructions. +subscriptionDowngrade-content-auto-renew = Your subscription will automatically renew each billing period unless you choose to cancel. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.mjml new file mode 100644 index 00000000000..b8774d16075 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.mjml @@ -0,0 +1,27 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<%- include ('/partials/icon/index.mjml') %> + + + + + You have successfully switched from <%- productNameOld %> to <%- productName %>. + + + + Starting with your next bill, your charge will change from <%- paymentAmountOld %> per <%- productPaymentCycleOld %> to <%- paymentAmountNew %> per <%- productPaymentCycleNew %>. At that time you will also be given a one-time credit of <%- paymentProrated %> to reflect the lower charge for the remainder of this <%- productPaymentCycleOld %>. + + + + If there is new software for you to install in order to use <%- productName %>, you will receive a separate email with download instructions. + + + + Your subscription will automatically renew each billing period unless you choose to cancel. + + + + +<%- include ('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.stories.ts new file mode 100644 index 00000000000..5f0afdf3ba8 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.stories.ts @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionDowngrade', +} as Meta; + +const data = { + paymentAmountNew: '£123,121.00', + paymentAmountOld: '¥99,991', + paymentProrated: '$5,231.00', + productIconURLNew: + 'https://cdn.accounts.firefox.com/product-icons/mozilla-vpn-email.png', + productIconURLOld: + 'https://cdn.accounts.firefox.com/product-icons/mozilla-vpn-email.png', + productName: 'Product Name B', + productNameOld: 'Product Name A', + productPaymentCycleNew: 'month', + productPaymentCycleOld: 'year', + subscriptionSupportUrl: 'http://localhost:3030/support', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionDowngrade', + 'Sent when a user downgrades their subscription.', + data, + includes +); + +export const SubscriptionDowngrade = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.ts new file mode 100644 index 00000000000..7f805691418 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + paymentAmountNew: string; + paymentAmountOld: string; + paymentProrated: string; + productIconURLNew: string; + productIconURLOld: string; + productName: string; + productNameOld: string; + productPaymentCycleNew: string; + productPaymentCycleOld: string; + subscriptionSupportUrl: string; +}; + +export const template = 'subscriptionDowngrade'; +export const version = 2; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionDowngrade-subject', + message: 'You have switched to <%- productName %>', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.txt new file mode 100644 index 00000000000..8c8280a8a2c --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionDowngrade/index.txt @@ -0,0 +1,11 @@ +subscriptionDowngrade-subject = "You have switched to <%- productName %>" + +subscriptionDowngrade-content-switch = "You have successfully switched from <%- productNameOld %> to <%- productName %>." + +subscriptionDowngrade-content-charge-info = "Starting with your next bill, your charge will change from <%- paymentAmountOld %> per <%- productPaymentCycleOld %> to <%- paymentAmountNew %> per <%- productPaymentCycleNew %>. At that time you will also be given a one-time credit of <%- paymentProrated %> to reflect the lower charge for the remainder of this <%- productPaymentCycleOld %>." + +subscriptionDowngrade-content-install = "If there is new software for you to install in order to use <%- productName %>, you will receive a separate email with download instructions." + +subscriptionDowngrade-content-auto-renew = "Your subscription will automatically renew each billing period unless you choose to cancel." + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/en.ftl new file mode 100644 index 00000000000..900f9adc59a --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/en.ftl @@ -0,0 +1,7 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionFailedPaymentsCancellation-subject = Your { $productName } subscription has been cancelled +subscriptionFailedPaymentsCancellation-title = Your subscription has been cancelled +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionFailedPaymentsCancellation-content = We’ve cancelled your { $productName } subscription because multiple payment attempts failed. To get access again, start a new subscription with an updated payment method. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.mjml new file mode 100644 index 00000000000..71f6fcb9de2 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.mjml @@ -0,0 +1,23 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your subscription has been cancelled + + + + + + + + + We’ve cancelled your <%- productName %> subscription because multiple payment attempts failed. To get access again, start a new subscription with an updated payment method. + + + + + +<%- include ('/partials/cancellationSurvey/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.stories.ts new file mode 100644 index 00000000000..e8dd856cf09 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionFailedPaymentsCancellation', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + isCancellationEmail: true, + cancellationSurveryUrl: + 'https://survey.alchemer.com/s3/6534408/Privacy-Security-Product-Cancellation-of-Service-Q4-21', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionFailedPaymentsCancellation', + 'Sent when failed payments result in cancellation of user subscription.', + data, + includes +); + +export const subscriptionFailedPaymentsCancellation = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.ts new file mode 100644 index 00000000000..7593aedcffc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.ts @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; + isCancellationEmail: boolean; + cancellationSurveryUrl: string; +}; + +export const template = 'subscriptionFailedPaymentsCancellation'; +export const version = 2; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionFailedPaymentsCancellation-subject', + message: 'Your <%- productName %> subscription has been cancelled', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.txt new file mode 100644 index 00000000000..abfff0bb551 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFailedPaymentsCancellation/index.txt @@ -0,0 +1,7 @@ +subscriptionFailedPaymentsCancellation-subject = "Your <%- productName %> subscription has been cancelled" + +subscriptionFailedPaymentsCancellation-title = "Your subscription has been cancelled" + +subscriptionFailedPaymentsCancellation-content = "We’ve cancelled your <%- productName %> subscription because multiple payment attempts failed. To get access again, start a new subscription with an updated payment method." + +<%- include ('/partials/cancellationSurvey/index.txt') %> \ No newline at end of file diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/en.ftl new file mode 100644 index 00000000000..768699f9d5f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/en.ftl @@ -0,0 +1,14 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionFirstInvoice-subject = { $productName } payment confirmed +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionFirstInvoice-title = Thank you for subscribing to { $productName } +subscriptionFirstInvoice-content-processing = Your payment is currently processing and may take up to four business days to complete. +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionFirstInvoice-content-install-2 = You will receive a separate email on how to start using { $productName }. +subscriptionFirstInvoice-content-auto-renew = Your subscription will automatically renew each billing period unless you choose to cancel. +# Variables: +# $nextInvoiceDateOnly (String) - The date of the next invoice, e.g. August 28, 2025 +subscriptionFirstInvoice-content-your-next-invoice = Your next invoice will be issued on { $nextInvoiceDateOnly }. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.mjml new file mode 100644 index 00000000000..5ded10822a7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.mjml @@ -0,0 +1,42 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<%- include ('/partials/icon/index.mjml') %> + + + + + Thank you for subscribing to <%- productName %> + + + + + Your payment is currently processing and may take up to four business days to complete. + + + + + + You will receive a separate email on how to start using <%- productName %>. + + + + + + Your subscription will automatically renew each billing period unless you choose to cancel. + + + + <%- include ('/partials/subscriptionCharges/index.mjml') %> + <%- include ('/partials/viewInvoice/index.mjml') %> + + + + Your next invoice will be issued on <%- nextInvoiceDateOnly %>. + + + + + +<%- include ('/partials/subscriptionSupportGetHelp/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.stories.ts new file mode 100644 index 00000000000..3d7e2af2977 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.stories.ts @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionFirstInvoice', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + invoiceAmountDue: '$10.00', + invoiceAmountDueInCents: 1000, + icon: 'https://cdn.accounts.firefox.com/product-icons/mozilla-vpn-email.png', + invoiceDateOnly: '10/13/2021', + invoiceLink: + 'https://pay.stripe.com/invoice/acct_1GCAr3BVqmGyQTMa/invst_GyHjTyIXBg8jj5yjt7Z0T4CCG3hfGtp', + invoiceNumber: '8675309', + invoiceTotal: '$20.00', + invoiceTotalInCents: 2000, + invoiceSubtotal: null, + invoiceTaxAmount: null, + invoiceDiscountAmount: null, + discountType: null, + discountDuration: null, + nextInvoiceDateOnly: '11/13/2021', + subscriptionSupportUrl: 'http://localhost:3030/support', + paymentProrated: null, + showPaymentMethod: true, + showProratedAmount: false, + showTaxAmount: false, + + // Had to add these in! Please double check values + cardName: 'foo', + paymentProviderName: 'bar', + lastFour: '4242', + remainingAmountTotalInCents: 1000, + offeringPrice: '$10.00', + offeringPriceInCents: 1000, + unusedAmountTotal: '$10.00', + unusedAmountTotalInCents: 1000, + invoiceSubtotalInCents: 1000, + creditAppliedInCents: 1000, + invoiceStartingBalance: '$10.00', + manageSubscriptionUrl: 'https://payments.firefox.com/', // Does this need UTM params? + invoiceTaxAmountInCents: 1000, +}; + +const createStory = subplatStoryWithProps( + 'subscriptionFirstInvoice', + 'Sent to inform a user that their first payment is currently being processed.', + data, + includes +); + +export const SubscriptionFirstInvoiceWithPayPal = createStory( + { + payment_provider: 'paypal', + }, + 'Payment method - PayPal' +); + +export const SubscriptionFirstInvoiceWithStripe = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + }, + 'Payment method - Stripe' +); + +export const SubscriptionFirstInvoiceWithStripeUnknownCard = createStory( + { + cardType: 'unknown', + cardName: 'Unknown', + lastFour: '5309', + payment_provider: 'stripe', + }, + 'Payment method - Stripe (Unknown card)' +); + +export const SubscriptionFirstInvoiceWithCoupon = createStory( + { + cardType: null, + lastFour: null, + payment_provider: 'stripe', + invoiceTotal: '$0.00', + invoiceDiscountAmount: '$20.00', + discountType: 'forever', + discountDuration: null, + showPaymentMethod: false, + }, + 'Payment method hidden - coupon covered entire amount' +); + +export const SubscriptionFirstInvoiceWithStripe3Month = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + invoiceTotal: '$15.00', + invoiceSubtotal: '$20.00', + invoiceDiscountAmount: '$5.00', + discountType: 'repeating', + discountDuration: 3, + }, + 'Stripe - 3 month Coupon' +); + +export const SubscriptionFirstInvoiceWithStripeOneTime = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + invoiceTotal: '$15.00', + invoiceSubtotal: '$20.00', + invoiceDiscountAmount: '$5.00', + discountType: 'once', + discountDuration: null, + }, + 'Stripe - One Time Coupon' +); + +export const SubscriptionFirstInvoiceWithStripeTaxAndForeverCoupon = + createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + invoiceTotal: '$15.00', + invoiceSubtotal: '$20.00', + invoiceDiscountAmount: '$5.00', + invoiceTaxAmount: '$2.00', + discountType: 'forever', + discountDuration: null, + showTaxAmount: true, + }, + 'Stripe - With Tax and Forever Coupon' + ); + +export const SubscriptionFirstInvoiceWithStripeTax = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + invoiceTaxAmount: '$3.00', + discountType: null, + discountDuration: null, + showTaxAmount: true, + }, + 'Stripe - With Tax' +); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.ts new file mode 100644 index 00000000000..f8475664290 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.ts @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + invoiceAmountDue: string; + invoiceAmountDueInCents: number; + icon: string; + invoiceDateOnly: string; + invoiceLink: string; + invoiceNumber: string; + invoiceTotal: string; + invoiceTotalInCents: number; + invoiceSubtotal: string | null; + invoiceTaxAmount: string | null; + invoiceDiscountAmount: string | null; + discountType: string | null; + discountDuration: string | null; + nextInvoiceDateOnly: string; + subscriptionSupportUrl: string; + paymentProrated: string | null; + showPaymentMethod: boolean; + showProratedAmount: boolean; + showTaxAmount: boolean; + cardName: string; + paymentProviderName: string; + lastFour: string; + remainingAmountTotalInCents: number; + offeringPrice: string; + offeringPriceInCents: number; + unusedAmountTotal: string; + unusedAmountTotalInCents: number; + invoiceSubtotalInCents: number; + creditAppliedInCents: number; + invoiceStartingBalance: string; + manageSubscriptionUrl: string; + invoiceTaxAmountInCents: number; +}; + +export const template = 'subscriptionFirstInvoice'; +export const version = 4; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionFirstInvoice-subject', + message: '<%- productName %> payment confirmed', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.txt new file mode 100644 index 00000000000..6502847e128 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionFirstInvoice/index.txt @@ -0,0 +1,16 @@ +subscriptionFirstInvoice-subject = "<%- productName %> payment confirmed" + +subscriptionFirstInvoice-title = "Thank you for subscribing to <%- productName%>" + +subscriptionFirstInvoice-content-processing = "Your payment is currently processing and may take up to four business days to complete." + +subscriptionFirstInvoice-content-install-2 = "You will receive a separate email on how to start using <%- productName %>." + +subscriptionFirstInvoice-content-auto-renew = "Your subscription will automatically renew each billing period unless you choose to cancel." + +<%- include ('/partials/subscriptionCharges/index.txt') %> +<%- include ('/partials/viewInvoice/index.txt') %> + +subscriptionFirstInvoice-content-your-next-invoice = "Your next invoice will be issued on <%- nextInvoiceDateOnly %>." + +<%- include ('/partials/subscriptionSupportGetHelp/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/en.ftl new file mode 100644 index 00000000000..e06b02b9a87 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/en.ftl @@ -0,0 +1,7 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionPaymentExpired-subject-2 = Payment method for { $productName } expired or expiring soon +subscriptionPaymentExpired-title-2 = Your payment method is expired or about to expire +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionPaymentExpired-content-2 = The payment method you’re using for { $productName } is expired or about to expire. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.mjml new file mode 100644 index 00000000000..93b96b4ed38 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.mjml @@ -0,0 +1,20 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your payment method is expired or about to expire + + + + + The payment method you’re using for <%- productName %> is expired or about to expire. + + + + + +<%- include('/partials/subscriptionUpdatePayment/index.mjml', { updateBillingUrl }) %> +<%- include ('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.stories.ts new file mode 100644 index 00000000000..6a66f22871f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionPaymentExpired', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + updateBillingUrl: 'http://localhost:3030/subscriptions', + subscriptionSupportUrl: 'http://localhost:3030/support', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionPaymentExpired', + 'Sent whenever a user has a single subscription and their payment method has expired or will expire at the end of the month, triggered by a Stripe webhook.', + data, + includes +); + +export const SubscriptionPaymentExpired = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.ts new file mode 100644 index 00000000000..5333c01e495 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.ts @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; + updateBillingUrl: string; + subscriptionSupportUrl: string; +}; + +export const template = 'subscriptionPaymentExpired'; +export const version = 4; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionPaymentExpired-subject-2', + message: 'Payment method for <%- productName %> expired or expiring soon', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.txt new file mode 100644 index 00000000000..32d3fbe4e14 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentExpired/index.txt @@ -0,0 +1,9 @@ +subscriptionPaymentExpired-subject-2 = "Payment method for <%- productName %> expired or expiring soon" + +subscriptionPaymentExpired-title-2 = "Your payment method is expired or about to expire" + +subscriptionPaymentExpired-content-2 = "The payment method you’re using for <%- productName %> is expired or about to expire." + +<%- include('/partials/subscriptionUpdatePayment/index.txt', { updateBillingUrl }) %> + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/en.ftl new file mode 100644 index 00000000000..69e516238d0 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/en.ftl @@ -0,0 +1,8 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionPaymentFailed-subject = { $productName } payment failed +subscriptionPaymentFailed-title = Sorry, we’re having trouble with your payment +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionPaymentFailed-content-problem = We had a problem with your latest payment for { $productName }. +subscriptionPaymentFailed-content-outdated-1 = It may be that your payment method has expired, or your current payment method is out-of-date. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.mjml new file mode 100644 index 00000000000..18cb2eced95 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.mjml @@ -0,0 +1,32 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<%- include ('/partials/icon/index.mjml') %> + + + + + Sorry, we’re having trouble with your payment + + + + + + + + + We had a problem with your latest payment for <%- productName %>. + + + + + + It may be that your payment method has expired, or your current payment method is out-of-date. + + + + + +<%- include ('/partials/subscriptionUpdateBillingTry/index.mjml') %> +<%- include ('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.stories.ts new file mode 100644 index 00000000000..94281657fb4 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionPaymentFailed', +} as Meta; + +const data = { + icon: 'https://cdn.accounts.firefox.com/product-icons/mozilla-vpn-email.png', + productName: 'Firefox Fortress', + subscriptionSupportUrl: 'http://localhost:3030/support', + updateBillingUrl: 'http://localhost:3030/subscriptions', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionPaymentFailed', + 'Sent when there is a problem with the latest payment.', + data, + includes +); + +export const SubscriptionPaymentFailed = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.ts new file mode 100644 index 00000000000..add3cbb201e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.ts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + icon: string; + productName: string; + subscriptionSupportUrl: string; + updateBillingUrl: string; +}; + +export const template = 'subscriptionPaymentFailed'; +export const version = 3; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionPaymentFailed-subject', + message: '<%- productName %> payment failed', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.txt new file mode 100644 index 00000000000..ca7cd85f630 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentFailed/index.txt @@ -0,0 +1,10 @@ +subscriptionPaymentFailed-subject = "<%- productName %> payment failed" + +subscriptionPaymentFailed-title = "Sorry, we’re having trouble with your payment" + +subscriptionPaymentFailed-content-problem = "We had a problem with your latest payment for <%- productName %>." + +subscriptionPaymentFailed-content-outdated-1 = "It may be that your payment method has expired, or your current payment method is out-of-date." + +<%- include ('/partials/subscriptionUpdateBillingTry/index.txt') %> +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/en.ftl new file mode 100644 index 00000000000..32589f3490c --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/en.ftl @@ -0,0 +1,8 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionPaymentProviderCancelled-subject = Payment information update required for { $productName } +subscriptionPaymentProviderCancelled-title = Sorry, we’re having trouble with your payment method +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionPaymentProviderCancelled-content-detect = We have detected a problem with your payment method for { $productName }. +subscriptionPaymentProviderCancelled-content-reason-1 = It may be that your payment method has expired, or your current payment method is out-of-date. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.mjml new file mode 100644 index 00000000000..d189225bfd9 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.mjml @@ -0,0 +1,26 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Sorry, we’re having trouble with your payment method + + + + + We have detected a problem with your payment method for <%- productName %>. + + + + + + It may be that your payment method has expired, or your current payment method is out-of-date. + + + + + +<%- include('/partials/subscriptionUpdatePayment/index.mjml', { updateBillingUrl }) %> +<%- include ('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.stories.ts new file mode 100644 index 00000000000..5e3d32a9bbc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionPaymentProviderCancelled', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + subscriptionSupportUrl: 'http://localhost:3030/support', + updateBillingUrl: 'http://localhost:3030/subscriptions', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionPaymentProviderCancelled', + 'Sent when a problem is detected with the payment method.', + data, + includes +); + +export const SubscriptionPaymentProviderCancelled = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.ts new file mode 100644 index 00000000000..3b4958c8c51 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.ts @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; + subscriptionSupportUrl: string; + updateBillingUrl: string; +}; + +export const template = 'subscriptionPaymentProviderCancelled'; +export const version = 3; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionPaymentProviderCancelled-subject', + message: 'Your <%- productName %> subscription has been cancelled', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.txt new file mode 100644 index 00000000000..ca9c4a972f4 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionPaymentProviderCancelled/index.txt @@ -0,0 +1,10 @@ +subscriptionPaymentProviderCancelled-subject = "Payment information update required for <%- productName %>" + +subscriptionPaymentProviderCancelled-title = "Sorry, we’re having trouble with your payment method" + +subscriptionPaymentProviderCancelled-content-detect = "We have detected a problem with your payment method for <%- productName %>." + +subscriptionPaymentProviderCancelled-content-reason-1 = "It may be that your payment method has expired, or your current payment method is out-of-date." + +<%- include('/partials/subscriptionUpdatePayment/index.txt', { updateBillingUrl }) %> +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReactivation/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/en.ftl new file mode 100644 index 00000000000..d4dd0aa0685 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/en.ftl @@ -0,0 +1,10 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionReactivation-subject = { $productName } subscription reactivated +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionReactivation-title = Thank you for reactivating your { $productName } subscription! +# Variables: +# $invoiceTotal (String) - The amount of the subscription invoice, including currency, e.g. $10.00 +# $nextInvoiceDateOnly (String) - The date of the next invoice, e.g. 2016/01/20 +subscriptionReactivation-content = Your billing cycle and payment will remain the same. Your next charge will be { $invoiceTotal } on { $nextInvoiceDateOnly }. Your subscription will automatically renew each billing period unless you choose to cancel. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.mjml new file mode 100644 index 00000000000..edb5bba45f1 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.mjml @@ -0,0 +1,21 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<%- include ('/partials/icon/index.mjml') %> + + + + + Thank you for reactivating your <%- productName %> subscription! + + + + + Your billing cycle and payment will remain the same. Your next charge will be <%- invoiceTotal %> on <%- nextInvoiceDateOnly %>. Your subscription will automatically renew each billing period unless you choose to cancel. + + + + + +<%- include ('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.stories.ts new file mode 100644 index 00000000000..ef6c51397ea --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.stories.ts @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionReactivation', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + invoiceTotal: '$20', + nextInvoiceDateOnly: '11/13/2021', + icon: 'https://placekitten.com/512/512', + subscriptionSupportUrl: 'http://localhost:3030/support', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionReactivation', + 'Sent when a user reactivates their subscription.', + data, + includes +); + +export const SubscriptionReactivation = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.ts new file mode 100644 index 00000000000..badac8a4c3f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.ts @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; + invoiceTotal: string; + nextInvoiceDateOnly: string; + icon: string; + subscriptionSupportUrl: string; +}; + +export const template = 'subscriptionReactivation'; +export const version = 2; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionReactivation-subject-2', + message: 'Your <%- productName %> subscription has been reactivated', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.txt new file mode 100644 index 00000000000..9b9c1845664 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReactivation/index.txt @@ -0,0 +1,5 @@ +subscriptionReactivation-title = "Thank you for reactivating your <%- productName %> subscription!" + +subscriptionReactivation-content = "Your billing cycle and payment will remain the same. Your next charge will be <%- invoiceTotal %> on <%- nextInvoiceDateOnly %>. Your subscription will automatically renew each billing period unless you choose to cancel." + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/en.ftl new file mode 100644 index 00000000000..c4dae569090 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/en.ftl @@ -0,0 +1,17 @@ +# Variables +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionRenewalReminder-subject = { $productName } automatic renewal notice +subscriptionRenewalReminder-title = Your subscription will be renewed soon +# Variables +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionRenewalReminder-content-greeting = Dear { $productName } customer, +# Variables +# $invoiceTotal (String) - The amount of the subscription invoice, including currency, e.g. $10.00 +# $planIntervalCount (String) - The interval count of subscription plan, e.g. 2 +# $planInterval (String) - The interval of time of the subscription plan, e.g. week +# $reminderLength (String) - The number of days until the current subscription is set to automatically renew, e.g. 14 +subscriptionRenewalReminder-content-current = Your current subscription is set to automatically renew in { $reminderLength } days. At that time, { -brand-mozilla } will renew your { $planIntervalCount } { $planInterval } subscription and a charge of { $invoiceTotal } will be applied to the payment method on your account. +subscriptionRenewalReminder-content-closing = Sincerely, +# Variables +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionRenewalReminder-content-signature = The { $productName } team diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.mjml new file mode 100644 index 00000000000..e00aceed148 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.mjml @@ -0,0 +1,32 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your subscription will be renewed soon + + + + Dear <%- productName %> customer, + + + + + Your current subscription is set to automatically renew in <%- reminderLength %> days. At that time, Mozilla will renew your <%- planIntervalCount%> <%- planInterval%> subscription and a charge of <%- invoiceTotal%> will be applied to the payment method on your account. + + + + <%- include ('/partials/subscriptionUpdateBillingEnsure/index.mjml') %> + <%- include ('/partials/subscriptionSupportContact/index.mjml') %> + + + Sincerely, + + + + The <%- productName %> team + + + diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.stories.ts new file mode 100644 index 00000000000..66a3846846f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.stories.ts @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionRenewalReminder', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + invoiceTotal: '$20.00', + planInterval: 'week', + planIntervalCount: '2', + reminderLength: '14', + subscriptionSupportUrl: 'http://localhost:3030/support', + updateBillingUrl: 'http://localhost:3030/subscriptions', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionRenewalReminder', + 'Sent to remind a user of an upcoming automatic subscription renewal X days out from charge (X being what is set in the Stripe dashboard)', + data, + includes +); + +export const SubscriptionReactivation = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.ts new file mode 100644 index 00000000000..8ed78415b10 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.ts @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; + invoiceTotal: string; + planInterval: string; + planIntervalCount: string; + reminderLength: string; + subscriptionSupportUrl: string; + updateBillingUrl: string; +}; + +export const template = 'subscriptionRenewalReminder'; +export const version = 2; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionRenewalReminder-subject-2', + message: 'Your <%- productName %> subscription is about to renew', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.txt new file mode 100644 index 00000000000..b56272c6c28 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionRenewalReminder/index.txt @@ -0,0 +1,15 @@ +subscriptionRenewalReminder-subject = "<%- productName %> automatic renewal notice" + +subscriptionRenewalReminder-title = "Your subscription will be renewed soon" + +subscriptionRenewalReminder-content-greeting = "Dear <%- productName %> customer," + +subscriptionRenewalReminder-content-current = "Your current subscription is set to automatically renew in <%- reminderLength %> days. At that time, Mozilla will renew your <%- planIntervalCount%> <%- planInterval%> subscription and a charge of <%- invoiceTotal%> will be applied to the payment method on your account." + +<%- include ('/partials/subscriptionUpdateBillingEnsure/index.txt') %> + +<%- include ('/partials/subscriptionSupportContact/index.txt') %> + +subscriptionRenewalReminder-content-closing = "Sincerely," + +subscriptionRenewalReminder-content-signature = "The <%- productName %> team" diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReplaced/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/en.ftl new file mode 100644 index 00000000000..a9321338139 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/en.ftl @@ -0,0 +1,8 @@ +subscriptionReplaced-subject = Your subscription has been updated as part of your upgrade +subscriptionReplaced-title = Your subscription has been updated + +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionReplaced-content-replaced = Your individual { $productName } subscription has been replaced and is now included in your new bundle. + +subscriptionReplaced-content-credit = You’ll receive a credit for any unused time from your previous subscription. This credit will be automatically applied to your account and used toward future charges. +subscriptionReplaced-content-no-action = No action is required on your part. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReplaced/includes.json b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/includes.json new file mode 100644 index 00000000000..9b2d6882731 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/includes.json @@ -0,0 +1,6 @@ +{ + "subject": { + "id": "subscriptionReplaced-subject", + "message": "Your subscription has been updated as part of your upgrade" + } +} diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.mjml new file mode 100644 index 00000000000..377a2b48d68 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.mjml @@ -0,0 +1,29 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not + distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your subscription has been updated + + + + + Your individual <%- productName %> subscription has been replaced and is now included in your new bundle. + + + + + + You’ll receive a credit for any unused time from your previous subscription. This credit will be automatically applied to your account and used toward future charges. + + + + + + No action is required on your part. + + + + diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.stories.ts new file mode 100644 index 00000000000..3af632b74af --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.stories.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionReplaced', +} as Meta; + +const data = { + productName: 'Firefox Fortress', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionReplaced', + 'Sent when a user has overlapping subscriptions when upgrading to a bundle.', + data, + includes +); + +export const SubscriptionReplaced = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.ts new file mode 100644 index 00000000000..b2b08ef0a92 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.ts @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; +}; + +export const template = 'subscriptionReplaced'; +export const version = 1; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionReplaced-subject', + message: 'Your subscription has been updated as part of your upgrade', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.txt new file mode 100644 index 00000000000..86d6589c3ee --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionReplaced/index.txt @@ -0,0 +1,9 @@ +subscriptionReplaced-subject = "Your subscription has been updated as part of your upgrade" + +subscriptionReplaced-title = "Your subscription has been updated" + +subscriptionReplaced-content-replaced = "Your individual <%- productName %> subscription has been replaced and is now included in your new bundle." + +subscriptionReplaced-content-credit = "You’ll receive a credit for any unused time from your previous subscription. This credit will be automatically applied to your account and used toward future charges." + +subscriptionReplaced-content-no-action = "No action is required on your part." diff --git a/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/en.ftl new file mode 100644 index 00000000000..fb7cda4ce0d --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/en.ftl @@ -0,0 +1,10 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionSubsequentInvoice-subject = { $productName } payment received +subscriptionSubsequentInvoice-title = Thank you for being a subscriber! +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionSubsequentInvoice-content-received = We received your latest payment for { $productName }. +# Variables: +# $nextInvoiceDateOnly (String) - The date of the next invoice, e.g. August 28, 2025 +subscriptionSubsequentInvoice-content-your-next-invoice = Your next invoice will be issued on { $nextInvoiceDateOnly }. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/includes.json b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/includes.json new file mode 100644 index 00000000000..f6ae4839254 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/includes.json @@ -0,0 +1,6 @@ +{ + "subject": { + "id": "subscriptionSubsequentInvoice-subject", + "message": "<%- productName %> payment received" + } +} diff --git a/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.mjml new file mode 100644 index 00000000000..d95c389a258 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.mjml @@ -0,0 +1,30 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<%- include ('/partials/icon/index.mjml') %> + + + + + Thank you for being a subscriber! + + + + + We received your latest payment for <%- productName %>. + + + + <%- include ('/partials/subscriptionCharges/index.mjml') %> + <%- include ('/partials/viewInvoice/index.mjml') %> + + + + Your next invoice will be issued on <%- nextInvoiceDateOnly %>. + + + + + +<%- include ('/partials/subscriptionSupportGetHelp/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.stories.ts new file mode 100644 index 00000000000..0807de7ebcf --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.stories.ts @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionSubsequentInvoice', +} as Meta; + +const data = { + productName: 'Firefox Fortress', + invoiceAmountDue: '$10.00', + invoiceDateOnly: '12/14/2021', + invoiceNumber: '8675309', + invoiceTotalInCents: 2000, + invoiceTotal: '$20.00', + invoiceSubtotal: null, + invoiceTaxAmount: null, + invoiceDiscountAmount: null, + discountType: null, + discountDuration: null, + icon: 'https://cdn.accounts.firefox.com/product-icons/mozilla-vpn-email.png', + invoiceLink: + 'https://pay.stripe.com/invoice/acct_1GCAr3BVqmGyQTMa/invst_GyHjTyIXBg8jj5yjt7Z0T4CCG3hfGtp', + nextInvoiceDateOnly: '1/14/2022', + subscriptionSupportUrl: 'http://localhost:3030/support', + paymentProrated: null, + showPaymentMethod: true, + showProratedAmount: false, + showTaxAmount: false, + + productPaymentCycle: 'monthly', + invoiceAmountDueInCents: 1000, + paymentProviderName: 'foo', + remainingAmountTotalInCents: 1000, + offeringPriceInCents: 1000, + offeringPrice: '$10.00', + unusedAmountTotalInCents: 1000, + unusedAmountTotal: '$10.00', + invoiceSubtotalInCents: 1000, + creditAppliedInCents: 1000, + invoiceStartingBalance: '$10.00', + manageSubscriptionUrl: 'https://payments.firefox.com/', // Does this need UTM params? + invoiceTaxAmountInCents: 1000, +}; + +const createStory = subplatStoryWithProps( + 'subscriptionSubsequentInvoice', + 'Sent when the latest subscription payment is received.', + data, + includes +); + +export const SubscriptionSubsequentInvoicePayPalProrated = createStory( + { + payment_provider: 'paypal', + paymentProrated: '$5,231.00', + showProratedAmount: true, + }, + 'PayPal with prorated amount' +); + +export const SubscriptionSubsequentInvoicePayPalNoProrated = createStory( + { + payment_provider: 'paypal', + showProratedAmount: false, + }, + 'PayPal with no prorated amount' +); + +export const SubscriptionSubsequentInvoiceStripeProrated = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + paymentProrated: '$5,231.00', + showProratedAmount: true, + }, + 'Stripe with prorated amount' +); + +export const SubscriptionSubsequentInvoiceStripeNoProrated = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + showProratedAmount: false, + }, + 'Stripe with no prorated amount' +); + +export const SubscriptionSubsequentInvoiceCouponFullAmount = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + invoiceSubtotal: '$20.00', + invoiceTotal: '$0.00', + invoiceDiscountAmount: '$20.00', + showProratedAmount: false, + showPaymentMethod: false, + }, + 'Payment method hidden - coupon covered entire amount' +); + +export const SubscriptionSubsequentInvoiceStripeNoProrated3Month = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + showProratedAmount: false, + invoiceTotal: '$15.00', + invoiceSubtotal: '$20.00', + invoiceDiscountAmount: '$5.00', + discountType: 'repeating', + discountDuration: 3, + }, + 'Stripe - 3 Month Coupon' +); + +export const SubscriptionSubsequentInvoiceStripeNoProratedOneTime = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + showProratedAmount: false, + invoiceTotal: '$15.00', + invoiceSubtotal: '$20.00', + invoiceDiscountAmount: '$5.00', + discountType: 'once', + discountDuration: null, + }, + 'Stripe - One Time Coupon' +); + +export const SubscriptionSubsequentInvoiceStripeNoProratedOneTimeWithTax = + createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + showProratedAmount: false, + invoiceTotal: '$15.00', + invoiceSubtotal: '$20.00', + invoiceDiscountAmount: '$5.00', + invoiceTaxAmount: '$2.00', + discountType: 'once', + discountDuration: null, + showTaxAmount: true, + }, + 'Stripe - One Time Coupon with Tax' + ); + +export const SubscriptionSubsequentInvoiceStripeNoProratedWithTax = createStory( + { + cardType: 'mastercard', + cardName: 'Mastercard', + lastFour: '5309', + payment_provider: 'stripe', + showProratedAmount: false, + invoiceTaxAmount: '$2.00', + discountType: null, + discountDuration: null, + showTaxAmount: true, + }, + 'Stripe - With Tax' +); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.ts new file mode 100644 index 00000000000..d604eff9b7e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.ts @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + productName: string; + invoiceAmountDue: string; + invoiceDateOnly: string; + invoiceNumber: string; + invoiceTotalInCents: number; + invoiceTotal: string; + invoiceSubtotal: number | null; + invoiceTaxAmount: number | null; + invoiceDiscountAmount: number | null; + discountType: string | null; + discountDuration: string | null; + productPaymentCycle: string; + invoiceAmountDueInCents: number; + paymentProviderName: string; + remainingAmountTotalInCents: number; + offeringPriceInCents: number; + offeringPrice: string; + unusedAmountTotalInCents: number; + unusedAmountTotal: string; + invoiceSubtotalInCents: number; + creditAppliedInCents: number; + invoiceStartingBalance: string; + manageSubscriptionUrl: string; + invoiceTaxAmountInCents: number; +}; + +export const template = 'subscriptionSubsequentInvoice'; +export const version = 5; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionSubsequentInvoice-subject-2', + message: + 'Your <%- productName %> <%- productPaymentCycle %> invoice from Mozilla is ready', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.txt new file mode 100644 index 00000000000..33ab8ce64a7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionSubsequentInvoice/index.txt @@ -0,0 +1,12 @@ +subscriptionSubsequentInvoice-subject = "<%- productName %> payment received" + +subscriptionSubsequentInvoice-title = "Thank you for being a subscriber!" + +subscriptionSubsequentInvoice-content-received = "We received your latest payment for <%- productName %>." + +<%- include ('/partials/viewInvoice/index.txt') %> +<%- include ('/partials/subscriptionCharges/index.txt') %> + +subscriptionSubsequentInvoice-content-your-next-invoice = "Your next invoice will be issued on <%- nextInvoiceDateOnly %>." + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/en.ftl new file mode 100644 index 00000000000..ef858fe8e51 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/en.ftl @@ -0,0 +1,48 @@ +# Variables: +# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN +subscriptionUpgrade-subject = You have upgraded to { $productName } +subscriptionUpgrade-title = Thank you for upgrading! + +# Variables: +# $productNameOld (String) - The name of the previously subscribed product, e.g. Mozilla VPN +# $productName (String) - The name of the new subscribed product, e.g. Mozilla VPN +subscriptionUpgrade-upgrade-info-2 = You have successfully upgraded to { $productName }. + +## Variables: +## $paymentAmountOld (String) - The amount of the previous subscription payment, including currency, e.g. $10.00 +## $paymentAmountNew (String) - The amount of the new subscription payment, including currency, e.g. $10.00 +## $paymentTaxOld (String) - The tax amount of the previous subscription payment, including currency, e.g. $1.00 +## $paymentTaxNew (String) - The tax amount of the new subscription payment, including currency, e.g. $1.00 +## $productPaymentCycleNew (String) - The interval of time from the end of one payment statement date to the next payment statement date of the new subscription, e.g. month +## $productPaymentCycleOld (String) - The interval of time from the end of one payment statement date to the next payment statement date of the old subscription, e.g. month +## $invoiceAmountDue (String) - The total that the customer owes after all credits, discounts, and taxes have been applied +## $paymentProrated (String) - The one time fee to reflect the higher charge for the remainder of the payment cycle, including currency, e.g. $10.00 +subscriptionUpgrade-content-charge-prorated-1 = You have been charged a one-time fee of { $invoiceAmountDue } to reflect your subscription’s higher price for the remainder of this billing period ({ $productPaymentCycleOld }). +subscriptionUpgrade-content-charge-credit = You have received an account credit in the amount of { $paymentProrated }. +subscriptionUpgrade-content-subscription-next-bill-change = Starting with your next bill, the price of your subscription will change. +subscriptionUpgrade-content-old-price-day = The previous rate was { $paymentAmountOld } per day. +subscriptionUpgrade-content-old-price-week = The previous rate was { $paymentAmountOld } per week. +subscriptionUpgrade-content-old-price-month = The previous rate was { $paymentAmountOld } per month. +subscriptionUpgrade-content-old-price-halfyear = The previous rate was { $paymentAmountOld } per six months. +subscriptionUpgrade-content-old-price-year = The previous rate was { $paymentAmountOld } per year. +subscriptionUpgrade-content-old-price-default = The previous rate was { $paymentAmountOld } per billing interval. +subscriptionUpgrade-content-old-price-day-tax = The previous rate was { $paymentAmountOld } + { $paymentTaxOld } tax per day. +subscriptionUpgrade-content-old-price-week-tax = The previous rate was { $paymentAmountOld } + { $paymentTaxOld } tax per week. +subscriptionUpgrade-content-old-price-month-tax = The previous rate was { $paymentAmountOld } + { $paymentTaxOld } tax per month. +subscriptionUpgrade-content-old-price-halfyear-tax = The previous rate was { $paymentAmountOld } + { $paymentTaxOld } tax per six months. +subscriptionUpgrade-content-old-price-year-tax = The previous rate was { $paymentAmountOld } + { $paymentTaxOld } tax per year. +subscriptionUpgrade-content-old-price-default-tax = The previous rate was { $paymentAmountOld } + { $paymentTaxOld } tax per billing interval. +subscriptionUpgrade-content-new-price-day = Going forward, you will be charged { $paymentAmountNew } per day, excluding discounts. +subscriptionUpgrade-content-new-price-week = Going forward, you will be charged { $paymentAmountNew } per week, excluding discounts. +subscriptionUpgrade-content-new-price-month = Going forward, you will be charged { $paymentAmountNew } per month, excluding discounts. +subscriptionUpgrade-content-new-price-halfyear = Going forward, you will be charged { $paymentAmountNew } per six months, excluding discounts. +subscriptionUpgrade-content-new-price-year = Going forward, you will be charged { $paymentAmountNew } per year, excluding discounts. +subscriptionUpgrade-content-new-price-default = Going forward, you will be charged { $paymentAmountNew } per billing interval, excluding discounts. +subscriptionUpgrade-content-new-price-day-dtax = Going forward, you will be charged { $paymentAmountNew } + { $paymentTaxNew } tax per day, excluding discounts. +subscriptionUpgrade-content-new-price-week-tax = Going forward, you will be charged { $paymentAmountNew } + { $paymentTaxNew } tax per week, excluding discounts. +subscriptionUpgrade-content-new-price-month-tax = Going forward, you will be charged { $paymentAmountNew } + { $paymentTaxNew } tax per month, excluding discounts. +subscriptionUpgrade-content-new-price-halfyear-tax = Going forward, you will be charged { $paymentAmountNew } + { $paymentTaxNew } tax per six months, excluding discounts. +subscriptionUpgrade-content-new-price-year-tax = Going forward, you will be charged { $paymentAmountNew } + { $paymentTaxNew } tax per year, excluding discounts. +subscriptionUpgrade-content-new-price-default-tax = Going forward, you will be charged { $paymentAmountNew } + { $paymentTaxNew } tax per billing interval, excluding discounts. +subscriptionUpgrade-existing = If any of your existing subscriptions overlap with this upgrade, we’ll handle them and send you a separate email with the details. If your new plan includes products that require installation, we’ll send you a separate email with setup instructions. +subscriptionUpgrade-auto-renew = Your subscription will automatically renew each billing period unless you choose to cancel. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.mjml new file mode 100644 index 00000000000..13843b1e1ef --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.mjml @@ -0,0 +1,46 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + +<%- include ('/partials/icon/index.mjml') %> + + + + + Thank you for upgrading! + + + + You have successfully upgraded to <%- productName %>. + + + + <% if (locals.paymentProratedInCents < 0) { %> + You have received an account credit in the amount of <%- paymentProrated %>. + <% } else if (locals.paymentProratedInCents > 0) { %> + You have been charged a one-time fee of <%- invoiceAmountDue %> to reflect your subscription’s higher price for the remainder of this billing period (<%- productPaymentCycleOld %>). + <% } %> + + Starting with your next bill, the price of your subscription will change. + + + <%= previousRate.message %> + + + <%= newRate.message %> + + + + + If any of your existing subscriptions overlap with this upgrade, we’ll handle them and send you a separate email with the details. If your new plan includes products that require installation, we’ll send you a separate email with setup instructions. + + + + Your subscription will automatically renew each billing period unless you choose to cancel. + + + + +<%- include ('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.stories.ts new file mode 100644 index 00000000000..c500eabc372 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.stories.ts @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionUpgrade', +} as Meta; + +const data = { + invoiceAmountDue: '$10.00', + paymentAmountNew: '$69.89', + paymentAmountOld: '$9.89', + productIconURLNew: + 'https://cdn.accounts.firefox.com/product-icons/mozilla-vpn-email.png', + productName: 'Product Name B', + productNameOld: 'Product Name A', + productPaymentCycleNew: 'year', + productPaymentCycleOld: 'month', + paymentProrated: '$60.00', + subscriptionSupportUrl: 'http://localhost:3030/support', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionUpgrade', + 'Sent when a user upgrades their subscription.', + data, + includes +); + +export const PositiveProrated = createStory( + { + paymentProrated: '$60.00', + paymentProratedInCents: 6000, + previousRate: { + message: 'Your previous rate was $9.89 per month.', + }, + newRate: { + message: + 'Going forward, you will be charged $69.89 per year, excluding discounts.', + }, + }, + 'Charge - Prorated amount is positive' +); + +export const PositiveProratedWithTax = createStory( + { + paymentProrated: '$60.00', + paymentProratedInCents: 6000, + paymentTaxOldInCents: 50, + paymentTaxOld: '$0.50', + paymentTaxNewInCents: 60, + paymentTaxNew: '$0.60', + previousRate: { + message: 'Your previous rate was $9.89 + $0.50 tax per month.', + }, + newRate: { + message: + 'Going forward, you will be charged $69.89 + $0.60 tax per year, excluding discounts.', + }, + }, + 'Charge - Prorated amount with tax is positive' +); + +export const NegativeProrated = createStory( + { + paymentProrated: '$60.00', + paymentProratedInCents: -6000, + previousRate: { + message: 'Your previous rate was $9.89 per month.', + }, + newRate: { + message: + 'Going forward, you will be charged $69.89 per year, excluding discounts.', + }, + }, + 'Credit - Prorated amount is negative' +); + +export const NegativeProratedWithTax = createStory( + { + paymentProrated: '$60.00', + paymentProratedInCents: -6000, + paymentTaxOldInCents: 50, + paymentTaxOld: '$0.50', + paymentTaxNewInCents: 60, + paymentTaxNew: '$0.60', + previousRate: { + message: 'Your previous rate was $9.89 + $0.50 tax per month.', + }, + newRate: { + message: + 'Going forward, you will be charged $69.89 + $0.60 tax per year, excluding discounts.', + }, + }, + 'Credit - Prorated amount with tax is negative' +); + +export const NoProrated = createStory( + { + paymentProrated: '$0.00', + paymentProratedInCents: 0, + previousRate: { + message: 'Your previous rate was $9.89 per month.', + }, + newRate: { + message: + 'Going forward, you will be charged $69.89 per year, excluding discounts.', + }, + }, + 'Prorated amount is zero' +); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.ts new file mode 100644 index 00000000000..f5b0e7d6bed --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + invoiceAmountDue: string; + paymentAmountNew: string; + paymentAmountOld: string; + productIconURLNew: string; + productName: string; + productNameOld: string; + productPaymentCycleNew: string; + productPaymentCycleOld: string; + paymentProrated: string; + subscriptionSupportUrl: string; +}; + +export const template = 'subscriptionUpgrade'; +export const version = 7; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionUpgrade-subject', + message: 'You have upgraded to <%- productName %>', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.txt new file mode 100644 index 00000000000..72f7f925f9d --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionUpgrade/index.txt @@ -0,0 +1,76 @@ +subscriptionUpgrade-subject = "You have upgraded to <%- productName %>" + +subscriptionUpgrade-title = "Thank you for upgrading!" + +subscriptionUpgrade-upgrade-info-2 = "You have successfully upgraded to <%- productName %>." + +<% if (locals.paymentProratedInCents < 0) { %> +subscriptionUpgrade-content-charge-credit = "You have received an account credit in the amount of <%- paymentProrated %>." +<% } else if (locals.paymentProratedInCents > 0) { %> +subscriptionUpgrade-content-charge-prorated-1 = "You have been charged a one-time fee of <%- invoiceAmountDue %> to reflect your subscription’s higher price for the remainder of this billing period (<%- productPaymentCycleOld %>)." +<% } %> +subscriptionUpgrade-content-subscription-will-change = "Starting with your next bill, the price of your subscription will change." +<% if (locals.paymentTaxOldInCents) { %> +<% if (locals.productPaymentCycleOld === 'day') { %> +subscriptionUpgrade-content-old-price-day-tax = "The previous rate was <%- paymentAmountOld %> + <%- paymentTaxOld %> tax per day." +<% } else if (locals.productPaymentCycleOld === 'week') { %> +subscriptionUpgrade-content-old-price-week-tax = "The previous rate was <%- paymentAmountOld %> + <%- paymentTaxOld %> tax per week." +<% } else if (locals.productPaymentCycleOld === 'month') { %> +subscriptionUpgrade-content-old-price-month-tax = "The previous rate was <%- paymentAmountOld %> + <%- paymentTaxOld %> tax per month." +<% } else if (locals.productPaymentCycleOld === 'halfyear') { %> +subscriptionUpgrade-content-old-price-halfyear-tax = "The previous rate was <%- paymentAmountOld %> + <%- paymentTaxOld %> tax per six months." +<% } else if (locals.productPaymentCycleOld === 'year') { %> +subscriptionUpgrade-content-old-price-year-tax = "The previous rate was <%- paymentAmountOld %> + <%- paymentTaxOld %> tax per year." +<% } else { %> +subscriptionUpgrade-content-old-price-default-tax = "The previous rate was <%- paymentAmountOld %> + <%- paymentTaxOld %> tax per billing interval." +<% } %> +<% } else { %> +<% if (locals.productPaymentCycleOld === 'day') { %> +subscriptionUpgrade-content-old-price-day = "The previous rate was <%- paymentAmountOld %> per day." +<% } else if (locals.productPaymentCycleOld === 'week') { %> +subscriptionUpgrade-content-old-price-week = "The previous rate was <%- paymentAmountOld %> per week." +<% } else if (locals.productPaymentCycleOld === 'month') { %> +subscriptionUpgrade-content-old-price-month = "The previous rate was <%- paymentAmountOld %> per month." +<% } else if (locals.productPaymentCycleOld === 'halfyear') { %> +subscriptionUpgrade-content-old-price-halfyear = "The previous rate was <%- paymentAmountOld %> per six months." +<% } else if (locals.productPaymentCycleOld === 'year') { %> +subscriptionUpgrade-content-old-price-year = "The previous rate was <%- paymentAmountOld %> per year." +<% } else { %> +subscriptionUpgrade-content-old-price-default = "The previous rate was <%- paymentAmountOld %> per billing interval." +<% } %> +<% } %> +<% if (locals.paymentTaxNewInCents) { %> +<% if (locals.productPaymentCycleNew === 'day') { %> +subscriptionUpgrade-content-new-price-day-tax = "Going forward, you will be charged <%- paymentAmountNew %> + <%- paymentTaxNew %> tax per day." +<% } else if (locals.productPaymentCycleNew === 'week') { %> +subscriptionUpgrade-content-new-price-week-tax = "Going forward, you will be charged <%- paymentAmountNew %> + <%- paymentTaxNew %> tax per week." +<% } else if (locals.productPaymentCycleNew === 'month') { %> +subscriptionUpgrade-content-new-price-month-tax = "Going forward, you will be charged <%- paymentAmountNew %> + <%- paymentTaxNew %> tax per month." +<% } else if (locals.productPaymentCycleNew === 'halfyear') { %> +subscriptionUpgrade-content-new-price-halfyear-tax = "Going forward, you will be charged <%- paymentAmountNew %> + <%- paymentTaxNew %> tax per six months." +<% } else if (locals.productPaymentCycleNew === 'year') { %> +subscriptionUpgrade-content-new-price-year-tax = "Going forward, you will be charged <%- paymentAmountNew %> + <%- paymentTaxNew %> tax per year." +<% } else { %> +subscriptionUpgrade-content-new-price-default-tax = "Going forward, you will be charged <%- paymentAmountNew %> + <%- paymentTaxNew %> tax per billing interval." +<% } %> +<% } else { %> +<% if (locals.productPaymentCycleNew === 'day') { %> +subscriptionUpgrade-content-new-price-day = "Going forward, you will be charged <%- paymentAmountNew %> per day." +<% } else if (locals.productPaymentCycleNew === 'week') { %> +subscriptionUpgrade-content-new-price-week = "Going forward, you will be charged <%- paymentAmountNew %> per week." +<% } else if (locals.productPaymentCycleNew === 'month') { %> +subscriptionUpgrade-content-new-price-month = "Going forward, you will be charged <%- paymentAmountNew %> per month." +<% } else if (locals.productPaymentCycleNew === 'halfyear') { %> +subscriptionUpgrade-content-new-price-halfyear = "Going forward, you will be charged <%- paymentAmountNew %> per six months." +<% } else if (locals.productPaymentCycleNew === 'year') { %> +subscriptionUpgrade-content-new-price-year = "Going forward, you will be charged <%- paymentAmountNew %> per year." +<% } else { %> +subscriptionUpgrade-content-new-price-default = "Going forward, you will be charged <%- paymentAmountNew %> per billing interval." +<% } %> +<% } %> + +subscriptionUpgrade-existing = "If any of your existing subscriptions overlap with this upgrade, we’ll handle them and send you a separate email with the details. If your new plan includes products that require installation, we’ll send you a separate email with setup instructions." + +subscriptionUpgrade-auto-renew = "Your subscription will automatically renew each billing period unless you choose to cancel." + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/en.ftl new file mode 100644 index 00000000000..46509e40507 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/en.ftl @@ -0,0 +1,3 @@ +subscriptionsPaymentExpired-subject-2 = The payment method for your subscriptions is expired or expiring soon +subscriptionsPaymentExpired-title-2 = Your payment method is expired or about to expire +subscriptionsPaymentExpired-content-2 = The payment method you’re using to make payments for the following subscriptions is expired or about to expire. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.mjml new file mode 100644 index 00000000000..0572508b98e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.mjml @@ -0,0 +1,30 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Your payment method is expired or about to expire + + + + + The payment method you’re using to make payments for the following subscriptions is expired or about to expire. + + + + +
    + <% for (const { productName } of subscriptions) { %> +
  • + <%- productName %> +
  • + <% } %> +
+
+
+
+ +<%- include('/partials/subscriptionUpdatePayment/index.mjml', { updateBillingUrl }) %> +<%- include ('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.stories.ts new file mode 100644 index 00000000000..bcb806b8d00 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.stories.ts @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionsPaymentExpired', +} as Meta; + +const data = { + subscriptions: [ + { + productName: 'Firefox Fortress', + }, + { + productName: 'Mozilla VPN', + }, + ], + updateBillingUrl: 'http://localhost:3030/subscriptions', + subscriptionSupportUrl: 'http://localhost:3030/support', + + // Had to add this in! Double check value. + productName: 'Firefox Fortress', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionsPaymentExpired', + 'Sent whenever a user has multiple subscriptions and their payment method has expired or will expire at the end of the month.', + data, + includes +); + +export const SubscriptionsPaymentExpired = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.ts new file mode 100644 index 00000000000..a5530f3c2b6 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.ts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + subscriptions: Array<{ productName: string }>; + updateBillingUrl: string; + subscriptionSupportUrl: string; + productName: string; +}; + +export const template = 'subscriptionsPaymentExpired'; +export const version = 4; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionsPaymentExpired-subject', + message: 'Payment method for <%- productName %> expired or expiring soon', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.txt new file mode 100644 index 00000000000..eabb8553174 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentExpired/index.txt @@ -0,0 +1,11 @@ +subscriptionsPaymentExpired-subject-2 = "The payment method for your subscriptions is expired or expiring soon" + +subscriptionsPaymentExpired-title-2 = "Your payment method is expired or about to expire" + +subscriptionsPaymentExpired-content-2 = "The payment method you’re using to make payments for the following subscriptions is expired or about to expire." + +<%- subscriptions.map(({ productName }) => ` - ${productName}`).join("\r") %> + +<%- include('/partials/subscriptionUpdatePayment/index.txt', { updateBillingUrl }) %> + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/en.ftl b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/en.ftl new file mode 100644 index 00000000000..8ab68db693b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/en.ftl @@ -0,0 +1,4 @@ +subscriptionsPaymentProviderCancelled-subject = Payment information update required for { -brand-mozilla } subscriptions +subscriptionsPaymentProviderCancelled-title = Sorry, we’re having trouble with your payment method +subscriptionsPaymentProviderCancelled-content-detected = We have detected a problem with your payment method for the following subscriptions. +subscriptionsPaymentProviderCancelled-content-payment-1 = It may be that your payment method has expired, or your current payment method is out-of-date. diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.mjml b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.mjml new file mode 100644 index 00000000000..e7315b909ef --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.mjml @@ -0,0 +1,36 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Sorry, we’re having trouble with your payment method + + + + + We have detected a problem with your payment method for the following subscriptions. + + + + +
    + <% for (const { productName } of subscriptions) { %> +
  • + <%- productName %> +
  • + <% } %> +
+
+ + + + It may be that your payment method has expired, or your current payment method is out-of-date. + + +
+
+ +<%- include('/partials/subscriptionUpdatePayment/index.mjml', { updateBillingUrl }) %> +<%- include ('/partials/subscriptionSupport/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.stories.ts b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.stories.ts new file mode 100644 index 00000000000..a036af9ba52 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.stories.ts @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { subplatStoryWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'SubPlat Emails/Templates/subscriptionsPaymentProviderCancelled', +} as Meta; + +const data = { + subscriptions: [ + { + productName: 'Firefox Fortress', + }, + { + productName: 'Mozilla VPN', + }, + ], + subscriptionSupportUrl: 'http://localhost:3030/support', + updateBillingUrl: 'http://localhost:3030/subscriptions', + + // Had to add this in. Double check! + productName: 'Firefox Fortress', +}; + +const createStory = subplatStoryWithProps( + 'subscriptionsPaymentProviderCancelled', + 'Sent when a user has multiple subscriptions and a problem has been detected with payment method.', + data, + includes +); + +export const SubscriptionsPaymentProviderCancelled = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.ts b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.ts new file mode 100644 index 00000000000..f52d883f842 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.ts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + subscriptions: Array<{ productName: string }>; + subscriptionSupportUrl: string; + updateBillingUrl: string; + productName: string; +}; + +export const template = 'subscriptionsPaymentProviderCancelled'; +export const version = 3; +export const layout = 'subscription'; +export const includes = { + subject: { + id: 'subscriptionsPaymentProviderCancelled-subject-2', + message: 'Your <%- productName %> subscription has been cancelled', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.txt b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.txt new file mode 100644 index 00000000000..d0cbcaae0d8 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/subscriptionsPaymentProviderCancelled/index.txt @@ -0,0 +1,13 @@ +subscriptionsPaymentProviderCancelled-subject = "Payment information update required for Mozilla subscriptions" + +subscriptionsPaymentProviderCancelled-title = "Sorry, we’re having trouble with your payment method" + +subscriptionsPaymentProviderCancelled-content-detected = "We have detected a problem with your payment method for the following subscriptions." + +<%- subscriptions.map(({ productName }) => ` - ${productName}`).join("\r") %> + +subscriptionsPaymentProviderCancelled-content-payment-1 = "It may be that your payment method has expired, or your current payment method is out-of-date." + +<%- include ('/partials/subscriptionUpdatePayment/index.txt', { updateBillingUrl }) %> + +<%- include ('/partials/subscriptionSupport/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/unblockCode/en.ftl b/libs/accounts/email-renderer/src/templates/unblockCode/en.ftl new file mode 100644 index 00000000000..91bce8916d9 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/unblockCode/en.ftl @@ -0,0 +1,11 @@ +# Variables: +# $unblockCode (String) - The authorization code for sign-in +unblockCode-subject-2 = Use { $unblockCode } to sign in +unblockCode-preview = This code expires in one hour +unblockCode-title = Is this you signing in? +unblockCode-prompt = If yes, here is the authorization code you need: +# Variables: +# $unblockCode (String) - An alphanumeric code +unblockCode-prompt-plaintext = If yes, here is the authorization code you need: { $unblockCode } +unblockCode-report = If no, help us fend off intruders and report it to us. +unblockCode-report-plaintext = If no, help us fend off intruders and report it to us. diff --git a/libs/accounts/email-renderer/src/templates/unblockCode/index.mjml b/libs/accounts/email-renderer/src/templates/unblockCode/index.mjml new file mode 100644 index 00000000000..2b993306686 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/unblockCode/index.mjml @@ -0,0 +1,32 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Is this you signing in? + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + If yes, here is the authorization code you need: + + + + <%- unblockCode %> + + + + + If no, help us fend off intruders and + report it to us. + + + + diff --git a/libs/accounts/email-renderer/src/templates/unblockCode/index.stories.ts b/libs/accounts/email-renderer/src/templates/unblockCode/index.stories.ts new file mode 100644 index 00000000000..40de1caeabf --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/unblockCode/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/unblockCode', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + unblockCode: '1ILO0Z5P', + reportSignInLink: 'http://localhost:3030/report_signin', +}; + +const createStory = storyWithProps( + 'unblockCode', + 'Sent to verify or unblock an account via code that has reached the login attempt rate limit.', + data, + includes +); + +export const UnblockCode = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/unblockCode/index.ts b/libs/accounts/email-renderer/src/templates/unblockCode/index.ts new file mode 100644 index 00000000000..17c2d55b846 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/unblockCode/index.ts @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + unblockCode: string; + reportSignInLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'unblockCode'; +export const version = 7; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'unblockCode-subject-2', + message: 'Use <%- unblockCode %> to sign in', + }, + preview: { + id: 'unblockCode-preview', + message: 'This code expires in one hour', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/unblockCode/index.txt b/libs/accounts/email-renderer/src/templates/unblockCode/index.txt new file mode 100644 index 00000000000..ac7349715b1 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/unblockCode/index.txt @@ -0,0 +1,8 @@ +unblockCode-title = "Is this you signing in?" + +<%- include('/partials/userInfo/index.txt') %> + +unblockCode-prompt-plaintext = "If yes, here is the authorization code you need: <%- unblockCode %>" + +unblockCode-report-plaintext = "If no, help us fend off intruders and report it to us." +<%- reportSignInLink %> diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFinal/en.ftl b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/en.ftl new file mode 100644 index 00000000000..d4552ba4d21 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/en.ftl @@ -0,0 +1,4 @@ +verificationReminderFinal-subject = Final reminder to confirm your account +verificationReminderFinal-description-2 = A couple of weeks ago you created a { -product-mozilla-account }, but never confirmed it. For your security, we will delete the account if not verified in the next 24 hours. +confirm-account = Confirm account +confirm-account-plaintext = { confirm-account }: diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.mjml b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.mjml new file mode 100644 index 00000000000..6983566c632 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.mjml @@ -0,0 +1,17 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + A couple of weeks ago you created a Mozilla account, but never confirmed it. For your security, we will delete the account if not verified in the next 24 hours. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "confirm-account", + buttonText: "Confirm account" +}) %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.stories.ts b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.stories.ts new file mode 100644 index 00000000000..59283c0f299 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.stories.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verificationReminderFinal', +} as Meta; + +const data = { + link: 'http://localhost:3030/verify_email', +}; + +const createStory = storyWithProps( + 'verificationReminderFinal', + 'Final reminder sent to users that have not verified their account, sent 15 days after initial attempt.', + data, + includes +); + +export const VerificationReminderFinal = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.ts b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.ts new file mode 100644 index 00000000000..e33c993dc46 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.ts @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; +}; + +export const template = 'verificationReminderFinal'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verificationReminderFinal-subject', + message: 'Final reminder to confirm your account', + }, + action: { + id: 'confirm-account', + message: 'Confirm account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.txt b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.txt new file mode 100644 index 00000000000..08f9cea8227 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFinal/index.txt @@ -0,0 +1,7 @@ +verificationReminderFinal-description-2 = "A couple of weeks ago you created a Mozilla account, but never confirmed it. For your security, we will delete the account if not verified in the next 24 hours." + +confirm-account-plaintext = "Confirm account:" +<%- link %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFirst/en.ftl b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/en.ftl new file mode 100644 index 00000000000..8e78c8a8ee7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/en.ftl @@ -0,0 +1,7 @@ +verificationReminderFirst-subject-2 = Remember to confirm your account +verificationReminderFirst-title-3 = Welcome to { -brand-mozilla }! +verificationReminderFirst-description-3 = A few days ago you created a { -product-mozilla-account }, but never confirmed it. Please confirm your account in the next 15 days or it will be automatically deleted. +verificationReminderFirst-sub-description-3 = Don’t miss out on the browser that puts you and your privacy first. +confirm-email-2 = Confirm account +confirm-email-plaintext-2 = { confirm-email-2 }: +verificationReminderFirst-action-2 = Confirm account diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.mjml b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.mjml new file mode 100644 index 00000000000..ce83f13c3c4 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.mjml @@ -0,0 +1,28 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Welcome to Mozilla! + + + + + + + + A few days ago you created a Mozilla account, but never confirmed it. Please confirm your account in the next 15 days or it will be automatically deleted. + + + Don’t miss out on the browser that puts you and your privacy first. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "verificationReminderFirst-action-2", + buttonText: "Confirm account" +}) %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.stories.ts b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.stories.ts new file mode 100644 index 00000000000..306a3cfb61e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.stories.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verificationReminderFirst', +} as Meta; + +const data = { + link: 'http://localhost:3030/verify_email', +}; + +const createStory = storyWithProps( + 'verificationReminderFirst', + 'Reminder sent 1 day after an ignored verification.', + data, + includes +); + +export const VerificationReminderFirst = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.ts b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.ts new file mode 100644 index 00000000000..c12e4d5fa22 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.ts @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; +}; + +export const template = 'verificationReminderFirst'; +export const version = 10; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verificationReminderFirst-subject-2', + message: 'Remember to confirm your account', + }, + action: { + id: 'verificationReminderFirst-action-2', + message: 'Confirm account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.txt b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.txt new file mode 100644 index 00000000000..583f598c943 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderFirst/index.txt @@ -0,0 +1,11 @@ +verificationReminderFirst-title-3 = "Welcome to Mozilla" + +verificationReminderFirst-description-3 = "A few days ago you created a Mozilla account, but never confirmed it. Please confirm your account in the next 15 days or it will be automatically deleted." + +verificationReminderFirst-sub-description-3 = "Don’t miss out on the browser that puts you and your privacy first." + +confirm-email-plaintext-2 = "Confirm account:" +<%- link %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderSecond/en.ftl b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/en.ftl new file mode 100644 index 00000000000..b18d293994f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/en.ftl @@ -0,0 +1,6 @@ +verificationReminderSecond-subject-2 = Remember to confirm your account +verificationReminderSecond-title-3 = Don’t miss out on { -brand-mozilla }! +verificationReminderSecond-description-4 = A few days ago you created a { -product-mozilla-account }, but never confirmed it. Please confirm your account in the next 10 days or it will be automatically deleted. +verificationReminderSecond-second-description-3 = Your { -product-mozilla-account } lets you sync your { -brand-firefox } experience across devices and unlocks access to more privacy-protecting products from { -brand-mozilla }. +verificationReminderSecond-sub-description-2 = Be part of our mission to transform the internet into a place that’s open for everyone. +verificationReminderSecond-action-2 = Confirm account diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderSecond/includes.json b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/includes.json new file mode 100644 index 00000000000..955c7e30552 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/includes.json @@ -0,0 +1,10 @@ +{ + "subject": { + "id": "verificationReminderSecond-subject-2", + "message": "Remember to confirm your account" + }, + "action": { + "id": "verificationReminderSecond-action-2", + "message": "Confirm account" + } +} diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.mjml b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.mjml new file mode 100644 index 00000000000..d2abed44f0f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.mjml @@ -0,0 +1,31 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Don’t miss out on Mozilla! + + + + + + + + A few days ago you created a Mozilla account, but never confirmed it. Please confirm your account in the next 10 days or it will be automatically deleted. + + + Your Mozilla account lets you sync your Firefox experience across devices and unlocks access to more privacy-protecting products from Mozilla. + + + Be part of our mission to transform the internet into a place that’s open for everyone. + + + + +<%- include('/partials/button/index.mjml', { + buttonL10nId: "verificationReminderSecond-action-2", + buttonText: "Confirm account" +}) %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.stories.ts b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.stories.ts new file mode 100644 index 00000000000..289e2174ca3 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.stories.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verificationReminderSecond', +} as Meta; + +const data = { + link: 'http://localhost:3030/verify_email', +}; + +const createStory = storyWithProps( + 'verificationReminderSecond', + 'Reminder sent 5 days after an ignored verification.', + data, + includes +); + +export const verificationReminderSecond = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.ts b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.ts new file mode 100644 index 00000000000..8c339c2d83c --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.ts @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; +}; + +export const template = 'verificationReminderSecond'; +export const version = 11; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verificationReminderSecond-subject-2', + message: 'Remember to confirm your account', + }, + action: { + id: 'verificationReminderSecond-action-2', + message: 'Confirm account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.txt b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.txt new file mode 100644 index 00000000000..294123e9505 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verificationReminderSecond/index.txt @@ -0,0 +1,13 @@ +verificationReminderSecond-title-3 = "Don’t miss out on Mozilla!" + +verificationReminderSecond-description-4 = "A few days ago you created a Mozilla account, but never confirmed it. Please confirm your account in the next 10 days or it will be automatically deleted." + +verificationReminderSecond-second-description-3 = "Your Mozilla account lets you sync your Firefox experience across devices and unlocks access to more privacy-protecting products from Mozilla." + +verificationReminderSecond-sub-description-2 = "Be part of our mission to transform the internet into a place that’s open for everyone." + +confirm-email-plaintext-2 = "Confirm account:" +<%- link %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verify/en.ftl b/libs/accounts/email-renderer/src/templates/verify/en.ftl new file mode 100644 index 00000000000..44d3dd44ec6 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verify/en.ftl @@ -0,0 +1,4 @@ +verify-title-3 = Open the internet with { -brand-mozilla } +verify-description-2 = Confirm your account and get the most out of { -brand-mozilla } everywhere you sign in starting with: +verify-subject = Finish creating your account +verify-action-2 = Confirm account diff --git a/libs/accounts/email-renderer/src/templates/verify/index.mjml b/libs/accounts/email-renderer/src/templates/verify/index.mjml new file mode 100644 index 00000000000..bfff6b582d6 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verify/index.mjml @@ -0,0 +1,22 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + Open the internet with Mozilla + + + + + + Confirm your account and get the most out of Mozilla everywhere you sign in starting with: + + + +<%- include('/partials/userInfo/index.mjml') %> +<%- include('/partials/button/index.mjml', { + buttonL10nId: "verify-action-2", + buttonText: "Confirm account" +}) %> +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verify/index.stories.ts b/libs/accounts/email-renderer/src/templates/verify/index.stories.ts new file mode 100644 index 00000000000..4dd82fdd915 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verify/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verify', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/verify_email', + sync: true, +}; + +const createStory = storyWithProps( + 'verify', + "Sent to users that create an account through Firefox, don't verify their email, and go into Sync preferences to resend the verification email as a link.", + data, + includes +); + +export const VerifyEmail = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verify/index.ts b/libs/accounts/email-renderer/src/templates/verify/index.ts new file mode 100644 index 00000000000..145866f714e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verify/index.ts @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + sync: boolean; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'verify'; +export const version = 7; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verify-subject', + message: 'Finish creating your account', + }, + action: { + id: 'verify-action-2', + message: 'Confirm account', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verify/index.txt b/libs/accounts/email-renderer/src/templates/verify/index.txt new file mode 100644 index 00000000000..d1b0e621bdc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verify/index.txt @@ -0,0 +1,10 @@ +verify-title-3 = "Open the internet with Mozilla" + +verify-description-2 = "Confirm your account and get the most out of Mozilla everywhere you sign in starting with:" + +confirm-email-plaintext-2 = "Confirm account:" +<%- link %> + +<%- include('/partials/automatedEmailNoAction/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyAccountChange/en.ftl b/libs/accounts/email-renderer/src/templates/verifyAccountChange/en.ftl new file mode 100644 index 00000000000..c70744467a8 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyAccountChange/en.ftl @@ -0,0 +1,19 @@ +# Variables: +# $code (String) - The verification code +verifyAccountChange-subject = Use { $code } to change your account +# Variables: +# $expirationTime (Number) - Represents the expiration time in minutes +verifyAccountChange-preview = { $expirationTime -> + [one] This code expires in { $expirationTime } minute. + *[other] This code expires in { $expirationTime } minutes. +} +verifyAccountChange-title = Are you changing your account info? +# After the colon is a description of the device used to sign in to the service +verifyAccountChange-safe = Help us keep your account safe by approving this change on: +verifyAccountChange-prompt = If yes, here is your authorization code: +# Variables: +# $expirationTime (Number) - Represents the expiration time in minutes +verifyAccountChange-expiry-notice = { $expirationTime -> + [one] It expires in { $expirationTime } minute. + *[other] It expires in { $expirationTime } minutes. +} diff --git a/libs/accounts/email-renderer/src/templates/verifyAccountChange/includes.json b/libs/accounts/email-renderer/src/templates/verifyAccountChange/includes.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.mjml b/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.mjml new file mode 100644 index 00000000000..cc882000b80 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.mjml @@ -0,0 +1,36 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Are you changing your account info? + + + Help us keep your account safe by approving this change on: + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + + If yes, here is your authorization code: + + + + <%- code %> + + + + It expires in <%- expirationTime === 1 ? '1 minute' : expirationTime + ' minutes' %>. + + + + + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.stories.ts b/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.stories.ts new file mode 100644 index 00000000000..f3f3b287e05 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.stories.ts @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verifyAccountChange', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + code: '918398', + expirationTime: 5, + passwordChangeLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'verifyAccountChange', + 'Sent to verify a change in account settings via code.', + data, + includes +); + +export const VerifyAccountChangeEmail = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.ts b/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.ts new file mode 100644 index 00000000000..5fb2475196b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.ts @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + code: string; + expirationTime: number; + passwordChangeLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'verifyAccountChange'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verifyAccountChange-subject', + message: 'Use <%- code %> to change your account', + }, + preview: { + id: 'verifyAccountChange-preview', + message: + "This code expires in <%- expirationTime === 1 ? '1 minute' : expirationTime + ' minutes' %>.", + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.txt b/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.txt new file mode 100644 index 00000000000..6ab4e2ef041 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyAccountChange/index.txt @@ -0,0 +1,13 @@ +verifyAccountChange-title = "Are you changing your account info?" + +verifyAccountChange-safe = "Help us keep your account safe by approving this change on:" + +<%- include('/partials/userInfo/index.txt') %> + +verifyAccountChange-prompt = "If yes, here is your authorization code:" + +<%- code %> + +verifyAccountChange-expiry-notice = "It expires in <%- expirationTime === 1 ? '1 minute' : expirationTime + ' minutes' %>." + +<%- include('/partials/automatedEmailChangePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyLogin/en.ftl b/libs/accounts/email-renderer/src/templates/verifyLogin/en.ftl new file mode 100644 index 00000000000..f61885fee5e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLogin/en.ftl @@ -0,0 +1,6 @@ +# Variables: +# $clientName (String) - A client the user hasn't signed into before (e.g. Firefox, Sync) +verifyLogin-title-2 = Did you sign in to { $clientName }? +verifyLogin-description-2 = Help us keep your account safe by confirming you signed in on: +verifyLogin-subject-2 = Confirm sign-in +verifyLogin-action = Confirm sign-in diff --git a/libs/accounts/email-renderer/src/templates/verifyLogin/includes.json b/libs/accounts/email-renderer/src/templates/verifyLogin/includes.json new file mode 100644 index 00000000000..8bac11ada82 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLogin/includes.json @@ -0,0 +1,10 @@ +{ + "subject": { + "id": "verifyLogin-subject-2", + "message": "Confirm sign-in" + }, + "action": { + "id": "verifyLogin-action", + "message": "Confirm sign-in" + } +} diff --git a/libs/accounts/email-renderer/src/templates/verifyLogin/index.mjml b/libs/accounts/email-renderer/src/templates/verifyLogin/index.mjml new file mode 100644 index 00000000000..d4d795448ce --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLogin/index.mjml @@ -0,0 +1,30 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + + Did you sign in to <%- clientName %>? + + + + + + + + + + Help us keep your account safe by confirming you signed in on: + + + + + +<%- include('/partials/userInfo/index.mjml') %> +<%- include('/partials/button/index.mjml', { + buttonL10nId: "verifyLogin-action", + buttonText: "Confirm sign-in" +}) %> +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyLogin/index.stories.ts b/libs/accounts/email-renderer/src/templates/verifyLogin/index.stories.ts new file mode 100644 index 00000000000..2920a6675f2 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLogin/index.stories.ts @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verifyLogin', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/complete_signin', + clientName: 'Firefox', + passwordChangeLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'verifyLogin', + 'Sent to users to verify a new login.', + data, + includes +); + +export const VerifyLoginFirefox = createStory( + {}, + 'Sent to users to confirm a new login to a Firefox Browser' +); +export const VerifyLoginOther = createStory( + { clientName: 'Some Other Relier' }, + 'Sent to users to confirm a new login to a Firefox Service' +); diff --git a/libs/accounts/email-renderer/src/templates/verifyLogin/index.ts b/libs/accounts/email-renderer/src/templates/verifyLogin/index.ts new file mode 100644 index 00000000000..2fd880b5e0e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLogin/index.ts @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + clientName: string; + passwordChangeLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'verifyLogin'; +export const version = 6; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verifyLogin-subject-2', + message: 'Confirm sign-in', + }, + action: { + id: 'verifyLogin-action', + message: 'Confirm sign-in', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verifyLogin/index.txt b/libs/accounts/email-renderer/src/templates/verifyLogin/index.txt new file mode 100644 index 00000000000..34b07239ef8 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLogin/index.txt @@ -0,0 +1,12 @@ +verifyLogin-title-2 = "Did you sign in to <%- clientName %>?" + +verifyLogin-description-2 = "Help us keep your account safe by confirming you signed in on:" + +<%- include('/partials/userInfo/index.txt') %> + +verifyLogin-action = "Confirm sign-in" +<%- link %> + +<%- include('/partials/automatedEmailNotAuthorized/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyLoginCode/en.ftl b/libs/accounts/email-renderer/src/templates/verifyLoginCode/en.ftl new file mode 100644 index 00000000000..ed62a636476 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLoginCode/en.ftl @@ -0,0 +1,11 @@ +# Variables: +# $code (String) - The confirmation code for sign-in +verifyLoginCode-subject-line-3 = Use { $code } to sign in +verifyLoginCode-preview = This code expires in 5 minutes. +# Variables: +# $serviceName (String) - A service the user hasn't signed into before (e.g. Firefox) +verifyLoginCode-title-2 = Did you sign in to { $serviceName }? +# After the colon is a description of the device used to sign in to the service +verifyLoginCode-safe = Help us keep your account safe by approving your sign-in on: +verifyLoginCode-prompt-3 = If yes, here is your authorization code: +verifyLoginCode-expiry-notice = It expires in 5 minutes. diff --git a/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.mjml b/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.mjml new file mode 100644 index 00000000000..d7d88180282 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.mjml @@ -0,0 +1,34 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Did you sign in to <%- locals.serviceName %>? + + + Help us keep your account safe by approving your sign-in on: + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + + If yes, here is your authorization code: + + + + <%- code %> + + + It expires in 5 minutes. + + + + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.stories.ts b/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.stories.ts new file mode 100644 index 00000000000..3cc575895fc --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.stories.ts @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verifyLoginCode', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + code: '918398', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + serviceName: 'Firefox', +}; + +const createStory = storyWithProps( + 'verifyLoginCode', + 'Sent to verify a login via code.', + data, + includes +); + +export const VerifyLoginCodeEmail = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.ts b/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.ts new file mode 100644 index 00000000000..15d5fd02a61 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.ts @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + code: string; + passwordChangeLink: string; + serviceName: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; + + cmsRpClientId?: string; + cmsRpFromName?: string; + entrypoint?: string; + subject?: string; + headline?: string; + description?: string; +}; + +export const template = 'verifyLoginCode'; +export const version = 9; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verifyLoginCode-subject-line-3', + message: 'Use <%- code %> to sign in', + }, + preview: { + id: 'verifyLoginCode-preview', + message: 'This code expires in 5 minutes.', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.txt b/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.txt new file mode 100644 index 00000000000..33d666a7f7c --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLoginCode/index.txt @@ -0,0 +1,13 @@ +verifyLoginCode-title-2 = "Did you sign in to <%- serviceName %>?" + +verifyLoginCode-safe = "Help us keep your account safe by approving your sign-in on:" + +<%- include('/partials/userInfo/index.txt') %> + +verifyLoginCode-prompt-3 = "If yes, here is your authorization code:" + +<%- code %> + +verifyLoginCode-expiry-notice = "It expires in 5 minutes." + +<%- include('/partials/automatedEmailChangePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.mjml b/libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.mjml new file mode 100644 index 00000000000..ce1a7f987ad --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.mjml @@ -0,0 +1,34 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + <%- locals.headline %> + + + <%- locals.description %> + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + + If yes, here is your authorization code: + + + + <%- code %> + + + It expires in 5 minutes. + + + + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.stories.ts b/libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.stories.ts new file mode 100644 index 00000000000..5f5bbc6bbc9 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.stories.ts @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { includes, TemplateData } from './index'; +import { storyWithProps } from '../../storybook-email'; + +export default { + title: 'FxA Emails/Templates/verifyLoginCode/Strapi', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + code: '918398', + passwordChangeLink: 'http://localhost:3030/settings/change_password', + serviceName: 'Firefox', + cmsRpClientId: '00f00f', + cmsRpFromName: 'Testo Inc.', + entrypoint: 'quux', + subject: 'Confirm Your Login', + headline: 'Are you signing in to use Product?', + description: 'Use the code below to verify your signin.', +}; + +const createStory = storyWithProps( + 'verifyLoginCode', + 'Sent to verify a login via code.', + data, + includes +); + +export const VerifyLoginCodeEmailStrapi = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.txt b/libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.txt new file mode 100644 index 00000000000..f245a832130 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyLoginCode/strapi.txt @@ -0,0 +1,13 @@ +<%- locals.headline %> + +<%- locals.description %> + +<%- include('/partials/userInfo/index.txt') %> + +verifyLoginCode-prompt-3 = "If yes, here is your authorization code:" + +<%- code %> + +verifyLoginCode-expiry-notice = "It expires in 5 minutes." + +<%- include('/partials/automatedEmailChangePassword/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyPrimary/en.ftl b/libs/accounts/email-renderer/src/templates/verifyPrimary/en.ftl new file mode 100644 index 00000000000..d3c66979238 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyPrimary/en.ftl @@ -0,0 +1,6 @@ +verifyPrimary-title-2 = Confirm primary email +verifyPrimary-description = A request to perform an account change has been made from the following device: +verifyPrimary-subject = Confirm primary email +verifyPrimary-action-2 = Confirm email +verifyPrimary-action-plaintext-2 = { verifyPrimary-action-2 }: +verifyPrimary-post-verify-2 = Once confirmed, account changes like adding a secondary email will become possible from this device. diff --git a/libs/accounts/email-renderer/src/templates/verifyPrimary/includes.json b/libs/accounts/email-renderer/src/templates/verifyPrimary/includes.json new file mode 100644 index 00000000000..2f6f0ae787e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyPrimary/includes.json @@ -0,0 +1,10 @@ +{ + "subject": { + "id": "verifyPrimary-subject", + "message": "Confirm primary email" + }, + "action": { + "id": "verifyPrimary-action-2", + "message": "Confirm email" + } +} diff --git a/libs/accounts/email-renderer/src/templates/verifyPrimary/index.mjml b/libs/accounts/email-renderer/src/templates/verifyPrimary/index.mjml new file mode 100644 index 00000000000..4b4de17081b --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyPrimary/index.mjml @@ -0,0 +1,41 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Confirm primary email + + + + + + + + + A request to perform an account change has been made from the following + device: + + + + + +<%- include('/partials/userInfo/index.mjml') %> +<%- include('/partials/button/index.mjml', { + buttonL10nId: "verifyPrimary-action-2", + buttonText: "Confirm email" +}) %> + + + + + + Once confirmed, account changes like adding a secondary email will become + possible from this device. + + + + + +<%- include('/partials/automatedEmailChangePassword/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyPrimary/index.stories.ts b/libs/accounts/email-renderer/src/templates/verifyPrimary/index.stories.ts new file mode 100644 index 00000000000..ec73602bd14 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyPrimary/index.stories.ts @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verifyPrimary', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + link: 'http://localhost:3030/verify_primary_email', + sync: false, + passwordChangeLink: 'http://localhost:3030/settings/change_password', +}; + +const createStory = storyWithProps( + 'verifyPrimary', + 'Sent to users with an unverified primary email, meaning an unverified account, when they attempt an action requiring a verified account.', + data, + includes +); + +export const VerifyPrimaryEmail = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verifyPrimary/index.ts b/libs/accounts/email-renderer/src/templates/verifyPrimary/index.ts new file mode 100644 index 00000000000..800ca9ce890 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyPrimary/index.ts @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + link: string; + sync: boolean; + passwordChangeLink: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'verifyPrimary'; +export const version = 8; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verifyPrimary-subject', + message: 'Confirm primary email', + }, + action: { + id: 'verifyPrimary-action-2', + message: 'Confirm email', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verifyPrimary/index.txt b/libs/accounts/email-renderer/src/templates/verifyPrimary/index.txt new file mode 100644 index 00000000000..4f8cff57c54 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyPrimary/index.txt @@ -0,0 +1,13 @@ +verifyPrimary-title-2 = "Confirm primary email" + +verifyPrimary-description = "A request to perform an account change has been made from the following device:" +<%- include('/partials/userInfo/index.txt') %> + +verifyPrimary-action-plaintext-2 = "Confirm email:" +<%- link %> + +verifyPrimary-post-verify-2 = "Once confirmed, account changes like adding a secondary email will become possible from this device." + +<%- include('/partials/changePassword/index.txt') %> + +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verifySecondaryCode/en.ftl b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/en.ftl new file mode 100644 index 00000000000..d7fcab131ce --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/en.ftl @@ -0,0 +1,11 @@ +# Variables: +# $code (String) - The confirmation code for secondary email +verifySecondaryCode-subject-2 = Use { $code } to confirm your secondary email +verifySecondaryCode-preview = This code expires in 5 minutes. +verifySecondaryCode-title-2 = Confirm secondary email +verifySecondaryCode-action-2 = Confirm email +# Variables: +# $email (string) A user's unverified secondary email address +verifySecondaryCode-explainer-2 = A request to use { $email } as a secondary email address has been made from the following { -product-mozilla-account }: +verifySecondaryCode-prompt-2 = Use this confirmation code: +verifySecondaryCode-expiry-notice-2 = It expires in 5 minutes. Once confirmed, this address will begin receiving security notifications and confirmations. diff --git a/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.mjml b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.mjml new file mode 100644 index 00000000000..0358517db5e --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.mjml @@ -0,0 +1,33 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Confirm secondary email + + + + A request to use <%= email %> as a secondary email address has been made from the following Mozilla account: + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + Use this confirmation code: + + + <%- code %> + + + It expires in 5 minutes. Once confirmed, this address will begin receiving security notifications and confirmations. + + + + +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.stories.ts b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.stories.ts new file mode 100644 index 00000000000..93cb0f27a25 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.stories.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verifySecondaryCode', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + email: 'foo@bar.com', + code: '918398', +}; + +const createStory = storyWithProps( + 'verifySecondaryCode', + 'Sent to verify the addition of a secondary email via code.', + data, + includes +); + +export const VerifySecondaryCode = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.ts b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.ts new file mode 100644 index 00000000000..672e0068df7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.ts @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + email: string; + code: string; + time: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; +}; + +export const template = 'verifySecondaryCode'; +export const version = 1; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verifySecondaryCode-subject-2', + message: 'Use <%- code %> to confirm your secondary email', + }, + preview: { + id: 'verifySecondaryCode-preview', + message: 'This code expires in 5 minutes.', + }, + action: { + id: 'verifySecondaryCode-action-2', + message: 'Confirm email', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.txt b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.txt new file mode 100644 index 00000000000..e4d99296e79 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifySecondaryCode/index.txt @@ -0,0 +1,13 @@ +verifySecondaryCode-title-2 = "Confirm secondary email" + +verifySecondaryCode-explainer-2 = "A request to use <%- email %> as a secondary email address has been made from the following Mozilla account:" + +<%- include('/partials/userInfo/index.txt') %> + +verifySecondaryCode-prompt-2 = "Use this confirmation code:" +<%- code %> + +verifySecondaryCode-expiry-notice-2 = "It expires in 5 minutes. Once confirmed, this address will begin receiving security notifications and confirmations." + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyShortCode/en.ftl b/libs/accounts/email-renderer/src/templates/verifyShortCode/en.ftl new file mode 100644 index 00000000000..ee66a0c879f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyShortCode/en.ftl @@ -0,0 +1,9 @@ +# Variables: +# $code (String) - comfirmation code for the account +verifyShortCode-subject-4 = Use { $code } to confirm your account +verifyShortCode-preview-2 = This code expires in 5 minutes +verifyShortCode-title-3 = Open the internet with { -brand-mozilla } +# Information on the browser and device triggering this confirmation email follows below this string. +verifyShortCode-title-subtext-2 = Confirm your account and get the most out of { -brand-mozilla } everywhere you sign in starting with: +verifyShortCode-prompt-3 = Use this confirmation code: +verifyShortCode-expiry-notice = It expires in 5 minutes. diff --git a/libs/accounts/email-renderer/src/templates/verifyShortCode/index.mjml b/libs/accounts/email-renderer/src/templates/verifyShortCode/index.mjml new file mode 100644 index 00000000000..4f5253c5be7 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyShortCode/index.mjml @@ -0,0 +1,32 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + Open the internet with Mozilla + + + Confirm your account and get the most out of Mozilla everywhere you sign in starting with: + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + Use this confirmation code: + + + <%- code %> + + + It expires in 5 minutes. + + + + +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyShortCode/index.stories.ts b/libs/accounts/email-renderer/src/templates/verifyShortCode/index.stories.ts new file mode 100644 index 00000000000..8820ec83dc6 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyShortCode/index.stories.ts @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verifyShortCode', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + code: '918398', +}; + +const createStory = storyWithProps( + 'verifyShortCode', + 'Sent to users to verify their account via code after signing up.', + data, + includes +); + +export const VerifyShortCode = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verifyShortCode/index.ts b/libs/accounts/email-renderer/src/templates/verifyShortCode/index.ts new file mode 100644 index 00000000000..73d23c92e98 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyShortCode/index.ts @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export type TemplateData = { + code: string; + device: { + uaBrowser: string; + uaOSVersion: string; + uaOS: string; + }; + location: { + stateCode: string; + country: string; + city: string; + }; + date: string; + cmsRpClientId?: string; + cmsRpFromName?: string; + entrypoint?: string; + subject?: string; + headline?: string; + description?: string; + time?: string; +}; + +export const template = 'verifyShortCode'; +export const version = 4; +export const layout = 'fxa'; +export const includes = { + subject: { + id: 'verifyShortCode-subject-4', + message: 'Use <%- code %> to confirm your account', + }, + preview: { + id: 'verifyShortCode-preview-2', + message: 'This code expires in 5 minutes', + }, +}; diff --git a/libs/accounts/email-renderer/src/templates/verifyShortCode/index.txt b/libs/accounts/email-renderer/src/templates/verifyShortCode/index.txt new file mode 100644 index 00000000000..50dcb73688d --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyShortCode/index.txt @@ -0,0 +1,13 @@ +verifyShortCode-title-3 = "Open the internet with Mozilla" + +verifyShortCode-title-subtext-2 = "Confirm your account and get the most out of Mozilla everywhere you sign in starting with:" + +<%- include('/partials/userInfo/index.txt') %> + +verifyShortCode-prompt-3 = "Use this confirmation code:" +<%- code %> + +verifyShortCode-expiry-notice = "It expires in 5 minutes." + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.mjml b/libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.mjml new file mode 100644 index 00000000000..6674641017f --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.mjml @@ -0,0 +1,32 @@ +<%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. %> + + + + + <%- locals.headline %> + + + <%- locals.description %> + + + + +<%- include('/partials/userInfo/index.mjml') %> + + + + + Use this confirmation code: + + + <%- code %> + + + It expires in 5 minutes. + + + + +<%- include('/partials/automatedEmailNoAction/index.mjml') %> diff --git a/libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.stories.ts b/libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.stories.ts new file mode 100644 index 00000000000..5b4530a5b36 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.stories.ts @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Meta } from '@storybook/html'; +import { MOCK_USER_INFO } from '../../partials/userInfo/mocks'; +import { storyWithProps } from '../../storybook-email'; +import { includes, TemplateData } from './index'; + +export default { + title: 'FxA Emails/Templates/verifyShortCode/Strapi', +} as Meta; + +const data = { + ...MOCK_USER_INFO, + code: '918398', + cmsRpClientId: '00f00f', + cmsRpFromName: 'Testo Inc.', + entrypoint: 'quux', + subject: 'Confirm Your Account', + headline: 'Verify you account to start using Product!', + description: 'Use the code below to verify your account and gogogo', +}; + +const createStory = storyWithProps( + 'verifyShortCode', + 'Sent to users to verify their account via code after signing up.', + data, + includes, + 'fxa', + 'strapi' +); + +export const VerifyShortCodeStrapi = createStory(); diff --git a/libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.txt b/libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.txt new file mode 100644 index 00000000000..1ff05b4b391 --- /dev/null +++ b/libs/accounts/email-renderer/src/templates/verifyShortCode/strapi.txt @@ -0,0 +1,13 @@ +<%- locals.headline %> + +<%- locals.description %> + +<%- include('/partials/userInfo/index.txt') %> + +verifyShortCode-prompt-3 = "Use this confirmation code:" +<%- code %> + +verifyShortCode-expiry-notice = "It expires in 5 minutes." + +<%- include('/partials/automatedEmailNoAction/index.txt') %> +<%- include('/partials/support/index.txt') %> diff --git a/libs/accounts/email-renderer/tsconfig.json b/libs/accounts/email-renderer/tsconfig.json new file mode 100644 index 00000000000..e626819bed5 --- /dev/null +++ b/libs/accounts/email-renderer/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "include": [], + "exclude": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/accounts/email-renderer/tsconfig.lib.json b/libs/accounts/email-renderer/tsconfig.lib.json new file mode 100644 index 00000000000..62ffc3fa2a5 --- /dev/null +++ b/libs/accounts/email-renderer/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "src/renderer/vendor/ejs.js"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], +} diff --git a/libs/accounts/email-renderer/tsconfig.spec.json b/libs/accounts/email-renderer/tsconfig.spec.json new file mode 100644 index 00000000000..40ee4531e03 --- /dev/null +++ b/libs/accounts/email-renderer/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ], +} diff --git a/package.json b/package.json index 604d8a7a415..d4795993061 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "kysely": "^0.27.2", "lint-staged": "^15.2.0", "maxmind": "^4.3.22", + "mjml-browser": "^4.16.1", "module-alias": "^2.2.3", "mozlog": "^3.0.2", "mysql": "^2.18.1", @@ -124,9 +125,11 @@ "node-jose": "^2.2.0", "nps": "^5.10.0", "objection": "^3.1.3", + "os-browserify": "^0.3.0", "passport": "^0.7.0", "passport-http-bearer": "^1.0.1", "passport-jwt": "^4.0.1", + "path-browserify": "^1.0.1", "pm2": "^5.4.2", "prettier": "^3.5.3", "react": "18.3.1", @@ -192,13 +195,13 @@ "@nx/webpack": "21.1.2", "@nx/workspace": "21.1.2", "@opentelemetry/semantic-conventions": "^1.32.0", - "@storybook/addon-essentials": "7.6.15", + "@storybook/addon-essentials": "7.6.17", "@storybook/addon-styling": "1.3.7", - "@storybook/core-common": "7.4.5", - "@storybook/core-server": "7.6.17", - "@storybook/html-webpack5": "7.6.13", - "@storybook/nextjs": "7.6.14", - "@storybook/react-webpack5": "7.5.3", + "@storybook/core-common": "7.6.20", + "@storybook/core-server": "7.6.20", + "@storybook/html-webpack5": "7.6.20", + "@storybook/nextjs": "7.6.20", + "@storybook/react-webpack5": "7.6.20", "@swc-node/register": "1.10.9", "@swc/cli": "0.6.0", "@swc/core": "1.11.11", @@ -211,6 +214,7 @@ "@types/hapi": "^18.0.15", "@types/jest": "29.5.14", "@types/jsdom": "^21", + "@types/mjml-browser": "^4.15.0", "@types/module-alias": "^2", "@types/mysql": "^2", "@types/node": "^22.13.5", @@ -229,6 +233,7 @@ "autoprefixer": "^10.4.14", "babel-eslint": "^10.1.0", "babel-jest": "29.7.0", + "copyfiles": "^2.4.1", "esbuild": "^0.17.15", "esbuild-register": "^3.5.0", "eslint": "^8.57.1", diff --git a/yarn.lock b/yarn.lock index 27a5b5d30ac..06e9b966ef0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16269,35 +16269,35 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-actions@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/addon-actions@npm:7.6.14" +"@storybook/addon-actions@npm:7.6.15": + version: 7.6.15 + resolution: "@storybook/addon-actions@npm:7.6.15" dependencies: - "@storybook/core-events": "npm:7.6.14" + "@storybook/core-events": "npm:7.6.15" "@storybook/global": "npm:^5.0.0" "@types/uuid": "npm:^9.0.1" dequal: "npm:^2.0.2" polished: "npm:^4.2.2" uuid: "npm:^9.0.0" - checksum: 10c0/e00641eaf1ae98c03c9bd384e874c77c5a011a43c7c782f946c117de1a85aaa00191099858916f99b4a6bc07528938a8e5130de4ad68592d4e8e33f2e718a7c3 + checksum: 10c0/392db9e57e95d0914ba5e42c31c6f52623db781ba1fceb1b6399e558ba2ed9ac7b31d02c3583fc68abecc8dba52f9bb21228e9a8d40976cd1b6983ef0c909c67 languageName: node linkType: hard -"@storybook/addon-actions@npm:7.6.15": - version: 7.6.15 - resolution: "@storybook/addon-actions@npm:7.6.15" +"@storybook/addon-actions@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-actions@npm:7.6.17" dependencies: - "@storybook/core-events": "npm:7.6.15" + "@storybook/core-events": "npm:7.6.17" "@storybook/global": "npm:^5.0.0" "@types/uuid": "npm:^9.0.1" dequal: "npm:^2.0.2" polished: "npm:^4.2.2" uuid: "npm:^9.0.0" - checksum: 10c0/392db9e57e95d0914ba5e42c31c6f52623db781ba1fceb1b6399e558ba2ed9ac7b31d02c3583fc68abecc8dba52f9bb21228e9a8d40976cd1b6983ef0c909c67 + checksum: 10c0/91d20a7c35fff6a0b2aa33f2c1171d457c68fb9d955da12629d6f75d931d5aa3756837e413ab7bb928c4cc4b48dcc5cdd63510e6028e7bd8fc8c82d93be967d0 languageName: node linkType: hard -"@storybook/addon-actions@npm:^7.0.0": +"@storybook/addon-actions@npm:7.6.20, @storybook/addon-actions@npm:^7.0.0": version: 7.6.20 resolution: "@storybook/addon-actions@npm:7.6.20" dependencies: @@ -16322,6 +16322,17 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-backgrounds@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-backgrounds@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + memoizerific: "npm:^1.11.3" + ts-dedent: "npm:^2.0.0" + checksum: 10c0/43518d762efa8dd140d029541e8e2bb748173a8428e3de67287ca132525e33e443282a2b06f3b381250d9557ada9ea3a07039aa69cf3de6b04aec02027fb9943 + languageName: node + linkType: hard + "@storybook/addon-controls@npm:7.4.6": version: 7.4.6 resolution: "@storybook/addon-controls@npm:7.4.6" @@ -16361,6 +16372,17 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-controls@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-controls@npm:7.6.17" + dependencies: + "@storybook/blocks": "npm:7.6.17" + lodash: "npm:^4.17.21" + ts-dedent: "npm:^2.0.0" + checksum: 10c0/da66466b801064a916e059ce127efb2ab074a5c80fb65b568ac361d09fe55e0e993cd5400d6b0361bdfd783725e59449bbd30f87643964fa0db8e02a5f9550fd + languageName: node + linkType: hard + "@storybook/addon-docs@npm:7.6.12": version: 7.6.12 resolution: "@storybook/addon-docs@npm:7.6.12" @@ -16421,6 +16443,36 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-docs@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-docs@npm:7.6.17" + dependencies: + "@jest/transform": "npm:^29.3.1" + "@mdx-js/react": "npm:^2.1.5" + "@storybook/blocks": "npm:7.6.17" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/components": "npm:7.6.17" + "@storybook/csf-plugin": "npm:7.6.17" + "@storybook/csf-tools": "npm:7.6.17" + "@storybook/global": "npm:^5.0.0" + "@storybook/mdx2-csf": "npm:^1.0.0" + "@storybook/node-logger": "npm:7.6.17" + "@storybook/postinstall": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + "@storybook/react-dom-shim": "npm:7.6.17" + "@storybook/theming": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + fs-extra: "npm:^11.1.0" + remark-external-links: "npm:^8.0.0" + remark-slug: "npm:^6.0.0" + ts-dedent: "npm:^2.0.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/b43666832f1657f4dfac976ac8b8071995d65860a29f1ac66b80adb69a0d02f0d1d70684d94ddb76f0957f003b94b4252599e19f1e6a4342686598bbb40280ae + languageName: node + linkType: hard + "@storybook/addon-essentials@npm:7.6.15": version: 7.6.15 resolution: "@storybook/addon-essentials@npm:7.6.15" @@ -16446,6 +16498,31 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-essentials@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-essentials@npm:7.6.17" + dependencies: + "@storybook/addon-actions": "npm:7.6.17" + "@storybook/addon-backgrounds": "npm:7.6.17" + "@storybook/addon-controls": "npm:7.6.17" + "@storybook/addon-docs": "npm:7.6.17" + "@storybook/addon-highlight": "npm:7.6.17" + "@storybook/addon-measure": "npm:7.6.17" + "@storybook/addon-outline": "npm:7.6.17" + "@storybook/addon-toolbars": "npm:7.6.17" + "@storybook/addon-viewport": "npm:7.6.17" + "@storybook/core-common": "npm:7.6.17" + "@storybook/manager-api": "npm:7.6.17" + "@storybook/node-logger": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + ts-dedent: "npm:^2.0.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/3208790b219e88fadc634aa00134eb3f0da9d2c05cd84e733d07e201177c58bccb85879ee4c26441a35b1e7fd318111dd668fdd8b3e57b37da512a658d4f50e9 + languageName: node + linkType: hard + "@storybook/addon-highlight@npm:7.6.15": version: 7.6.15 resolution: "@storybook/addon-highlight@npm:7.6.15" @@ -16455,6 +16532,15 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-highlight@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-highlight@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + checksum: 10c0/5f16a648a38257bdd66f592b519cc6b4ecf36c50d0cb01696f1c42c6c9fa2b44b7056b64d611579f2ec4764787b6bd34ea6b9ebddb01b0e562b3eb8100b1cf96 + languageName: node + linkType: hard + "@storybook/addon-interactions@npm:7.6.16": version: 7.6.16 resolution: "@storybook/addon-interactions@npm:7.6.16" @@ -16509,6 +16595,16 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-measure@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-measure@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + tiny-invariant: "npm:^1.3.1" + checksum: 10c0/18c26fd08c6b369ae74cbea4447ae0791efb6968875223b12e84021cf1c7a48496d56c35c6b1de03603081b650c3e4b54530b8704b68467bc667cbf550623ef9 + languageName: node + linkType: hard + "@storybook/addon-outline@npm:7.6.15": version: 7.6.15 resolution: "@storybook/addon-outline@npm:7.6.15" @@ -16519,6 +16615,16 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-outline@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-outline@npm:7.6.17" + dependencies: + "@storybook/global": "npm:^5.0.0" + ts-dedent: "npm:^2.0.0" + checksum: 10c0/840a554504c457e3dee273266ba90a7f36b7488a72644d046f0233c305d7fe3a0773848d104a3dc7d6efafc3e1b41a3fc4d6cdd7a37b3a3fe75a03fcde206efb + languageName: node + linkType: hard + "@storybook/addon-styling@npm:1.3.0": version: 1.3.0 resolution: "@storybook/addon-styling@npm:1.3.0" @@ -16627,6 +16733,13 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-toolbars@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-toolbars@npm:7.6.17" + checksum: 10c0/af4453848c29ab8edb0cf6ca42ff14750841eaf3b523920620e42c27c0f07574a83c0dfe75f6a0de1846178aafb6833d59cef7faa7268777c24ed490da647814 + languageName: node + linkType: hard + "@storybook/addon-viewport@npm:7.6.15": version: 7.6.15 resolution: "@storybook/addon-viewport@npm:7.6.15" @@ -16636,6 +16749,15 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-viewport@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/addon-viewport@npm:7.6.17" + dependencies: + memoizerific: "npm:^1.11.3" + checksum: 10c0/d888954f45ab358189cf0172e1c9b8a1bd2b68aa99d5d6518abe7fc355bbfeb91cc1c21c64e461994f5987652d05944aaa270366e22475eaeccadc701419b0d7 + languageName: node + linkType: hard + "@storybook/addons@npm:7.6.17": version: 7.6.17 resolution: "@storybook/addons@npm:7.6.17" @@ -16792,27 +16914,37 @@ __metadata: languageName: node linkType: hard -"@storybook/builder-manager@npm:7.6.17": +"@storybook/blocks@npm:7.6.17": version: 7.6.17 - resolution: "@storybook/builder-manager@npm:7.6.17" + resolution: "@storybook/blocks@npm:7.6.17" dependencies: - "@fal-works/esbuild-plugin-global-externals": "npm:^2.1.2" - "@storybook/core-common": "npm:7.6.17" - "@storybook/manager": "npm:7.6.17" - "@storybook/node-logger": "npm:7.6.17" - "@types/ejs": "npm:^3.1.1" - "@types/find-cache-dir": "npm:^3.2.1" - "@yarnpkg/esbuild-plugin-pnp": "npm:^3.0.0-rc.10" - browser-assert: "npm:^1.2.1" - ejs: "npm:^3.1.8" - esbuild: "npm:^0.18.0" - esbuild-plugin-alias: "npm:^0.2.1" - express: "npm:^4.17.3" - find-cache-dir: "npm:^3.0.0" - fs-extra: "npm:^11.1.0" - process: "npm:^0.11.10" - util: "npm:^0.12.4" - checksum: 10c0/1b2ca77f7f3bf3c72890e949cfadc45d633fee7315ebcabfc1d6e23cd259db93114cbd9b9197597057f90c5fd60b3e72b0782a284a4f80c6efdd15f118b2c594 + "@storybook/channels": "npm:7.6.17" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/components": "npm:7.6.17" + "@storybook/core-events": "npm:7.6.17" + "@storybook/csf": "npm:^0.1.2" + "@storybook/docs-tools": "npm:7.6.17" + "@storybook/global": "npm:^5.0.0" + "@storybook/manager-api": "npm:7.6.17" + "@storybook/preview-api": "npm:7.6.17" + "@storybook/theming": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + "@types/lodash": "npm:^4.14.167" + color-convert: "npm:^2.0.1" + dequal: "npm:^2.0.2" + lodash: "npm:^4.17.21" + markdown-to-jsx: "npm:^7.1.8" + memoizerific: "npm:^1.11.3" + polished: "npm:^4.2.2" + react-colorful: "npm:^5.1.2" + telejson: "npm:^7.2.0" + tocbot: "npm:^4.20.1" + ts-dedent: "npm:^2.0.0" + util-deprecate: "npm:^1.0.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/f38233c935679345b4893d3d75b38ca8e74f3749b1f42a2356b61754bf1886cde8565546cdf53217335c8318506c56954aee7cc23c627b06f2d8c3b842d5d12b languageName: node linkType: hard @@ -16887,68 +17019,19 @@ __metadata: languageName: node linkType: hard -"@storybook/builder-webpack5@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/builder-webpack5@npm:7.6.13" - dependencies: - "@babel/core": "npm:^7.23.2" - "@storybook/channels": "npm:7.6.13" - "@storybook/client-logger": "npm:7.6.13" - "@storybook/core-common": "npm:7.6.13" - "@storybook/core-events": "npm:7.6.13" - "@storybook/core-webpack": "npm:7.6.13" - "@storybook/node-logger": "npm:7.6.13" - "@storybook/preview": "npm:7.6.13" - "@storybook/preview-api": "npm:7.6.13" - "@swc/core": "npm:^1.3.82" - "@types/node": "npm:^18.0.0" - "@types/semver": "npm:^7.3.4" - babel-loader: "npm:^9.0.0" - browser-assert: "npm:^1.2.1" - case-sensitive-paths-webpack-plugin: "npm:^2.4.0" - cjs-module-lexer: "npm:^1.2.3" - constants-browserify: "npm:^1.0.0" - css-loader: "npm:^6.7.1" - es-module-lexer: "npm:^1.4.1" - express: "npm:^4.17.3" - fork-ts-checker-webpack-plugin: "npm:^8.0.0" - fs-extra: "npm:^11.1.0" - html-webpack-plugin: "npm:^5.5.0" - magic-string: "npm:^0.30.5" - path-browserify: "npm:^1.0.1" - process: "npm:^0.11.10" - semver: "npm:^7.3.7" - style-loader: "npm:^3.3.1" - swc-loader: "npm:^0.2.3" - terser-webpack-plugin: "npm:^5.3.1" - ts-dedent: "npm:^2.0.0" - url: "npm:^0.11.0" - util: "npm:^0.12.4" - util-deprecate: "npm:^1.0.2" - webpack: "npm:5" - webpack-dev-middleware: "npm:^6.1.1" - webpack-hot-middleware: "npm:^2.25.1" - webpack-virtual-modules: "npm:^0.5.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/51e6e5df990482f6c84ab6834c685ad86ccfc955c94b433d66b436ab1a030869848569d9453f29b2c2baaabacc9fbfec8c2c9f41076b2f6641f65a946269e857 - languageName: node - linkType: hard - -"@storybook/builder-webpack5@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/builder-webpack5@npm:7.6.14" +"@storybook/builder-webpack5@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/builder-webpack5@npm:7.6.20" dependencies: "@babel/core": "npm:^7.23.2" - "@storybook/channels": "npm:7.6.14" - "@storybook/client-logger": "npm:7.6.14" - "@storybook/core-common": "npm:7.6.14" - "@storybook/core-events": "npm:7.6.14" - "@storybook/core-webpack": "npm:7.6.14" - "@storybook/node-logger": "npm:7.6.14" - "@storybook/preview": "npm:7.6.14" - "@storybook/preview-api": "npm:7.6.14" + "@storybook/channels": "npm:7.6.20" + "@storybook/client-logger": "npm:7.6.20" + "@storybook/core-common": "npm:7.6.20" + "@storybook/core-events": "npm:7.6.20" + "@storybook/core-webpack": "npm:7.6.20" + "@storybook/node-logger": "npm:7.6.20" + "@storybook/preview": "npm:7.6.20" + "@storybook/preview-api": "npm:7.6.20" "@swc/core": "npm:^1.3.82" "@types/node": "npm:^18.0.0" "@types/semver": "npm:^7.3.4" @@ -16981,7 +17064,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/d96cf38501c6aa8761593dfd07c81258d5c4c97d16f05fb5e46185e1f837295b1a322282847f96a73d41616afaf98e4432548c0346371fb22e89d3f386dce84d + checksum: 10c0/b592bf0e1a95f9bf61ee9124d329fcdc1275168bbce27675b2688637d0e88b4ea328188f77015556780c045f743f359aa2d4dd5490fa4c1ececad75f2d3f14aa languageName: node linkType: hard @@ -17052,20 +17135,6 @@ __metadata: languageName: node linkType: hard -"@storybook/channels@npm:7.4.5": - version: 7.4.5 - resolution: "@storybook/channels@npm:7.4.5" - dependencies: - "@storybook/client-logger": "npm:7.4.5" - "@storybook/core-events": "npm:7.4.5" - "@storybook/global": "npm:^5.0.0" - qs: "npm:^6.10.0" - telejson: "npm:^7.2.0" - tiny-invariant: "npm:^1.3.1" - checksum: 10c0/d6da39c420dfe001e8a2929b79e0bc2d7683fcc74cd24b95df56067b4da76e02a2d174319587aefbb294da0678823eae20bff5b9595001557c3a24f41c323dac - languageName: node - linkType: hard - "@storybook/channels@npm:7.4.6": version: 7.4.6 resolution: "@storybook/channels@npm:7.4.6" @@ -17108,34 +17177,6 @@ __metadata: languageName: node linkType: hard -"@storybook/channels@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/channels@npm:7.6.13" - dependencies: - "@storybook/client-logger": "npm:7.6.13" - "@storybook/core-events": "npm:7.6.13" - "@storybook/global": "npm:^5.0.0" - qs: "npm:^6.10.0" - telejson: "npm:^7.2.0" - tiny-invariant: "npm:^1.3.1" - checksum: 10c0/522c1f484ba9775dd85cb8428fd5d4ee580a40153e7aca2d24f8b47f29592a39ae5bf5fe37022c82afa67ee0a8ed39440a4197f40d113360b6b5f01f2e7996ce - languageName: node - linkType: hard - -"@storybook/channels@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/channels@npm:7.6.14" - dependencies: - "@storybook/client-logger": "npm:7.6.14" - "@storybook/core-events": "npm:7.6.14" - "@storybook/global": "npm:^5.0.0" - qs: "npm:^6.10.0" - telejson: "npm:^7.2.0" - tiny-invariant: "npm:^1.3.1" - checksum: 10c0/159984c0d7427d03f5e1c4812ebfadb1de0bd36796afd6d8450bb57baa074c85968774843fce10cbde81e0d61fbb3c0c91f33ff7c3351dc4c4aeefbbfe95bb8a - languageName: node - linkType: hard - "@storybook/channels@npm:7.6.15": version: 7.6.15 resolution: "@storybook/channels@npm:7.6.15" @@ -17284,15 +17325,6 @@ __metadata: languageName: node linkType: hard -"@storybook/client-logger@npm:7.4.5": - version: 7.4.5 - resolution: "@storybook/client-logger@npm:7.4.5" - dependencies: - "@storybook/global": "npm:^5.0.0" - checksum: 10c0/cd5e70d7dcfa1fdb3b3beada1a2342a091959ec6a99c03805749bc3c0ad31d8f2d2ba62ec00ed18b8bb2a1ab9623a42e9fe47d97b9cc5df51bebbbe0f11aedeb - languageName: node - linkType: hard - "@storybook/client-logger@npm:7.4.6": version: 7.4.6 resolution: "@storybook/client-logger@npm:7.4.6" @@ -17320,24 +17352,6 @@ __metadata: languageName: node linkType: hard -"@storybook/client-logger@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/client-logger@npm:7.6.13" - dependencies: - "@storybook/global": "npm:^5.0.0" - checksum: 10c0/eff367f28ff4b6aff62fcf3978447addb6655409eabe3ed3091f2416c1a40998f0971fd5af2c69764e26ea9abc9ba797f63a27e45bc689e95e7a59c251bd4d0c - languageName: node - linkType: hard - -"@storybook/client-logger@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/client-logger@npm:7.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - checksum: 10c0/2fb2cb63d5e781a02e2a74fdd85b1cef931d08dd73b8061bbeaa5bc055fe20de87b64486cca2e780c0e206bfe3a3266c300e411fc021a1683f68297afcc20eaa - languageName: node - linkType: hard - "@storybook/client-logger@npm:7.6.15": version: 7.6.15 resolution: "@storybook/client-logger@npm:7.6.15" @@ -17506,6 +17520,27 @@ __metadata: languageName: node linkType: hard +"@storybook/components@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/components@npm:7.6.17" + dependencies: + "@radix-ui/react-select": "npm:^1.2.2" + "@radix-ui/react-toolbar": "npm:^1.0.4" + "@storybook/client-logger": "npm:7.6.17" + "@storybook/csf": "npm:^0.1.2" + "@storybook/global": "npm:^5.0.0" + "@storybook/theming": "npm:7.6.17" + "@storybook/types": "npm:7.6.17" + memoizerific: "npm:^1.11.3" + use-resize-observer: "npm:^9.1.0" + util-deprecate: "npm:^1.0.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/199421d7668a3afcce9375c567443704778b4288bed16a39f02e5c1aaa9892b4ffba829b47d5a3fa8328521f6e0c26e5e7e7beed898cc0f8f835a99ec8f125a6 + languageName: node + linkType: hard + "@storybook/components@npm:^7.0.12": version: 7.6.20 resolution: "@storybook/components@npm:7.6.20" @@ -17547,26 +17582,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core-client@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/core-client@npm:7.6.13" - dependencies: - "@storybook/client-logger": "npm:7.6.13" - "@storybook/preview-api": "npm:7.6.13" - checksum: 10c0/20a5135c29a5ce587e3829e20336ece1c81b6341afb854a6dcdf8f98824f755899ee594f143dac5c8ecda6be93ce037dd3170330f143caac0bed20bceb938b75 - languageName: node - linkType: hard - -"@storybook/core-client@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/core-client@npm:7.6.14" - dependencies: - "@storybook/client-logger": "npm:7.6.14" - "@storybook/preview-api": "npm:7.6.14" - checksum: 10c0/e068550a3a37c50f1f61ac6418d4a656c3f388d5d067fa7a7487461df9f73646534a505616e70bb84d9eebef23c1897abe9b8c4e875641c00e75be17685ea84e - languageName: node - linkType: hard - "@storybook/core-client@npm:7.6.17": version: 7.6.17 resolution: "@storybook/core-client@npm:7.6.17" @@ -17577,6 +17592,16 @@ __metadata: languageName: node linkType: hard +"@storybook/core-client@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/core-client@npm:7.6.20" + dependencies: + "@storybook/client-logger": "npm:7.6.20" + "@storybook/preview-api": "npm:7.6.20" + checksum: 10c0/cce90a3dfb89e088f1b97aa238a7a6dbfffbc27c8e5298276ccc08d87f931bca20406ad3bcbd56045f161d2493c11b7ee9754e2b86503825d25df61826bcda83 + languageName: node + linkType: hard + "@storybook/core-common@npm:7.0.24": version: 7.0.24 resolution: "@storybook/core-common@npm:7.0.24" @@ -17636,37 +17661,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core-common@npm:7.4.5": - version: 7.4.5 - resolution: "@storybook/core-common@npm:7.4.5" - dependencies: - "@storybook/core-events": "npm:7.4.5" - "@storybook/node-logger": "npm:7.4.5" - "@storybook/types": "npm:7.4.5" - "@types/find-cache-dir": "npm:^3.2.1" - "@types/node": "npm:^16.0.0" - "@types/node-fetch": "npm:^2.6.4" - "@types/pretty-hrtime": "npm:^1.0.0" - chalk: "npm:^4.1.0" - esbuild: "npm:^0.18.0" - esbuild-register: "npm:^3.4.0" - file-system-cache: "npm:2.3.0" - find-cache-dir: "npm:^3.0.0" - find-up: "npm:^5.0.0" - fs-extra: "npm:^11.1.0" - glob: "npm:^10.0.0" - handlebars: "npm:^4.7.7" - lazy-universal-dotenv: "npm:^4.0.0" - node-fetch: "npm:^2.0.0" - picomatch: "npm:^2.3.0" - pkg-dir: "npm:^5.0.0" - pretty-hrtime: "npm:^1.0.3" - resolve-from: "npm:^5.0.0" - ts-dedent: "npm:^2.0.0" - checksum: 10c0/4b0324368480f2d45cbc633e35d185074306e95de0aecc3a227e13f9145da11b1193c54692c2550e8342badc290affa51dc99a1f3e0b42dc9b4f8d06ab13e2d9 - languageName: node - linkType: hard - "@storybook/core-common@npm:7.4.6": version: 7.4.6 resolution: "@storybook/core-common@npm:7.4.6" @@ -17760,68 +17754,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core-common@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/core-common@npm:7.6.13" - dependencies: - "@storybook/core-events": "npm:7.6.13" - "@storybook/node-logger": "npm:7.6.13" - "@storybook/types": "npm:7.6.13" - "@types/find-cache-dir": "npm:^3.2.1" - "@types/node": "npm:^18.0.0" - "@types/node-fetch": "npm:^2.6.4" - "@types/pretty-hrtime": "npm:^1.0.0" - chalk: "npm:^4.1.0" - esbuild: "npm:^0.18.0" - esbuild-register: "npm:^3.5.0" - file-system-cache: "npm:2.3.0" - find-cache-dir: "npm:^3.0.0" - find-up: "npm:^5.0.0" - fs-extra: "npm:^11.1.0" - glob: "npm:^10.0.0" - handlebars: "npm:^4.7.7" - lazy-universal-dotenv: "npm:^4.0.0" - node-fetch: "npm:^2.0.0" - picomatch: "npm:^2.3.0" - pkg-dir: "npm:^5.0.0" - pretty-hrtime: "npm:^1.0.3" - resolve-from: "npm:^5.0.0" - ts-dedent: "npm:^2.0.0" - checksum: 10c0/0ba1f5822ced53c600b7ce3ff607371c0df7d3d5702caf1ed7cd422c559d86327f633983c699affcbe4fed1af44ada97e7eac7ccc85842102d066f47cfb08e87 - languageName: node - linkType: hard - -"@storybook/core-common@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/core-common@npm:7.6.14" - dependencies: - "@storybook/core-events": "npm:7.6.14" - "@storybook/node-logger": "npm:7.6.14" - "@storybook/types": "npm:7.6.14" - "@types/find-cache-dir": "npm:^3.2.1" - "@types/node": "npm:^18.0.0" - "@types/node-fetch": "npm:^2.6.4" - "@types/pretty-hrtime": "npm:^1.0.0" - chalk: "npm:^4.1.0" - esbuild: "npm:^0.18.0" - esbuild-register: "npm:^3.5.0" - file-system-cache: "npm:2.3.0" - find-cache-dir: "npm:^3.0.0" - find-up: "npm:^5.0.0" - fs-extra: "npm:^11.1.0" - glob: "npm:^10.0.0" - handlebars: "npm:^4.7.7" - lazy-universal-dotenv: "npm:^4.0.0" - node-fetch: "npm:^2.0.0" - picomatch: "npm:^2.3.0" - pkg-dir: "npm:^5.0.0" - pretty-hrtime: "npm:^1.0.3" - resolve-from: "npm:^5.0.0" - ts-dedent: "npm:^2.0.0" - checksum: 10c0/250c36206486e12408b025b8fb0f34a94842848f4a56f622fec7c6afbace9110962ec0d1c9386930d0da32e483adf809ab78fe86e44ba83ab121c1ecae7f6073 - languageName: node - linkType: hard - "@storybook/core-common@npm:7.6.15": version: 7.6.15 resolution: "@storybook/core-common@npm:7.6.15" @@ -17936,15 +17868,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core-events@npm:7.4.5": - version: 7.4.5 - resolution: "@storybook/core-events@npm:7.4.5" - dependencies: - ts-dedent: "npm:^2.0.0" - checksum: 10c0/d5d331ac85198e4b3c2c41e2ef5b36069638b2bfba83b6c5fc5263a06745c31c6f33b5a5e38ae8789f44ddb27834f8248758d813cb6aa6802bc9e18c13c2a3c7 - languageName: node - linkType: hard - "@storybook/core-events@npm:7.4.6": version: 7.4.6 resolution: "@storybook/core-events@npm:7.4.6" @@ -17972,24 +17895,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core-events@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/core-events@npm:7.6.13" - dependencies: - ts-dedent: "npm:^2.0.0" - checksum: 10c0/56f8101d8999053207c5abc223ec40ac6de62b3c75947971e7524c2ee3fcf4a6a625fa1ea4c99e6b2b111a6631383a2b6365753c8e937473621caa4ae87158ad - languageName: node - linkType: hard - -"@storybook/core-events@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/core-events@npm:7.6.14" - dependencies: - ts-dedent: "npm:^2.0.0" - checksum: 10c0/1afc2b7576aee76650fb79cf9ba93db0ad1df585142ec4c50b237e88f6bfe0e8c7d72a9c518c3e217ecf844a3a173387213b9876b646195891dcd2017a1fa059 - languageName: node - linkType: hard - "@storybook/core-events@npm:7.6.15": version: 7.6.15 resolution: "@storybook/core-events@npm:7.6.15" @@ -18035,55 +17940,6 @@ __metadata: languageName: node linkType: hard -"@storybook/core-server@npm:7.6.17": - version: 7.6.17 - resolution: "@storybook/core-server@npm:7.6.17" - dependencies: - "@aw-web-design/x-default-browser": "npm:1.4.126" - "@discoveryjs/json-ext": "npm:^0.5.3" - "@storybook/builder-manager": "npm:7.6.17" - "@storybook/channels": "npm:7.6.17" - "@storybook/core-common": "npm:7.6.17" - "@storybook/core-events": "npm:7.6.17" - "@storybook/csf": "npm:^0.1.2" - "@storybook/csf-tools": "npm:7.6.17" - "@storybook/docs-mdx": "npm:^0.1.0" - "@storybook/global": "npm:^5.0.0" - "@storybook/manager": "npm:7.6.17" - "@storybook/node-logger": "npm:7.6.17" - "@storybook/preview-api": "npm:7.6.17" - "@storybook/telemetry": "npm:7.6.17" - "@storybook/types": "npm:7.6.17" - "@types/detect-port": "npm:^1.3.0" - "@types/node": "npm:^18.0.0" - "@types/pretty-hrtime": "npm:^1.0.0" - "@types/semver": "npm:^7.3.4" - better-opn: "npm:^3.0.2" - chalk: "npm:^4.1.0" - cli-table3: "npm:^0.6.1" - compression: "npm:^1.7.4" - detect-port: "npm:^1.3.0" - express: "npm:^4.17.3" - fs-extra: "npm:^11.1.0" - globby: "npm:^11.0.2" - ip: "npm:^2.0.1" - lodash: "npm:^4.17.21" - open: "npm:^8.4.0" - pretty-hrtime: "npm:^1.0.3" - prompts: "npm:^2.4.0" - read-pkg-up: "npm:^7.0.1" - semver: "npm:^7.3.7" - telejson: "npm:^7.2.0" - tiny-invariant: "npm:^1.3.1" - ts-dedent: "npm:^2.0.0" - util: "npm:^0.12.4" - util-deprecate: "npm:^1.0.2" - watchpack: "npm:^2.2.0" - ws: "npm:^8.2.3" - checksum: 10c0/b56077bea18c22151adb72c96efb1717034314b08bba5cae12b1f8a0e4135773f5c1e334ad3523dfeb578078b2d41a6091e2b0a992a110ca1859fdd89b1a4702 - languageName: node - linkType: hard - "@storybook/core-server@npm:7.6.20": version: 7.6.20 resolution: "@storybook/core-server@npm:7.6.20" @@ -18145,29 +18001,16 @@ __metadata: languageName: node linkType: hard -"@storybook/core-webpack@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/core-webpack@npm:7.6.13" - dependencies: - "@storybook/core-common": "npm:7.6.13" - "@storybook/node-logger": "npm:7.6.13" - "@storybook/types": "npm:7.6.13" - "@types/node": "npm:^18.0.0" - ts-dedent: "npm:^2.0.0" - checksum: 10c0/a8443c1d0998167a5bdb5a20c5570c96fb5f81d815650e6456f242e6a68237f82bb28c70fd2a5537542d38e39e362bed9e2c0899376f8cfcd48234a3b552d028 - languageName: node - linkType: hard - -"@storybook/core-webpack@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/core-webpack@npm:7.6.14" +"@storybook/core-webpack@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/core-webpack@npm:7.6.20" dependencies: - "@storybook/core-common": "npm:7.6.14" - "@storybook/node-logger": "npm:7.6.14" - "@storybook/types": "npm:7.6.14" + "@storybook/core-common": "npm:7.6.20" + "@storybook/node-logger": "npm:7.6.20" + "@storybook/types": "npm:7.6.20" "@types/node": "npm:^18.0.0" ts-dedent: "npm:^2.0.0" - checksum: 10c0/08835a87ae28095ad34634c7e9de0c8cf9acfdd327e56288ca0232cb5d39682329ccbc52108d0a566fa39f47b3b5ec36ce0ce86945107a5fe4b69d2ed04323da + checksum: 10c0/b2ac0de767eace222c6e83bc19be0e7aba451767a43d97163f888b65a5288a8499fb5e9b50e039d98bbeac5923d7c54c096d08681cdd3cf52fed936a343d5cf4 languageName: node linkType: hard @@ -18191,6 +18034,16 @@ __metadata: languageName: node linkType: hard +"@storybook/csf-plugin@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/csf-plugin@npm:7.6.17" + dependencies: + "@storybook/csf-tools": "npm:7.6.17" + unplugin: "npm:^1.3.1" + checksum: 10c0/720ecbd2e845f6d6d575b8fb5b05a085ddba1eb486318a9b7d6f2ea6646fe3e62d7c9589e18aab15ce0a715c653c9d24b2e0f38117e92845e636f0410a85f76d + languageName: node + linkType: hard + "@storybook/csf-tools@npm:7.6.12": version: 7.6.12 resolution: "@storybook/csf-tools@npm:7.6.12" @@ -18347,36 +18200,6 @@ __metadata: languageName: node linkType: hard -"@storybook/docs-tools@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/docs-tools@npm:7.6.13" - dependencies: - "@storybook/core-common": "npm:7.6.13" - "@storybook/preview-api": "npm:7.6.13" - "@storybook/types": "npm:7.6.13" - "@types/doctrine": "npm:^0.0.3" - assert: "npm:^2.1.0" - doctrine: "npm:^3.0.0" - lodash: "npm:^4.17.21" - checksum: 10c0/e9f6ae68907729107a369344a9ebe38cd8d6e98e58ef05d233a987d6b32785a60d1123bb0b145cc881ff350fe29b531ec6f5ca6ffb00b2f0c0dc4d1b289a9dd4 - languageName: node - linkType: hard - -"@storybook/docs-tools@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/docs-tools@npm:7.6.14" - dependencies: - "@storybook/core-common": "npm:7.6.14" - "@storybook/preview-api": "npm:7.6.14" - "@storybook/types": "npm:7.6.14" - "@types/doctrine": "npm:^0.0.3" - assert: "npm:^2.1.0" - doctrine: "npm:^3.0.0" - lodash: "npm:^4.17.21" - checksum: 10c0/c38325ec89498723080e9087b2ceb821d85bf2364baf2ccf41ae6e0ebe08f4ae5d9da1eb55f231b125631747f901f0149628e618ba08a6a0ebb031434590c6ef - languageName: node - linkType: hard - "@storybook/docs-tools@npm:7.6.15": version: 7.6.15 resolution: "@storybook/docs-tools@npm:7.6.15" @@ -18407,6 +18230,21 @@ __metadata: languageName: node linkType: hard +"@storybook/docs-tools@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/docs-tools@npm:7.6.20" + dependencies: + "@storybook/core-common": "npm:7.6.20" + "@storybook/preview-api": "npm:7.6.20" + "@storybook/types": "npm:7.6.20" + "@types/doctrine": "npm:^0.0.3" + assert: "npm:^2.1.0" + doctrine: "npm:^3.0.0" + lodash: "npm:^4.17.21" + checksum: 10c0/4a20296f6ac8b426d6050addaa6cc5b4d20fe2a5a4895641e7bdbe82c1048e14add35f61e72c0fc4f20e67d84dfc420d4b0af38ad5688a51604bb8dddba4fa81 + languageName: node + linkType: hard + "@storybook/global@npm:^5.0.0": version: 5.0.0 resolution: "@storybook/global@npm:5.0.0" @@ -18432,19 +18270,19 @@ __metadata: languageName: node linkType: hard -"@storybook/html-webpack5@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/html-webpack5@npm:7.6.13" +"@storybook/html-webpack5@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/html-webpack5@npm:7.6.20" dependencies: - "@storybook/builder-webpack5": "npm:7.6.13" - "@storybook/core-common": "npm:7.6.13" + "@storybook/builder-webpack5": "npm:7.6.20" + "@storybook/core-common": "npm:7.6.20" "@storybook/global": "npm:^5.0.0" - "@storybook/html": "npm:7.6.13" - "@storybook/preset-html-webpack": "npm:7.6.13" + "@storybook/html": "npm:7.6.20" + "@storybook/preset-html-webpack": "npm:7.6.20" "@types/node": "npm:^18.0.0" peerDependencies: "@babel/core": "*" - checksum: 10c0/f6540bfc1f6521f4a945077e260c86170e07e73d6aac349f95f1f8c6a5582e2528b7a64924f56607b422804d4c210cd6fd5e2de5e6165b4a5cabf594f2014728 + checksum: 10c0/6ca2cc410f827b3c2519deb376bdd98e5d64e568903e1b99f48b4e16f766b3a13126e253859cde92d94eab8be685fdfa341fc3e17eab8a10f628e19d5ac4a3af languageName: node linkType: hard @@ -18464,22 +18302,6 @@ __metadata: languageName: node linkType: hard -"@storybook/html@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/html@npm:7.6.13" - dependencies: - "@storybook/core-client": "npm:7.6.13" - "@storybook/docs-tools": "npm:7.6.13" - "@storybook/global": "npm:^5.0.0" - "@storybook/preview-api": "npm:7.6.13" - "@storybook/types": "npm:7.6.13" - ts-dedent: "npm:^2.0.0" - peerDependencies: - "@babel/core": "*" - checksum: 10c0/fe90f67fa0465effb0a8e2b19ce46ed66743c5b11d9205aa3b6a47767e903909acd3ee62ee58c7a2f59ca389fd96b8a1a539ca11d3050e4f48e9bc91e0a5d7a0 - languageName: node - linkType: hard - "@storybook/html@npm:7.6.17": version: 7.6.17 resolution: "@storybook/html@npm:7.6.17" @@ -18496,6 +18318,22 @@ __metadata: languageName: node linkType: hard +"@storybook/html@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/html@npm:7.6.20" + dependencies: + "@storybook/core-client": "npm:7.6.20" + "@storybook/docs-tools": "npm:7.6.20" + "@storybook/global": "npm:^5.0.0" + "@storybook/preview-api": "npm:7.6.20" + "@storybook/types": "npm:7.6.20" + ts-dedent: "npm:^2.0.0" + peerDependencies: + "@babel/core": "*" + checksum: 10c0/10eca4ae35965bf8a655c33c587ab1a7cea83de3c63a2e85dea1d3a220651e690c21ab48d570230b4270ed6317aed0719a9bf337539e2a0ac453a1cea66cafee + languageName: node + linkType: hard + "@storybook/manager-api@npm:7.0.23": version: 7.0.23 resolution: "@storybook/manager-api@npm:7.0.23" @@ -18662,13 +18500,6 @@ __metadata: languageName: node linkType: hard -"@storybook/manager@npm:7.6.17": - version: 7.6.17 - resolution: "@storybook/manager@npm:7.6.17" - checksum: 10c0/e703466e95b0fca58963ac0abec188164e6bce904471171dd360c0d63ead0183a5b242db034af63157acd42d38348984e5fe4e6414af6190234c4d5d41608cee - languageName: node - linkType: hard - "@storybook/manager@npm:7.6.20": version: 7.6.20 resolution: "@storybook/manager@npm:7.6.20" @@ -18683,9 +18514,9 @@ __metadata: languageName: node linkType: hard -"@storybook/nextjs@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/nextjs@npm:7.6.14" +"@storybook/nextjs@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/nextjs@npm:7.6.20" dependencies: "@babel/core": "npm:^7.23.2" "@babel/plugin-syntax-bigint": "npm:^7.8.3" @@ -18700,14 +18531,14 @@ __metadata: "@babel/preset-react": "npm:^7.22.15" "@babel/preset-typescript": "npm:^7.23.2" "@babel/runtime": "npm:^7.23.2" - "@storybook/addon-actions": "npm:7.6.14" - "@storybook/builder-webpack5": "npm:7.6.14" - "@storybook/core-common": "npm:7.6.14" - "@storybook/core-events": "npm:7.6.14" - "@storybook/node-logger": "npm:7.6.14" - "@storybook/preset-react-webpack": "npm:7.6.14" - "@storybook/preview-api": "npm:7.6.14" - "@storybook/react": "npm:7.6.14" + "@storybook/addon-actions": "npm:7.6.20" + "@storybook/builder-webpack5": "npm:7.6.20" + "@storybook/core-common": "npm:7.6.20" + "@storybook/core-events": "npm:7.6.20" + "@storybook/node-logger": "npm:7.6.20" + "@storybook/preset-react-webpack": "npm:7.6.20" + "@storybook/preview-api": "npm:7.6.20" + "@storybook/react": "npm:7.6.20" "@types/node": "npm:^18.0.0" "@types/semver": "npm:^7.3.4" css-loader: "npm:^6.7.3" @@ -18741,7 +18572,7 @@ __metadata: optional: true webpack: optional: true - checksum: 10c0/db7608801e894b2d5a028dcb803d0faff045435561da2c2a77feec379ef00f3dfd49d7b49c07d7539b070552fad8c75de00d46f0f1be5c1204ce662e76ccc317 + checksum: 10c0/ec9eec63a467a07060f55b645d586172efd62d37d68c2c02ef448945d63cc205dfba3937b581e2392081671faaf0620bd6565c6ca238dbd69bb3c1e506736a27 languageName: node linkType: hard @@ -18764,13 +18595,6 @@ __metadata: languageName: node linkType: hard -"@storybook/node-logger@npm:7.4.5": - version: 7.4.5 - resolution: "@storybook/node-logger@npm:7.4.5" - checksum: 10c0/e4a9017ab837439d0b32343f1cb8437838ba884668d91c82cc5f681f1e8c49bba4332d2590e8a04ce83e0e5aac06206f3b670ebc9bc1e38f742deccd2ac02020 - languageName: node - linkType: hard - "@storybook/node-logger@npm:7.4.6": version: 7.4.6 resolution: "@storybook/node-logger@npm:7.4.6" @@ -18792,20 +18616,6 @@ __metadata: languageName: node linkType: hard -"@storybook/node-logger@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/node-logger@npm:7.6.13" - checksum: 10c0/e9d0f93e6ae2977e194c3be040ba8006b7c28a3cc0a607b2f657aa285f0e8f21586efb5f6a0d589b62f37684520ac348e758017cbbb27fe74189acad36fcb5ab - languageName: node - linkType: hard - -"@storybook/node-logger@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/node-logger@npm:7.6.14" - checksum: 10c0/c5df8429038cc1b20dace8101ce112f05b35f30f3c7393e1c7c3bcb9911e48ed6a58ea303d3722071fe1cb58d031984251eeece64e605bd1c9ea356fb36f229c - languageName: node - linkType: hard - "@storybook/node-logger@npm:7.6.15": version: 7.6.15 resolution: "@storybook/node-logger@npm:7.6.15" @@ -18841,6 +18651,13 @@ __metadata: languageName: node linkType: hard +"@storybook/postinstall@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/postinstall@npm:7.6.17" + checksum: 10c0/62038e1feacfa5b9acc85afd1cdcbee3c9d780c8dbb6d2eb8cf7bfbb6a14d989fa61351958f512415761d5190075367f1f3641e104c0cec0a2c8dd056617dea6 + languageName: node + linkType: hard + "@storybook/preset-create-react-app@npm:7.6.4": version: 7.6.4 resolution: "@storybook/preset-create-react-app@npm:7.6.4" @@ -18872,17 +18689,17 @@ __metadata: languageName: node linkType: hard -"@storybook/preset-html-webpack@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/preset-html-webpack@npm:7.6.13" +"@storybook/preset-html-webpack@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/preset-html-webpack@npm:7.6.20" dependencies: - "@storybook/core-webpack": "npm:7.6.13" + "@storybook/core-webpack": "npm:7.6.20" "@types/node": "npm:^18.0.0" html-loader: "npm:^3.1.0" webpack: "npm:5" peerDependencies: "@babel/core": "*" - checksum: 10c0/19c25823cae3003ca29dcdaf3fab3d11c730cfd2f04d27436da69ead7ac029e758003e096288866dbff62795b2f329fa2cb871042a7514750c0df105fc06d994 + checksum: 10c0/bbb4fff3e6fc886aa4bde228f4ba02db3b1d7bf70dd76ec3eeb7e277e725c5c379044cf4934bc49688adbdf1f88ef649cea6fb5db347a04a6cae67f77271c2af languageName: node linkType: hard @@ -18919,17 +18736,17 @@ __metadata: languageName: node linkType: hard -"@storybook/preset-react-webpack@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/preset-react-webpack@npm:7.6.14" +"@storybook/preset-react-webpack@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/preset-react-webpack@npm:7.6.20" dependencies: "@babel/preset-flow": "npm:^7.22.15" "@babel/preset-react": "npm:^7.22.15" "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.11" - "@storybook/core-webpack": "npm:7.6.14" - "@storybook/docs-tools": "npm:7.6.14" - "@storybook/node-logger": "npm:7.6.14" - "@storybook/react": "npm:7.6.14" + "@storybook/core-webpack": "npm:7.6.20" + "@storybook/docs-tools": "npm:7.6.20" + "@storybook/node-logger": "npm:7.6.20" + "@storybook/react": "npm:7.6.20" "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" "@types/node": "npm:^18.0.0" "@types/semver": "npm:^7.3.4" @@ -18949,7 +18766,7 @@ __metadata: optional: true typescript: optional: true - checksum: 10c0/39be5d3b64d895d04a8ab77d8285dab8b0366185389cd1dbd6c0a7b617c51bcafcb9f7688a73a503a11d0ea112d39a08c850c6dc6d6a16278e33222becc12dc4 + checksum: 10c0/c212cc4ff22cc817dcdb704cafa23cc5e41fae936e6fb7ba8d50887f5445b498e4696de17333f5e403e110cd127e0118f0acf1a46c578b0067a6f138149a9ce5 languageName: node linkType: hard @@ -19088,50 +18905,6 @@ __metadata: languageName: node linkType: hard -"@storybook/preview-api@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/preview-api@npm:7.6.13" - dependencies: - "@storybook/channels": "npm:7.6.13" - "@storybook/client-logger": "npm:7.6.13" - "@storybook/core-events": "npm:7.6.13" - "@storybook/csf": "npm:^0.1.2" - "@storybook/global": "npm:^5.0.0" - "@storybook/types": "npm:7.6.13" - "@types/qs": "npm:^6.9.5" - dequal: "npm:^2.0.2" - lodash: "npm:^4.17.21" - memoizerific: "npm:^1.11.3" - qs: "npm:^6.10.0" - synchronous-promise: "npm:^2.0.15" - ts-dedent: "npm:^2.0.0" - util-deprecate: "npm:^1.0.2" - checksum: 10c0/452c88ced0470a0c2f50df6df05ddd0188c0974f4edd9d63b5f76e26700cf8108dce0ff61b141aea96430ba5b4f07eb6b0dee3834632758742b01021c2546259 - languageName: node - linkType: hard - -"@storybook/preview-api@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/preview-api@npm:7.6.14" - dependencies: - "@storybook/channels": "npm:7.6.14" - "@storybook/client-logger": "npm:7.6.14" - "@storybook/core-events": "npm:7.6.14" - "@storybook/csf": "npm:^0.1.2" - "@storybook/global": "npm:^5.0.0" - "@storybook/types": "npm:7.6.14" - "@types/qs": "npm:^6.9.5" - dequal: "npm:^2.0.2" - lodash: "npm:^4.17.21" - memoizerific: "npm:^1.11.3" - qs: "npm:^6.10.0" - synchronous-promise: "npm:^2.0.15" - ts-dedent: "npm:^2.0.0" - util-deprecate: "npm:^1.0.2" - checksum: 10c0/508a5426fc5a825696e43cb9661ac13f3d14dff02895f3ee571d3aa032695513ffe841f31b217654a554904c15e74832436b4950ac190fffba9d3fa85e3eae0d - languageName: node - linkType: hard - "@storybook/preview-api@npm:7.6.15": version: 7.6.15 resolution: "@storybook/preview-api@npm:7.6.15" @@ -19205,17 +18978,10 @@ __metadata: languageName: node linkType: hard -"@storybook/preview@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/preview@npm:7.6.13" - checksum: 10c0/dd04c5bab24fafacc5229bcbe88b8abbc837d44fb6f307616c994d7c33a5ac0d6419d3a868dc3ed131a748949f89a329c56586735049c100e322e5f5b5884813 - languageName: node - linkType: hard - -"@storybook/preview@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/preview@npm:7.6.14" - checksum: 10c0/9812804526da415bd4e8551b59d05608bf79bf19454ea24ca5d8139cf03862f1b5725003bd898f23814a068a02dd07a97d6033e9af730d05593d18b878c0ec7c +"@storybook/preview@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/preview@npm:7.6.20" + checksum: 10c0/8c779e4f12b7b35f4ba0c739ec1d58a06b124affb0120a198fe8b919eec66fcc5873409d7572ffa43cac0cdb12d48c2eb874558400366253d370f60a27cdb88e languageName: node linkType: hard @@ -19267,23 +19033,33 @@ __metadata: languageName: node linkType: hard -"@storybook/react-dom-shim@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/react-dom-shim@npm:7.6.14" +"@storybook/react-dom-shim@npm:7.6.15": + version: 7.6.15 + resolution: "@storybook/react-dom-shim@npm:7.6.15" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/6b1bb78a703b8cc678b91609d3860ce63593980dd3b34ce10678331774d6c1fcd4272e0e94b1b49e7bf5fcfcbc76ec8e2c71ac9655b45d495bcd75dcb100adf2 + languageName: node + linkType: hard + +"@storybook/react-dom-shim@npm:7.6.17": + version: 7.6.17 + resolution: "@storybook/react-dom-shim@npm:7.6.17" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10c0/b060195f05ed52022501d515bb04ff3618d429011892081afa86ab538c65aaadea273e1b22106514b964be449b206c8a8f364b6824d4d62eab090f2a38a1989f + checksum: 10c0/20558c58f9f0a3a00c5a1bbf2aa3517e3d318e6528f503129c99fb9ee4b604a225e79725f67e01e6e99d5d8c7db0614575dcc89af7768381afe59c976cb7cfc0 languageName: node linkType: hard -"@storybook/react-dom-shim@npm:7.6.15": - version: 7.6.15 - resolution: "@storybook/react-dom-shim@npm:7.6.15" +"@storybook/react-dom-shim@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/react-dom-shim@npm:7.6.20" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10c0/6b1bb78a703b8cc678b91609d3860ce63593980dd3b34ce10678331774d6c1fcd4272e0e94b1b49e7bf5fcfcbc76ec8e2c71ac9655b45d495bcd75dcb100adf2 + checksum: 10c0/444cd6bed0b4fb9f72038ce7c0ea0056377eba3c993a68f4e5a42357e0586e46ca2a4458669b38bbec2cd2569a3e5555eeb847e6e43496747b02989d9d42a884 languageName: node linkType: hard @@ -19309,6 +19085,28 @@ __metadata: languageName: node linkType: hard +"@storybook/react-webpack5@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/react-webpack5@npm:7.6.20" + dependencies: + "@storybook/builder-webpack5": "npm:7.6.20" + "@storybook/preset-react-webpack": "npm:7.6.20" + "@storybook/react": "npm:7.6.20" + "@types/node": "npm:^18.0.0" + peerDependencies: + "@babel/core": ^7.22.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + typescript: "*" + peerDependenciesMeta: + "@babel/core": + optional: true + typescript: + optional: true + checksum: 10c0/886e6f3328912721fc61a2f6f605801bd794665f63081ab02d40ac01da910529043b94d8c6e97b083b38ee82e33bc92f465d84952101fa04157de70e6ac4acba + languageName: node + linkType: hard + "@storybook/react@npm:7.1.1": version: 7.1.1 resolution: "@storybook/react@npm:7.1.1" @@ -19381,17 +19179,17 @@ __metadata: languageName: node linkType: hard -"@storybook/react@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/react@npm:7.6.14" +"@storybook/react@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/react@npm:7.6.20" dependencies: - "@storybook/client-logger": "npm:7.6.14" - "@storybook/core-client": "npm:7.6.14" - "@storybook/docs-tools": "npm:7.6.14" + "@storybook/client-logger": "npm:7.6.20" + "@storybook/core-client": "npm:7.6.20" + "@storybook/docs-tools": "npm:7.6.20" "@storybook/global": "npm:^5.0.0" - "@storybook/preview-api": "npm:7.6.14" - "@storybook/react-dom-shim": "npm:7.6.14" - "@storybook/types": "npm:7.6.14" + "@storybook/preview-api": "npm:7.6.20" + "@storybook/react-dom-shim": "npm:7.6.20" + "@storybook/types": "npm:7.6.20" "@types/escodegen": "npm:^0.0.6" "@types/estree": "npm:^0.0.51" "@types/node": "npm:^18.0.0" @@ -19413,7 +19211,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/206c24ffb925218839bcedf72055a0cdc5d8a69cd47383e86dd34262e21656526f868cecc4642d07303d1279c3cc47d7748a623942b7fd7bf16d695df21aae29 + checksum: 10c0/2a61c94bc9414637d31a9860c65263cf40fe9051711fea0d6723fad409d153b640f9f04cd515254bf64734774dfe2aa59f406dfe36e4a6f49696670503dd9104 languageName: node linkType: hard @@ -19503,22 +19301,6 @@ __metadata: languageName: node linkType: hard -"@storybook/telemetry@npm:7.6.17": - version: 7.6.17 - resolution: "@storybook/telemetry@npm:7.6.17" - dependencies: - "@storybook/client-logger": "npm:7.6.17" - "@storybook/core-common": "npm:7.6.17" - "@storybook/csf-tools": "npm:7.6.17" - chalk: "npm:^4.1.0" - detect-package-manager: "npm:^2.0.1" - fetch-retry: "npm:^5.0.2" - fs-extra: "npm:^11.1.0" - read-pkg-up: "npm:^7.0.1" - checksum: 10c0/2d13afef0fd73982c1efec1598583ed592bd608bbc61f9c4d96c47be9202d80043041764e00ea3b10b0636417cfbfe7b3d13c6898187a09554c8a696f89ac226 - languageName: node - linkType: hard - "@storybook/telemetry@npm:7.6.20": version: 7.6.20 resolution: "@storybook/telemetry@npm:7.6.20" @@ -19687,18 +19469,6 @@ __metadata: languageName: node linkType: hard -"@storybook/types@npm:7.4.5": - version: 7.4.5 - resolution: "@storybook/types@npm:7.4.5" - dependencies: - "@storybook/channels": "npm:7.4.5" - "@types/babel__core": "npm:^7.0.0" - "@types/express": "npm:^4.7.0" - file-system-cache: "npm:2.3.0" - checksum: 10c0/35cd3c5d388be8e1d2b636cd028b571ebb3bb17ede17669cbb1f8b26b95f69c90476f98340b8e70a1f98b910eac8435b80e6acf0a5ba9eff7a30fa0164216a22 - languageName: node - linkType: hard - "@storybook/types@npm:7.4.6": version: 7.4.6 resolution: "@storybook/types@npm:7.4.6" @@ -19735,30 +19505,6 @@ __metadata: languageName: node linkType: hard -"@storybook/types@npm:7.6.13": - version: 7.6.13 - resolution: "@storybook/types@npm:7.6.13" - dependencies: - "@storybook/channels": "npm:7.6.13" - "@types/babel__core": "npm:^7.0.0" - "@types/express": "npm:^4.7.0" - file-system-cache: "npm:2.3.0" - checksum: 10c0/b999d8583c8aad5b5c19a16e78fbb2250fd01e73b78e30db55591c120e97de4fec59b203c2e38f85ba8ccd8617df01ed464641f9f1641aad3b9b1c9452d13150 - languageName: node - linkType: hard - -"@storybook/types@npm:7.6.14": - version: 7.6.14 - resolution: "@storybook/types@npm:7.6.14" - dependencies: - "@storybook/channels": "npm:7.6.14" - "@types/babel__core": "npm:^7.0.0" - "@types/express": "npm:^4.7.0" - file-system-cache: "npm:2.3.0" - checksum: 10c0/08486a4ef8f647fcde7ffdb6e6f93aac496934bb40c224131600c6a4d46b8c4ac0b156ea1ecc66d810d7097d207a517818f849d6b6a11c2d5f603a5d90a64dfb - languageName: node - linkType: hard - "@storybook/types@npm:7.6.15": version: 7.6.15 resolution: "@storybook/types@npm:7.6.15" @@ -21865,6 +21611,15 @@ __metadata: languageName: node linkType: hard +"@types/mjml-browser@npm:^4.15.0": + version: 4.15.0 + resolution: "@types/mjml-browser@npm:4.15.0" + dependencies: + "@types/mjml-core": "npm:*" + checksum: 10c0/5d6f3d62d8b93535ab95df47cb276f52bb4499635b77b25978f6bf6d90dfa5d212fbc1590ae328147f1f8b8aa3a88779ce995634ea60d1e2b625e4d4109698fe + languageName: node + linkType: hard + "@types/mjml-core@npm:*": version: 4.15.2 resolution: "@types/mjml-core@npm:4.15.2" @@ -28663,6 +28418,24 @@ __metadata: languageName: node linkType: hard +"copyfiles@npm:^2.4.1": + version: 2.4.1 + resolution: "copyfiles@npm:2.4.1" + dependencies: + glob: "npm:^7.0.5" + minimatch: "npm:^3.0.3" + mkdirp: "npm:^1.0.4" + noms: "npm:0.0.0" + through2: "npm:^2.0.1" + untildify: "npm:^4.0.0" + yargs: "npm:^16.1.0" + bin: + copyfiles: copyfiles + copyup: copyfiles + checksum: 10c0/e65cd055ec9acc14997b0ace83973d73f8d9c68167cbf4293c40b52d100af09a8c8da329042d52dc33422c0a8cbf74c6efb25e9ae088667721653659bd67bf57 + languageName: node + linkType: hard + "core-js-compat@npm:^3.40.0": version: 3.43.0 resolution: "core-js-compat@npm:3.43.0" @@ -35324,13 +35097,13 @@ __metadata: "@sentry/node": "npm:8.42.0" "@sentry/opentelemetry": "npm:8.42.0" "@smithy/smithy-client": "npm:^4.5.0" - "@storybook/addon-essentials": "npm:7.6.15" + "@storybook/addon-essentials": "npm:7.6.17" "@storybook/addon-styling": "npm:1.3.7" - "@storybook/core-common": "npm:7.4.5" - "@storybook/core-server": "npm:7.6.17" - "@storybook/html-webpack5": "npm:7.6.13" - "@storybook/nextjs": "npm:7.6.14" - "@storybook/react-webpack5": "npm:7.5.3" + "@storybook/core-common": "npm:7.6.20" + "@storybook/core-server": "npm:7.6.20" + "@storybook/html-webpack5": "npm:7.6.20" + "@storybook/nextjs": "npm:7.6.20" + "@storybook/react-webpack5": "npm:7.6.20" "@stripe/react-stripe-js": "npm:^2.7.1" "@stripe/stripe-js": "npm:^4.9.0" "@swc-node/register": "npm:1.10.9" @@ -35348,6 +35121,7 @@ __metadata: "@types/hapi": "npm:^18.0.15" "@types/jest": "npm:29.5.14" "@types/jsdom": "npm:^21" + "@types/mjml-browser": "npm:^4.15.0" "@types/module-alias": "npm:^2" "@types/mysql": "npm:^2" "@types/node": "npm:^22.13.5" @@ -35375,6 +35149,7 @@ __metadata: class-validator: "npm:0.14.1" classnames: "npm:^2.5.1" clsx: "npm:^2.1.1" + copyfiles: "npm:^2.4.1" diffparser: "npm:^2.0.1" dotenv: "npm:^16.4.5" esbuild: "npm:^0.17.15" @@ -35413,6 +35188,7 @@ __metadata: kysely: "npm:^0.27.2" lint-staged: "npm:^15.2.0" maxmind: "npm:^4.3.22" + mjml-browser: "npm:^4.16.1" mocha-junit-reporter: "npm:^2.2.0" mocha-multi: "npm:^1.1.7" module-alias: "npm:^2.2.3" @@ -35430,9 +35206,11 @@ __metadata: nx: "npm:21.1.2" nx-cloud: "npm:19.1.0" objection: "npm:^3.1.3" + os-browserify: "npm:^0.3.0" passport: "npm:^0.7.0" passport-http-bearer: "npm:^1.0.1" passport-jwt: "npm:^4.0.1" + path-browserify: "npm:^1.0.1" persistgraphql: "npm:^0.3.11" pm2: "npm:^5.4.2" postcss: "npm:8.5.0" @@ -43625,7 +43403,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:2 || 3, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:2 || 3, minimatch@npm:^3.0.2, minimatch@npm:^3.0.3, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -43870,6 +43648,13 @@ __metadata: languageName: node linkType: hard +"mjml-browser@npm:^4.16.1": + version: 4.16.1 + resolution: "mjml-browser@npm:4.16.1" + checksum: 10c0/ab1112a95126d134031eca4d98735593b49937c567e11e7acb49ed06d02645c83d493b0bc52197f55d5b2807bed699fab0400e9d54fdf73c163ec5b9f8286bb7 + languageName: node + linkType: hard + "mjml-button@npm:4.15.3": version: 4.15.3 resolution: "mjml-button@npm:4.15.3" @@ -45389,6 +45174,16 @@ __metadata: languageName: node linkType: hard +"noms@npm:0.0.0": + version: 0.0.0 + resolution: "noms@npm:0.0.0" + dependencies: + inherits: "npm:^2.0.1" + readable-stream: "npm:~1.0.31" + checksum: 10c0/7790dbbef45c593b5444b361cb9cde3260244ab66aaa199c0728d334525eb69df96231115cff260b71b92fc7a6915a642aa22f2f8448696d8dd6e7d7cebfccce + languageName: node + linkType: hard + "nopt@npm:^7.2.1": version: 7.2.1 resolution: "nopt@npm:7.2.1" @@ -50820,6 +50615,18 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:~1.0.31": + version: 1.0.34 + resolution: "readable-stream@npm:1.0.34" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.1" + isarray: "npm:0.0.1" + string_decoder: "npm:~0.10.x" + checksum: 10c0/02272551396ed8930ddee1a088bdf0379f0f7cc47ac49ed8804e998076cb7daec9fbd2b1fd9c0490ec72e56e8bb3651abeb8080492b8e0a9c3f2158330908ed6 + languageName: node + linkType: hard + "readdirp@npm:^2.2.1": version: 2.2.1 resolution: "readdirp@npm:2.2.1" @@ -54266,7 +54073,7 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:0.10": +"string_decoder@npm:0.10, string_decoder@npm:~0.10.x": version: 0.10.31 resolution: "string_decoder@npm:0.10.31" checksum: 10c0/1c628d78f974aa7539c496029f48e7019acc32487fc695464f9d6bdfec98edd7d933a06b3216bc2016918f6e75074c611d84430a53cb0e43071597d6c1ac5e25 @@ -55668,7 +55475,7 @@ __metadata: languageName: node linkType: hard -"through2@npm:^2.0.0, through2@npm:^2.0.2, through2@npm:^2.0.3, through2@npm:~2.0.0": +"through2@npm:^2.0.0, through2@npm:^2.0.1, through2@npm:^2.0.2, through2@npm:^2.0.3, through2@npm:~2.0.0": version: 2.0.5 resolution: "through2@npm:2.0.5" dependencies: @@ -59791,7 +59598,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^16.1.1, yargs@npm:^16.2.0": +"yargs@npm:^16.1.0, yargs@npm:^16.1.1, yargs@npm:^16.2.0": version: 16.2.0 resolution: "yargs@npm:16.2.0" dependencies: