From b79f574fe841d809e2bf74d4b9e4f31284908ff4 Mon Sep 17 00:00:00 2001 From: Robert Field Date: Fri, 29 Sep 2023 21:37:21 +0100 Subject: [PATCH] feat: auto add env var ep payment (#80) * feat: unknown error message resolver * feat: devkit tree utilities * feat: fix for auth issue and added ep payment .env.local auto update * refactor: removed console log * refactor: remove catch * refactor: formatting * chore: adding logging * feat: change the handling of already existing ep payments gateways * chore: changeset * feat: fetch stores before building catalog prompts * feat: environment variable shared utilities * feat: warning for non-composable projects * fix: expiration checker * feat: improved error handling * chore: changeset --- .changeset/lovely-berries-speak.md | 8 ++ .changeset/shiny-dolphins-admire.md | 6 + packages/composable-cli/package.json | 1 + .../src/commands/config/config-command.tsx | 19 ++- .../src/commands/feedback/feedback-command.ts | 8 +- .../src/commands/generate/d2c/d2c-command.tsx | 23 +++- .../commands/generate/generate-command.tsx | 5 +- .../commands/insights/insights-command.tsx | 3 +- .../algolia/algolia-integration-command.tsx | 74 +++++++---- .../integration/integration-command.tsx | 16 ++- .../src/commands/login/login-command.ts | 47 ++++--- .../src/commands/logout/logout-command.ts | 9 +- .../ep-payments/ep-payments-command.tsx | 111 ++++++++++++++--- .../ep-payments-integration.types.ts | 18 ++- .../util/setup-ep-payments-schema.ts | 6 + .../ep-payments/util/setup-epcc-ep-payment.ts | 48 ++++---- .../commands/payments/payments-command.tsx | 5 +- .../src/commands/store/store-command.tsx | 16 +-- .../src/commands/ui/error/error.tsx | 29 +++++ .../src/commands/ui/login/welcome-note.tsx | 10 ++ .../authentication/ep-client-middleware.ts | 39 ++++-- .../src/lib/authentication/get-token.ts | 33 ++--- .../src/lib/catalog/build-catalog-prompts.ts | 15 ++- .../src/lib/composable-project-middleware.ts | 21 ++++ .../src/lib/config-middleware.ts | 18 +-- .../src/lib/devkit/add-env-variables.ts | 116 ++++++++++++++++++ .../src/lib/devkit/tree-util.ts | 19 +++ .../src/lib/stores/switch-store-schema.ts | 4 + packages/composable-cli/src/types/command.ts | 2 + packages/composable-cli/src/util/command.ts | 40 ++++++ .../src/util/conf-store/store-credentials.ts | 11 +- .../composable-cli/src/util/error-handler.ts | 37 +++--- .../src/util/has-expired.test.ts | 11 +- .../composable-cli/src/util/has-expired.ts | 11 +- .../src/util/process-unknown-error.ts | 15 +++ .../src/payment-gateways/check-gateway.ts | 2 +- .../ep-payments/setup-epcc-ep-payment.ts | 18 +-- yarn.lock | 12 ++ 38 files changed, 657 insertions(+), 229 deletions(-) create mode 100644 .changeset/lovely-berries-speak.md create mode 100644 .changeset/shiny-dolphins-admire.md create mode 100644 packages/composable-cli/src/commands/ui/error/error.tsx create mode 100644 packages/composable-cli/src/lib/composable-project-middleware.ts create mode 100644 packages/composable-cli/src/lib/devkit/add-env-variables.ts create mode 100644 packages/composable-cli/src/lib/devkit/tree-util.ts create mode 100644 packages/composable-cli/src/util/process-unknown-error.ts diff --git a/.changeset/lovely-berries-speak.md b/.changeset/lovely-berries-speak.md new file mode 100644 index 00000000..11171e67 --- /dev/null +++ b/.changeset/lovely-berries-speak.md @@ -0,0 +1,8 @@ +--- +"composable-cli": minor +--- + +- auto adds ep payments environment variables so the user no longer needs to add them on their own +- changes the way authentication was handled +- adds some additional logging and error handling + diff --git a/.changeset/shiny-dolphins-admire.md b/.changeset/shiny-dolphins-admire.md new file mode 100644 index 00000000..163667fd --- /dev/null +++ b/.changeset/shiny-dolphins-admire.md @@ -0,0 +1,6 @@ +--- +"@elasticpath/composable-common": patch +"composable-cli": patch +--- + +changed the handling of already existing ep payments gateways diff --git a/packages/composable-cli/package.json b/packages/composable-cli/package.json index c11e10d6..74f58a6d 100644 --- a/packages/composable-cli/package.json +++ b/packages/composable-cli/package.json @@ -34,6 +34,7 @@ "ink-big-text": "1", "ink-gradient": "2", "ink-link": "2", + "ink-table": "^3.0.0", "inquirer": "8.2.4", "node-fetch": "2.7.0", "open": "8", diff --git a/packages/composable-cli/src/commands/config/config-command.tsx b/packages/composable-cli/src/commands/config/config-command.tsx index 36f771fc..8269d945 100644 --- a/packages/composable-cli/src/commands/config/config-command.tsx +++ b/packages/composable-cli/src/commands/config/config-command.tsx @@ -1,7 +1,6 @@ import yargs from "yargs" import Conf from "conf" import { CommandContext, CommandHandlerFunction } from "../../types/command" -import { handleErrors } from "../../util/error-handler" import { ConfigCommandArguments, ConfigCommandData, @@ -16,7 +15,7 @@ export function configClearCommand(store: Conf): void { export function createConfigCommand( ctx: CommandContext, ): yargs.CommandModule<{}, ConfigCommandArguments> { - const { store, logger } = ctx + const { store, logger, handleErrors } = ctx return { command: "config", @@ -26,16 +25,24 @@ export function createConfigCommand( .command({ command: "list", describe: "List all stored configuration", - handler: (_args) => { + handler: handleErrors(async (_args) => { logger.info(JSON.stringify(store.store, null, 2)) - }, + return { + success: true, + data: {}, + } + }), }) .command({ command: "clear", describe: "Clear all stored configuration", - handler: (_args) => { + handler: handleErrors(async (_args) => { configClearCommand(store) - }, + return { + success: true, + data: {}, + } + }), }) .example("$0 config list", "list all stored configuration") .example("$0 config clear", "clear all stored configuration") diff --git a/packages/composable-cli/src/commands/feedback/feedback-command.ts b/packages/composable-cli/src/commands/feedback/feedback-command.ts index 5cc73eeb..d9c6e530 100644 --- a/packages/composable-cli/src/commands/feedback/feedback-command.ts +++ b/packages/composable-cli/src/commands/feedback/feedback-command.ts @@ -4,7 +4,6 @@ import { CommandHandlerFunction, RootCommandArguments, } from "../../types/command" -import { handleErrors } from "../../util/error-handler" import { renderInk } from "../../lib/ink/render-ink" import React from "react" import { @@ -17,19 +16,20 @@ import { Feedback } from "../ui/feedback/feedback" import { trackCommandHandler } from "../../util/track-command-handler" import { isTTY } from "../../util/is-tty" export function createFeedbackCommand( - ctx: CommandContext + ctx: CommandContext, ): yargs.CommandModule { + const { handleErrors } = ctx return { command: "feedback", describe: "Feedback to the Composable CLI", handler: handleErrors( - trackCommandHandler(ctx, createFeedbackCommandHandler) + trackCommandHandler(ctx, createFeedbackCommandHandler), ), } } export function createFeedbackCommandHandler( - _ctx: CommandContext + _ctx: CommandContext, ): CommandHandlerFunction< FeedbackCommandData, FeedbackCommandError, diff --git a/packages/composable-cli/src/commands/generate/d2c/d2c-command.tsx b/packages/composable-cli/src/commands/generate/d2c/d2c-command.tsx index 8e21898e..2970adcb 100644 --- a/packages/composable-cli/src/commands/generate/d2c/d2c-command.tsx +++ b/packages/composable-cli/src/commands/generate/d2c/d2c-command.tsx @@ -24,7 +24,6 @@ import { CommandHandlerFunction, CommandResult, } from "../../../types/command" -import { handleErrors } from "../../../util/error-handler" import { getRegion, resolveHostFromRegion } from "../../../util/resolve-region" import { createApplicationKeys } from "../../../util/create-client-secret" import { renderInk } from "../../../lib/ink/render-ink" @@ -48,7 +47,10 @@ import { detect } from "../../../lib/detect-package-manager" import { createAlgoliaIntegrationCommandHandler } from "../../integration/algolia/algolia-integration-command" import boxen from "boxen" import { getCredentials } from "../../../lib/authentication/get-token" -import { createEPPaymentsCommandHandler } from "../../payments/ep-payments/ep-payments-command" +import { + createEPPaymentsCommandHandler, + isAlreadyExistsError, +} from "../../payments/ep-payments/ep-payments-command" export function createD2CCommand( ctx: CommandContext, @@ -110,7 +112,9 @@ export function createD2CCommand( return addSchemaOptionsToCommand(result, options) }, - handler: handleErrors(trackCommandHandler(ctx, createD2CCommandHandler)), + handler: ctx.handleErrors( + trackCommandHandler(ctx, createD2CCommandHandler), + ), } } @@ -551,13 +555,22 @@ export function createD2CCommandHandler( ...args, }) + if (!result.success && isAlreadyExistsError(result.error)) { + notes.push({ + title: "EP Payments setup", + description: `The EP Payments integration was already setup. It was using the account id ${colors.bold.green( + result.error.accountId, + )}`, + }) + } + if (result.success) { notes.push({ title: "EP Payments setup", description: `Don't forget to add your EP Payment variables to .env.local ${colors.bold.green( - `\nNEXT_PUBLIC_STRIPE_ACCOUNT_ID=${gatheredOptions.epPaymentsStripeAccountId}`, + `\nNEXT_PUBLIC_STRIPE_ACCOUNT_ID=${result.data.accountId}`, )}${colors.bold.green( - `\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${gatheredOptions.epPaymentsStripePublishableKey}`, + `\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${result.data.publishableKey}`, )}`, }) } diff --git a/packages/composable-cli/src/commands/generate/generate-command.tsx b/packages/composable-cli/src/commands/generate/generate-command.tsx index cc46f406..b85f1bff 100644 --- a/packages/composable-cli/src/commands/generate/generate-command.tsx +++ b/packages/composable-cli/src/commands/generate/generate-command.tsx @@ -4,7 +4,6 @@ import { CommandHandlerFunction, RootCommandArguments, } from "../../types/command" -import { handleErrors } from "../../util/error-handler" import { GenerateCommandArguments, GenerateCommandData, @@ -67,7 +66,7 @@ export function createGenerateCommand( .demandCommand(1) .strict() }, - handler: handleErrors( + handler: ctx.handleErrors( trackCommandHandler(ctx, createGenerateCommandHandler), ), } @@ -125,7 +124,7 @@ export function createActiveStoreMiddleware( return } - return handleErrors(createSetStoreCommandHandler(ctx))(args) + return ctx.handleErrors(createSetStoreCommandHandler(ctx))(args) } } diff --git a/packages/composable-cli/src/commands/insights/insights-command.tsx b/packages/composable-cli/src/commands/insights/insights-command.tsx index a971d54e..4149e064 100644 --- a/packages/composable-cli/src/commands/insights/insights-command.tsx +++ b/packages/composable-cli/src/commands/insights/insights-command.tsx @@ -4,7 +4,6 @@ import { CommandHandlerFunction, RootCommandArguments, } from "../../types/command" -import { handleErrors } from "../../util/error-handler" import { InsightsCommandArguments, InsightsCommandData, @@ -30,7 +29,7 @@ export function createInsightsCommand( }) .help() }, - handler: handleErrors( + handler: ctx.handleErrors( trackCommandHandler(ctx, createInsightsCommandHandler), ), } diff --git a/packages/composable-cli/src/commands/integration/algolia/algolia-integration-command.tsx b/packages/composable-cli/src/commands/integration/algolia/algolia-integration-command.tsx index 4a263f55..db0d78a2 100644 --- a/packages/composable-cli/src/commands/integration/algolia/algolia-integration-command.tsx +++ b/packages/composable-cli/src/commands/integration/algolia/algolia-integration-command.tsx @@ -5,7 +5,6 @@ import { AlgoliaIntegrationCommandError, } from "./algolia-integration.types" import { CommandContext, CommandHandlerFunction } from "../../../types/command" -import { handleErrors } from "../../../util/error-handler" import { trackCommandHandler } from "../../../util/track-command-handler" import { createActiveStoreMiddleware, @@ -35,7 +34,10 @@ import { algoliaIntegrationSetupSchema, } from "./utility/integration-hub/setup-algolia-schema" import boxen from "boxen" -import { buildCatalogPrompts } from "../../../lib/catalog/build-catalog-prompts" +import { + buildCatalogPrompts, + getActiveStoreCatalogs, +} from "../../../lib/catalog/build-catalog-prompts" import { getCatalogRelease, publishCatalog, @@ -47,6 +49,7 @@ import { doesIndexExist, } from "./utility/algolia/algolia" import { logging } from "@angular-devkit/core" +import { attemptToAddEnvVariables } from "../../../lib/devkit/add-env-variables" export function createAlgoliaIntegrationCommand( ctx: CommandContext, @@ -73,7 +76,7 @@ export function createAlgoliaIntegrationCommand( .fail(false) .help() }, - handler: handleErrors( + handler: ctx.handleErrors( trackCommandHandler(ctx, createAlgoliaIntegrationCommandHandler), ), } @@ -182,7 +185,41 @@ export function createAlgoliaIntegrationCommandHandler( } } - const catalogsPrompts = await buildCatalogPrompts(ctx.requester) + const catalogsResult = await getActiveStoreCatalogs(ctx.requester) + + if (!catalogsResult.success) { + logger.error("Failed to fetch catalogs for active store") + return { + success: false, + error: { + code: "FAILED_TO_FETCH_CATALOGS", + message: "Failed to fetch catalogs for active store", + }, + } + } + + const catalogs = catalogsResult.data + + if (catalogs.length < 1) { + logger.warn( + boxen( + "The Algolia integration will only work correctly if you have a published catalog in your store. We were not able to find any catalogs in your store to publish. Please add a catalog and then rerun the `int algolia` command.\n\nLearn more about catalogs and publishing https://elasticpath.dev/docs/pxm/catalogs/catalogs", + { + padding: 1, + margin: 1, + }, + ), + ) + return { + success: false, + error: { + code: "FAILED_TO_FIND_ANY_CATALOGS", + message: "There were not catalogs in the store", + }, + } + } + + const catalogsPrompts = await buildCatalogPrompts(catalogs) if (!catalogsPrompts.success) { return { @@ -258,6 +295,17 @@ export function createAlgoliaIntegrationCommandHandler( catalog.id.split("-")[0] }` + const envVarResult = await attemptToAddEnvVariables(ctx, spinner, { + NEXT_PUBLIC_ALGOLIA_INDEX_NAME: algoliaIndexName, + }) + + if (!envVarResult.success) { + return { + success: false, + error: envVarResult.error, + } + } + logger.info( boxen( `Published catalog should have an Algolia index of ${colors.bold.green( @@ -270,13 +318,6 @@ export function createAlgoliaIntegrationCommandHandler( ), ) - // TODO: tell the user the name of the published indexes so they can add them to their .env.local file - // - need to wait for the users integration publish job to be finished. - // - indexes are made up of the _ e.g. Default_11ce355f - // - example with space in name "Default Catalog" -> Default_Catalog_11ce355f - // - check if the user has an .env.local file in the directory they have executed the command from - // - better yet prompt the user to ask if they want that done for them. - spinner.start(`Checking Algolia index exists...`) while (true) { const indexCheckResult = await doesIndexExist({ @@ -291,17 +332,6 @@ export function createAlgoliaIntegrationCommandHandler( await timer(3000) } - // if (!indexCheckResult) { - // spinner.fail(`Failed to check Algolia index`) - // return { - // success: false, - // error: { - // code: "FAILED_TO_CHECK_ALGOLIA_INDEX", - // message: "Failed to check Algolia index", - // } - // } - // } - spinner.text = `Found index ${algoliaIndexName} performing additional setup...` const additionalAlgoliaSetupResult = await additionalAlgoliaSetup({ diff --git a/packages/composable-cli/src/commands/integration/integration-command.tsx b/packages/composable-cli/src/commands/integration/integration-command.tsx index ce88aab3..09557467 100644 --- a/packages/composable-cli/src/commands/integration/integration-command.tsx +++ b/packages/composable-cli/src/commands/integration/integration-command.tsx @@ -4,14 +4,17 @@ import { CommandHandlerFunction, RootCommandArguments, } from "../../types/command" -import { handleErrors } from "../../util/error-handler" -import {IntegrationCommandArguments, IntegrationCommandData, IntegrationCommandError +import { + IntegrationCommandArguments, + IntegrationCommandData, + IntegrationCommandError, } from "./integration.types" import { trackCommandHandler } from "../../util/track-command-handler" import { createAlgoliaIntegrationCommand } from "./algolia/algolia-integration-command" +import { createComposableProjectMiddleware } from "../../lib/composable-project-middleware" export function createIntegrationCommand( - ctx: CommandContext + ctx: CommandContext, ): yargs.CommandModule { return { command: "integration", @@ -19,19 +22,20 @@ export function createIntegrationCommand( describe: "setup Elastic Path integrations for your storefront", builder: (yargs) => { return yargs + .middleware(createComposableProjectMiddleware(ctx)) .command(createAlgoliaIntegrationCommand(ctx)) .help() .demandCommand(1) .strict() }, - handler: handleErrors( - trackCommandHandler(ctx, createIntegrationCommandHandler) + handler: ctx.handleErrors( + trackCommandHandler(ctx, createIntegrationCommandHandler), ), } } export function createIntegrationCommandHandler( - _ctx: CommandContext + _ctx: CommandContext, ): CommandHandlerFunction< IntegrationCommandData, IntegrationCommandError, diff --git a/packages/composable-cli/src/commands/login/login-command.ts b/packages/composable-cli/src/commands/login/login-command.ts index e265267a..d0210f12 100644 --- a/packages/composable-cli/src/commands/login/login-command.ts +++ b/packages/composable-cli/src/commands/login/login-command.ts @@ -9,7 +9,6 @@ import { CommandHandlerFunction, RootCommandArguments, } from "../../types/command" -import { handleErrors } from "../../util/error-handler" import { LoginCommandArguments, LoginCommandData, @@ -27,6 +26,7 @@ import { WelcomeNote } from "../ui/login/welcome-note" import { render } from "ink" import { trackCommandHandler } from "../../util/track-command-handler" import { EpccRequester } from "../../util/command" +import { credentialsSchema } from "../../lib/authentication/credentials-schema" /** * Region prompts @@ -42,24 +42,6 @@ const regionPrompts = { default: "us-east", } as const -/* -choices: [ - { - name: "North America (free-trial region)", - value: "useast.api.elasticpath.com", - }, - { - name: "Europe", - value: "euwest.api.elasticpath.com", - }, - new inquirer.Separator(), - { - name: "Other", - value: "Other", - }, - ], - */ - function handleRegionUpdate(store: Conf, region: "eu-west" | "us-east"): void { store.set("region", region) } @@ -98,7 +80,9 @@ export function createLoginCommand( "strip-aliased": true, }) }, - handler: handleErrors(trackCommandHandler(ctx, createLoginCommandHandler)), + handler: ctx.handleErrors( + trackCommandHandler(ctx, createLoginCommandHandler), + ), } } @@ -114,9 +98,9 @@ export function createAuthenticationMiddleware( return } - return handleErrors(trackCommandHandler(ctx, createLoginCommandHandler))( - args, - ) + return ctx.handleErrors( + trackCommandHandler(ctx, createLoginCommandHandler), + )(args) } } @@ -224,16 +208,27 @@ async function authenticateUserPassword( password, ) - if (checkIsErrorResponse(credentialsResp)) { + const parsedCredentialsResp = credentialsSchema.safeParse(credentialsResp) + + if (!parsedCredentialsResp.success) { + return { + success: false, + code: "authentication-failure", + name: "data parsing error", + message: parsedCredentialsResp.error.message, + } + } + + if (checkIsErrorResponse(parsedCredentialsResp.data)) { return { success: false, code: "authentication-failure", name: "epcc error", - message: credentialsResp.errors.toString(), + message: parsedCredentialsResp.data.errors.toString(), } } - storeCredentials(store, credentialsResp as any) + storeCredentials(store, parsedCredentialsResp.data) return { success: true, diff --git a/packages/composable-cli/src/commands/logout/logout-command.ts b/packages/composable-cli/src/commands/logout/logout-command.ts index 4202c856..185964b8 100644 --- a/packages/composable-cli/src/commands/logout/logout-command.ts +++ b/packages/composable-cli/src/commands/logout/logout-command.ts @@ -1,6 +1,5 @@ import yargs from "yargs" import { CommandContext, CommandHandlerFunction } from "../../types/command" -import { handleErrors } from "../../util/error-handler" import { LogoutCommandArguments, LogoutCommandData, @@ -14,17 +13,19 @@ import { handleClearCredentials } from "../../util/conf-store/store-credentials" import { trackCommandHandler } from "../../util/track-command-handler" export function createLogoutCommand( - ctx: CommandContext + ctx: CommandContext, ): yargs.CommandModule<{}, LogoutCommandArguments> { return { command: "logout", describe: "Logout of the Composable CLI", - handler: handleErrors(trackCommandHandler(ctx, createLogoutCommandHandler)), + handler: ctx.handleErrors( + trackCommandHandler(ctx, createLogoutCommandHandler), + ), } } export function createLogoutCommandHandler( - ctx: CommandContext + ctx: CommandContext, ): CommandHandlerFunction< LogoutCommandData, LogoutCommandError, diff --git a/packages/composable-cli/src/commands/payments/ep-payments/ep-payments-command.tsx b/packages/composable-cli/src/commands/payments/ep-payments/ep-payments-command.tsx index 2de77c82..bbb420df 100644 --- a/packages/composable-cli/src/commands/payments/ep-payments/ep-payments-command.tsx +++ b/packages/composable-cli/src/commands/payments/ep-payments/ep-payments-command.tsx @@ -3,9 +3,9 @@ import { EPPaymentsCommandArguments, EPPaymentsCommandData, EPPaymentsCommandError, + EPPaymentsCommandErrorAlreadyExists, } from "./ep-payments-integration.types" import { CommandContext, CommandHandlerFunction } from "../../../types/command" -import { handleErrors } from "../../../util/error-handler" import { trackCommandHandler } from "../../../util/track-command-handler" import { createActiveStoreMiddleware, @@ -20,9 +20,13 @@ import ora from "ora" import { logging } from "@angular-devkit/core" import { setupEPPaymentsPaymentGateway } from "./util/setup-epcc-ep-payment" import { + EPPaymentsForce, EPPaymentsSetup, epPaymentsSetupSchema, } from "./util/setup-ep-payments-schema" +import { processUnknownError } from "../../../util/process-unknown-error" +import { attemptToAddEnvVariables } from "../../../lib/devkit/add-env-variables" +import { checkGateway } from "@elasticpath/composable-common" export function createEPPaymentsCommand( ctx: CommandContext, @@ -43,15 +47,25 @@ export function createEPPaymentsCommand( type: "string", description: "EP Payments publishable key", }) + .option("force", { + type: "boolean", + description: "Force setup of EP Payments even if already enabled", + }) .fail(false) .help() }, - handler: handleErrors( + handler: ctx.handleErrors( trackCommandHandler(ctx, createEPPaymentsCommandHandler), ), } } +export function isAlreadyExistsError( + error: EPPaymentsCommandError, +): error is EPPaymentsCommandErrorAlreadyExists { + return error.code === "ep_payments_already_setup" +} + export function createEPPaymentsCommandHandler( ctx: CommandContext, ): CommandHandlerFunction< @@ -62,8 +76,10 @@ export function createEPPaymentsCommandHandler( return async function epPaymentsCommandHandler(args) { const spinner = ora() + const { epClient, logger } = ctx + try { - if (!ctx.epClient) { + if (!epClient) { spinner.fail(`Failed to setup EP Payments.`) return { success: false, @@ -74,7 +90,45 @@ export function createEPPaymentsCommandHandler( } } - const options = await resolveOptions(args, ctx.logger, ansiColors) + spinner.start(`Checking if EP Payments already exists...`) + // check if EP Payments is already setup + if (!args.force) { + const checkGatewayResult = await checkGateway( + epClient, + "elastic_path_payments_stripe", + ) + spinner.stop() + + if (checkGatewayResult.success) { + const forceResult = await resolveForceOptions(args) + + if (!forceResult.force) { + spinner.fail( + `EP Payments already exists and you didn't want to force an update.`, + ) + + const existingAccountId = checkGatewayResult.data.stripe_account! + logger.warn( + boxen( + `EP Payments was already setup with account id: ${ctx.colors.bold.green( + existingAccountId, + )}\n\nMake sure you add the correct account id NEXT_PUBLIC_STRIPE_ACCOUNT_ID=${existingAccountId} and with the appropriate publishable key NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY to your .env.local file.`, + { padding: 1, borderColor: "yellow" }, + ), + ) + return { + success: false, + error: { + code: "ep_payments_already_setup", + message: "EP Payments was already setup", + accountId: existingAccountId, + }, + } + } + } + } + + const options = await resolveOptions(args, logger, ansiColors) spinner.start(`Setting up EP Payments...`) const result = await setupEPPaymentsPaymentGateway( @@ -82,8 +136,8 @@ export function createEPPaymentsCommandHandler( epPaymentsStripeAccountId: options.accountId, epPaymentsStripePublishableKey: options.publishableKey, }, - ctx.epClient, - ctx.logger, + epClient, + logger, ) if (!result.success) { @@ -97,26 +151,28 @@ export function createEPPaymentsCommandHandler( } } - if (result.data.stripe_account !== options.accountId) { - spinner.succeed(`EP Payments was already setup.`) - return { - success: true, - data: {}, - } - } + await attemptToAddEnvVariables(ctx, spinner, { + NEXT_PUBLIC_STRIPE_ACCOUNT_ID: options.accountId, + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: options.publishableKey, + }) spinner.succeed(`EP Payments setup successfully.`) + return { success: true, - data: {}, + data: { + accountId: options.accountId, + publishableKey: options.publishableKey, + }, } } catch (e) { - spinner.fail(`Failed to setup Algolia integration`) + spinner.fail(`Failed to setup EP Payment gateway.`) + logger.error(processUnknownError(e)) return { success: false, error: { - code: "ALGOLIA_INTEGRATION_SETUP_FAILED", - message: "Failed to setup Algolia integration", + code: "FAILED_TO_SETUP_EP_PAYMENT_GATEWAY", + message: "Failed to setup EP Payment gateway", }, } } @@ -144,6 +200,27 @@ async function resolveOptions( return parsed.data } +async function resolveForceOptions( + args: EPPaymentsCommandArguments, +): Promise { + if (args.interactive && isTTY()) { + const { force } = await inquirer.prompt([ + { + type: "confirm", + name: "force", + message: "EP Payments is already enabled would you like update anyway?", + }, + ]) + return { + force, + } + } + + throw new Error( + `Invalid arguments: ep payments is already enabled and missing force argument`, + ) +} + async function epPaymentsOptionsPrompts( args: EPPaymentsCommandArguments, logger: logging.Logger, diff --git a/packages/composable-cli/src/commands/payments/ep-payments/ep-payments-integration.types.ts b/packages/composable-cli/src/commands/payments/ep-payments/ep-payments-integration.types.ts index 29a3a315..04376556 100644 --- a/packages/composable-cli/src/commands/payments/ep-payments/ep-payments-integration.types.ts +++ b/packages/composable-cli/src/commands/payments/ep-payments/ep-payments-integration.types.ts @@ -1,13 +1,25 @@ import { PaymentsCommandArguments } from "../payments.types" -export type EPPaymentsCommandData = {} +export type EPPaymentsCommandData = { + accountId: string + publishableKey: string +} + +export type EPPaymentsCommandError = + | { + code: string + message: string + } + | EPPaymentsCommandErrorAlreadyExists -export type EPPaymentsCommandError = { - code: string +export type EPPaymentsCommandErrorAlreadyExists = { + code: "ep_payments_already_setup" message: string + accountId: string } export type EPPaymentsCommandArguments = { accountId?: string publishableKey?: string + force?: boolean } & PaymentsCommandArguments diff --git a/packages/composable-cli/src/commands/payments/ep-payments/util/setup-ep-payments-schema.ts b/packages/composable-cli/src/commands/payments/ep-payments/util/setup-ep-payments-schema.ts index a3c4e55d..4975a82a 100644 --- a/packages/composable-cli/src/commands/payments/ep-payments/util/setup-ep-payments-schema.ts +++ b/packages/composable-cli/src/commands/payments/ep-payments/util/setup-ep-payments-schema.ts @@ -6,3 +6,9 @@ export const epPaymentsSetupSchema = z.object({ }) export type EPPaymentsSetup = z.TypeOf + +export const epPaymentsForceSchema = z.object({ + force: z.boolean(), +}) + +export type EPPaymentsForce = z.TypeOf diff --git a/packages/composable-cli/src/commands/payments/ep-payments/util/setup-epcc-ep-payment.ts b/packages/composable-cli/src/commands/payments/ep-payments/util/setup-epcc-ep-payment.ts index a2479645..49d2ead6 100644 --- a/packages/composable-cli/src/commands/payments/ep-payments/util/setup-epcc-ep-payment.ts +++ b/packages/composable-cli/src/commands/payments/ep-payments/util/setup-epcc-ep-payment.ts @@ -1,30 +1,26 @@ import { logging } from "@angular-devkit/core" import type { Gateway, Moltin } from "@moltin/sdk" -import { checkGateway, OperationResult } from "@elasticpath/composable-common" +import { OperationResult } from "@elasticpath/composable-common" import { updateEpPaymentGateway } from "./update-gateway" import { EpPaymentGatewaySettings } from "./ep-payments-schema" +import { processUnknownError } from "../../../../util/process-unknown-error" export async function setupEPPaymentsPaymentGateway( sourceInput: EpPaymentGatewaySettings, epccClient: Moltin, logger: logging.LoggerApi, -): Promise> { +): Promise< + OperationResult< + Gateway, + { + code: "ep_payments_gateway_update_failed" | "unknown" + message: string + } + > +> { try { const { epPaymentsStripeAccountId } = sourceInput - /** - * Check if EP payments is enabled and do nothing if it is - */ - const checkGatewayResult = await checkGateway( - epccClient, - "elastic_path_payments_stripe", - ) - - if (checkGatewayResult.success) { - logger.debug(`EP Payment gateway is already enabled`) - return checkGatewayResult - } - /** * Update ep payment gateway to be enabled with test mode on */ @@ -35,22 +31,28 @@ export async function setupEPPaymentsPaymentGateway( if (!updateResult.success) { logger.debug(`Failed to update ep payment gateway.`) - return updateResult + return { + success: false, + error: { + code: "ep_payments_gateway_update_failed", + message: `Failed to update ep payment gateway. ${processUnknownError( + updateResult, + )}`, + }, + } } return updateResult } catch (err: unknown) { - const errorStr = `An unknown error occurred: ${ - err instanceof Error - ? `${err.name} = ${err.message}` - : JSON.stringify(err) - }` - + const errorStr = processUnknownError(err) logger.error(errorStr) return { success: false, - error: new Error(errorStr), + error: { + code: "unknown", + message: errorStr, + }, } } } diff --git a/packages/composable-cli/src/commands/payments/payments-command.tsx b/packages/composable-cli/src/commands/payments/payments-command.tsx index a1825bc2..0b29ff83 100644 --- a/packages/composable-cli/src/commands/payments/payments-command.tsx +++ b/packages/composable-cli/src/commands/payments/payments-command.tsx @@ -4,7 +4,6 @@ import { CommandHandlerFunction, RootCommandArguments, } from "../../types/command" -import { handleErrors } from "../../util/error-handler" import { trackCommandHandler } from "../../util/track-command-handler" import { createEPPaymentsCommand } from "./ep-payments/ep-payments-command" @@ -14,6 +13,7 @@ import { PaymentsCommandError, } from "./payments.types" import { createActiveStoreMiddleware } from "../generate/generate-command" +import { createComposableProjectMiddleware } from "../../lib/composable-project-middleware" export function createPaymentsCommand( ctx: CommandContext, @@ -24,13 +24,14 @@ export function createPaymentsCommand( describe: "setup Elastic Path payment gateways for your storefront", builder: (yargs) => { return yargs + .middleware(createComposableProjectMiddleware(ctx)) .middleware(createActiveStoreMiddleware(ctx)) .command(createEPPaymentsCommand(ctx)) .help() .demandCommand(1) .strict() }, - handler: handleErrors( + handler: ctx.handleErrors( trackCommandHandler(ctx, createPaymentsCommandHandler), ), } diff --git a/packages/composable-cli/src/commands/store/store-command.tsx b/packages/composable-cli/src/commands/store/store-command.tsx index c05b8d4b..9e31fbf2 100644 --- a/packages/composable-cli/src/commands/store/store-command.tsx +++ b/packages/composable-cli/src/commands/store/store-command.tsx @@ -5,7 +5,6 @@ import { CommandHandlerFunction, RootCommandArguments, } from "../../types/command" -import { handleErrors } from "../../util/error-handler" import { SetStoreCommandArguments, SetStoreCommandData, @@ -30,6 +29,7 @@ import { } from "../../util/epcc-error" import { trackCommandHandler } from "../../util/track-command-handler" import { EpccRequester } from "../../util/command" +import { storeUserStore } from "../../util/conf-store/store-credentials" export function createStoreCommand( ctx: CommandContext, @@ -46,7 +46,9 @@ export function createStoreCommand( .demandCommand(1) .strict() }, - handler: handleErrors(trackCommandHandler(ctx, createStoreCommandHandler)), + handler: ctx.handleErrors( + trackCommandHandler(ctx, createStoreCommandHandler), + ), } } @@ -64,7 +66,7 @@ export function createSetStoreCommand( }) .help() }, - handler: handleErrors( + handler: ctx.handleErrors( trackCommandHandler(ctx, createSetStoreCommandHandler), ), } @@ -121,14 +123,14 @@ export function createSetStoreCommandHandler( } export function createStoreCommandHandler( - _ctx: CommandContext, + ctx: CommandContext, ): CommandHandlerFunction< StoreCommandData, StoreCommandError, StoreCommandArguments > { return async function storeCommandHandler(_args) { - console.warn("command not recognized") + ctx.logger.warn("command not recognized") return { success: false, error: { @@ -184,7 +186,7 @@ export async function selectStoreById( } } - store.set("store", parsedResultData) + storeUserStore(store, parsedResultData) return { success: true, @@ -235,7 +237,7 @@ export async function storeSelectPrompt( } } - store.set("store", answers.store) + storeUserStore(store, answers.store) return { success: true, diff --git a/packages/composable-cli/src/commands/ui/error/error.tsx b/packages/composable-cli/src/commands/ui/error/error.tsx new file mode 100644 index 00000000..3a4deb30 --- /dev/null +++ b/packages/composable-cli/src/commands/ui/error/error.tsx @@ -0,0 +1,29 @@ +import React from "react" +import { Box, Newline, Text } from "ink" +import Table from "ink-table" + +export function ErrorTable({ data }: { data: Record }) { + const errors = Object.keys(data).map((key) => { + return { key, value: data[key] } + }) + + return ( + + + + There was an issue! + + + + To get support on this issue, report it on our slack community. + + Join us at + + {" "} + https://elasticpathcommunity.slack.com/join/shared_invite/zt-1upzq3nlc-O3sy1bT0UJYcOWEQQCtnqw + + + + + ) +} diff --git a/packages/composable-cli/src/commands/ui/login/welcome-note.tsx b/packages/composable-cli/src/commands/ui/login/welcome-note.tsx index e449ecb3..d4bccbb0 100644 --- a/packages/composable-cli/src/commands/ui/login/welcome-note.tsx +++ b/packages/composable-cli/src/commands/ui/login/welcome-note.tsx @@ -21,6 +21,16 @@ export function WelcomeNote({ name }: { name: string }) { A CLI for managing your Elasticpath powered storefront + + + + To get support or ask any question, join us in our slack community. + + + https://elasticpathcommunity.slack.com/join/shared_invite/zt-1upzq3nlc-O3sy1bT0UJYcOWEQQCtnqw + + + ) diff --git a/packages/composable-cli/src/lib/authentication/ep-client-middleware.ts b/packages/composable-cli/src/lib/authentication/ep-client-middleware.ts index 9dc85d11..86a04ff6 100644 --- a/packages/composable-cli/src/lib/authentication/ep-client-middleware.ts +++ b/packages/composable-cli/src/lib/authentication/ep-client-middleware.ts @@ -3,7 +3,12 @@ import { MiddlewareFunction } from "yargs" import { getCredentials, getToken } from "./get-token" import { gateway, MemoryStorageFactory, Moltin } from "@moltin/sdk" import Conf from "conf" -import { getRegion, resolveHostFromRegion } from "../../util/resolve-region" +import { + getRegion, + resolveHostFromRegion, + resolveHostNameFromRegion, +} from "../../util/resolve-region" +import { logging } from "@angular-devkit/core" export function createEpClientMiddleware( ctx: CommandContext, @@ -14,27 +19,36 @@ export function createEpClientMiddleware( const credentialsResult = getCredentials(store) if (credentialsResult.success) { - ctx.epClient = createEpccClient(store) + ctx.epClient = createEpccClient(store, ctx.logger) } return } } -function createEpccClient(store: Conf): Moltin { +function createEpccClient(store: Conf, logger: logging.Logger): Moltin { + const regionResult = getRegion(store) + if (!regionResult.success) { + logger.error("No region found - ep client custom authenticator") + throw new Error( + "No region found - ep client custom authenticator - are you authenticated? - `composable-cli login`", + ) + } + + const hostname = resolveHostNameFromRegion(regionResult.data) + const resolvedRegion = resolveHostFromRegion(regionResult.data) + return gateway({ + host: hostname, custom_authenticator: async () => { - const regionResult = getRegion(store) - if (!regionResult.success) { - throw new Error("No region found - ep client custom authenticator") - } - - const resolvedRegion = resolveHostFromRegion(regionResult.data) await getToken(resolvedRegion, store) const credentialsResult = getCredentials(store) if (!credentialsResult.success) { + logger.debug( + `Credentials not found in store: ${credentialsResult.error.message} - ep client custom authenticator`, + ) throw new Error( `Credentials not found in store: ${credentialsResult.error.message} - ep client custom authenticator`, ) @@ -42,7 +56,12 @@ function createEpccClient(store: Conf): Moltin { return credentialsResult.data }, - custom_fetch: fetch, + custom_fetch: (url: URL | RequestInfo, init?: RequestInit) => { + logger.debug("\nEP SDK Client Fetch") + logger.debug(JSON.stringify(url, null, 2)) + logger.debug(JSON.stringify(init, null, 2)) + return fetch(url, init) + }, reauth: false, disableCart: true, storage: new MemoryStorageFactory(), diff --git a/packages/composable-cli/src/lib/authentication/get-token.ts b/packages/composable-cli/src/lib/authentication/get-token.ts index c44afd34..59d06865 100644 --- a/packages/composable-cli/src/lib/authentication/get-token.ts +++ b/packages/composable-cli/src/lib/authentication/get-token.ts @@ -12,9 +12,16 @@ import { checkIsErrorResponse, resolveEPCCErrorMessage, } from "../../util/epcc-error" -import { handleClearCredentials } from "../../util/conf-store/store-credentials" +import { + handleClearCredentials, + storeCredentials, + storeUserStore, +} from "../../util/conf-store/store-credentials" import { getStore } from "../stores/get-store" -import { userSwitchStoreResponseSchema } from "../stores/switch-store-schema" +import { + UserSwitchStoreResponse, + userSwitchStoreResponseSchema, +} from "../stores/switch-store-schema" import { encodeObjectToQueryString } from "../../util/encode-object-to-query-str" export function getCredentials(store: Conf): Result { @@ -50,19 +57,20 @@ export async function getToken( } } - const { expires, expires_in, refresh_token, access_token } = - credentialsResult.data + const { expires, refresh_token, access_token } = credentialsResult.data if ( hasExpiredWithThreshold( expires, - expires_in, 300, // 5 minutes ) ) { return handleExpiredToken(store, apiUrl, refresh_token) } + // Switch EP store if there is an active store + await switchStoreIfActive(store, apiUrl, credentialsResult.data.access_token) + return { success: true, data: access_token, @@ -78,13 +86,6 @@ async function handleExpiredToken( const renewedToken = await renewToken(apiUrl, refresh_token) - if (process.env.NODE_ENV === "development") { - console.log( - "CALL WAS MADE TO RENEW TOKEN DID YOU EXPECT THIS? ", - renewedToken, - ) - } - if (!renewedToken.success) { handleClearCredentials(store) return { @@ -94,7 +95,7 @@ async function handleExpiredToken( } // Set credentials in conf store - store.set("credentials", renewedToken.data) + storeCredentials(store, renewedToken.data) // Switch EP store if there is an active store await switchStoreIfActive(store, apiUrl, renewedToken.data.access_token) @@ -115,7 +116,7 @@ async function switchStoreIfActive(store: Conf, apiUrl: string, token: string) { ) if (switchStoreResponse.success) { - store.set("store", switchStoreResponse.data) + storeUserStore(store, activeStoreResult.data) } } else { store.delete("store") @@ -161,7 +162,7 @@ export async function switchUserStore( apiUrl: string, token: string, storeId: string, -): Promise> { +): Promise> { const switchResult = await postSwitchUserStore(apiUrl, token, storeId) const parsedResult = userSwitchStoreResponseSchema.safeParse(switchResult) @@ -175,7 +176,7 @@ export async function switchUserStore( return { success: true, - data: {}, + data: parsedResult.data, } } diff --git a/packages/composable-cli/src/lib/catalog/build-catalog-prompts.ts b/packages/composable-cli/src/lib/catalog/build-catalog-prompts.ts index 61898240..62e2503f 100644 --- a/packages/composable-cli/src/lib/catalog/build-catalog-prompts.ts +++ b/packages/composable-cli/src/lib/catalog/build-catalog-prompts.ts @@ -6,9 +6,9 @@ import { } from "../../util/epcc-error" import { EpccRequester } from "../../util/command" -export async function buildCatalogPrompts( +export async function getActiveStoreCatalogs( requester: EpccRequester, -): Promise> { +): Promise> { const catalogsResponse = await fetchStoreCatalogs(requester) const parsedResponse = storeCatalogsResponseSchema.safeParse(catalogsResponse) @@ -33,7 +33,16 @@ export async function buildCatalogPrompts( return { success: true, - data: mapCatalogsToStorePrompts(parsedResultData.data), + data: parsedResultData.data, + } +} + +export async function buildCatalogPrompts( + catalogs: StoreCatalog[], +): Promise> { + return { + success: true, + data: mapCatalogsToStorePrompts(catalogs), } } diff --git a/packages/composable-cli/src/lib/composable-project-middleware.ts b/packages/composable-cli/src/lib/composable-project-middleware.ts new file mode 100644 index 00000000..a02eba78 --- /dev/null +++ b/packages/composable-cli/src/lib/composable-project-middleware.ts @@ -0,0 +1,21 @@ +import yargs, { MiddlewareFunction } from "yargs" +import { CommandContext } from "../types/command" +import boxen from "boxen" + +export function createComposableProjectMiddleware( + ctx: CommandContext, +): MiddlewareFunction { + return async function composableProjectMiddleware( + _args: yargs.ArgumentsCamelCase<{}>, + ) { + if (!ctx.composableRc) { + ctx.logger.info( + boxen( + "Failed to find .composablerc, is this a Composable Frontend workspace?\nThis command expects to be run in a composable frontend workspace and may not work as expected. It could be you're not inside your project folder.", + { padding: 1, margin: 1, borderColor: "yellow" }, + ), + ) + } + return + } +} diff --git a/packages/composable-cli/src/lib/config-middleware.ts b/packages/composable-cli/src/lib/config-middleware.ts index 500811ac..aea0f0cf 100644 --- a/packages/composable-cli/src/lib/config-middleware.ts +++ b/packages/composable-cli/src/lib/config-middleware.ts @@ -4,6 +4,7 @@ import { promises } from "node:fs" import { composableRcSchema } from "./composable-rc-schema" import findUp from "find-up" import path from "path" +import { processUnknownError } from "../util/process-unknown-error" export function createConfigMiddleware( ctx: CommandContext, @@ -16,6 +17,7 @@ export function createConfigMiddleware( if (!configPath) { ctx.logger.debug("No .composablerc file found") + ctx.workspaceRoot = process.cwd() return } @@ -43,19 +45,3 @@ export function createConfigMiddleware( } } } - -function processUnknownError(error: unknown): string { - let errorMessage = "An unknown error occurred" - - if (error instanceof Error) { - if (error.message) { - errorMessage += `: ${error.message}` - } - - if (error.stack) { - errorMessage += `\nStack Trace:\n${error.stack}` - } - } - - return errorMessage -} diff --git a/packages/composable-cli/src/lib/devkit/add-env-variables.ts b/packages/composable-cli/src/lib/devkit/add-env-variables.ts new file mode 100644 index 00000000..7dff0620 --- /dev/null +++ b/packages/composable-cli/src/lib/devkit/add-env-variables.ts @@ -0,0 +1,116 @@ +import { + callRule, + HostTree, + Rule, + SchematicContext, + Tree, +} from "@angular-devkit/schematics" +import { commitTree, createScopedHost } from "./tree-util" +import { CommandContext } from "../../types/command" +import ora from "ora" +import { Result } from "../../types/results" + +export async function addToEnvFile( + workspaceRoot: string, + filepath: string, + variables: Record, +): Promise { + const host = createScopedHost(workspaceRoot) + + const initialTree = new HostTree(host) + + if (!initialTree.exists(filepath)) { + initialTree.create(filepath, "") + } + + const context = {} as unknown as SchematicContext + + const rule = addEnvVariables(variables, filepath) + + const tree = await callRule(rule, initialTree, context).toPromise() + + await commitTree(host, tree) +} + +export function addEnvVariables( + envVars: Record, + path: string, +): Rule { + return (host: Tree) => { + const sourceText = host.readText(path) + const envData = parseEnv(sourceText) + + const updatedEnvData = { ...envData, ...envVars } + + host.overwrite(path, stringifyEnvFile(updatedEnvData)) + + return host + } +} + +export type EnvData = Record + +function stringifyEnvFile(envData: EnvData): string { + let result = "" + for (const [key, value] of Object.entries(envData)) { + if (key) { + const line = `${key}=${String(value)}` + result += line + "\n" + } + } + return result +} + +export function parseEnv(src: string): EnvData { + const result: EnvData = {} + const lines = src.toString().split("\n") + for (const line of lines) { + const match = line.match(/^([^=:#]+?)[=:](.*)/) + if (match) { + const key = match[1].trim() + result[key] = match[2].trim().replace(/['"]+/g, "") + } + } + return result +} + +export async function attemptToAddEnvVariables( + ctx: CommandContext, + spinner: ora.Ora, + variables: Record, +): Promise> { + const { workspaceRoot, composableRc } = ctx + + if (!composableRc) { + return { + success: false, + error: { + code: "NO_COMPOSABLE_RC", + message: "Could not detect workspace root - missing composable.rc file", + }, + } + } + + spinner.start(`Adding environment variables to .env.local file...`) + + if (!workspaceRoot) { + spinner.fail( + `Failed to add environment variables to .env.local file - missing workspace root`, + ) + return { + success: false, + error: { + code: "FAILED_TO_ADD_ENV_VARS", + message: "Failed to add env variables to .env.local file", + }, + } + } + await addToEnvFile(workspaceRoot, ".env.local", variables) + + spinner.succeed(`Added environment variables to .env.local file.`) + + return { + success: true, + data: {}, + } +} diff --git a/packages/composable-cli/src/lib/devkit/tree-util.ts b/packages/composable-cli/src/lib/devkit/tree-util.ts new file mode 100644 index 00000000..758d8b46 --- /dev/null +++ b/packages/composable-cli/src/lib/devkit/tree-util.ts @@ -0,0 +1,19 @@ +import { normalize, virtualFs } from "@angular-devkit/core" +import { Stats } from "fs" +import { HostSink, Tree } from "@angular-devkit/schematics" +import { NodeJsSyncHost } from "@angular-devkit/core/node" + +export async function commitTree( + host: virtualFs.Host, + tree: Tree, + { force }: { force?: boolean } = {}, +): Promise { + const sink = new HostSink(host, force) + await sink.commit(tree).toPromise() +} + +export function createScopedHost(workspaceRoot: string) { + const root = normalize(workspaceRoot) + + return new virtualFs.ScopedHost(new NodeJsSyncHost(), root) +} diff --git a/packages/composable-cli/src/lib/stores/switch-store-schema.ts b/packages/composable-cli/src/lib/stores/switch-store-schema.ts index e1c46cad..1f4004de 100644 --- a/packages/composable-cli/src/lib/stores/switch-store-schema.ts +++ b/packages/composable-cli/src/lib/stores/switch-store-schema.ts @@ -12,3 +12,7 @@ export const userSwitchStoreResponseSchema = z.union([ userSwitchStoreSuccessResponseSchema, epccErrorResponseSchema, ]) + +export type UserSwitchStoreResponse = z.infer< + typeof userSwitchStoreResponseSchema +> diff --git a/packages/composable-cli/src/types/command.ts b/packages/composable-cli/src/types/command.ts index 1c2c222b..53258e52 100644 --- a/packages/composable-cli/src/types/command.ts +++ b/packages/composable-cli/src/types/command.ts @@ -8,6 +8,7 @@ import { logging } from "@angular-devkit/core" import ansiColors from "ansi-colors" import { Moltin } from "@moltin/sdk" import { ComposableRc } from "../lib/composable-rc-schema" +import { ErrorHandler } from "../util/error-handler" export type CommandResult = | { @@ -34,6 +35,7 @@ export type CommandContext = { epClient?: Moltin composableRc?: ComposableRc workspaceRoot?: string + handleErrors: ErrorHandler } export type RootCommandArguments = { diff --git a/packages/composable-cli/src/util/command.ts b/packages/composable-cli/src/util/command.ts index e851e722..e2a45aa5 100644 --- a/packages/composable-cli/src/util/command.ts +++ b/packages/composable-cli/src/util/command.ts @@ -8,6 +8,10 @@ import path from "path" import ws from "ws" import { createConsoleLogger, ProcessOutput } from "@angular-devkit/core/node" import * as ansiColors from "ansi-colors" +import { makeErrorWrapper } from "./error-handler" +import { renderInk } from "../lib/ink/render-ink" +import React from "react" +import { ErrorTable } from "../commands/ui/error/error" // polyfill fetch & websocket const globalAny = global as any @@ -77,9 +81,45 @@ export function createCommandContext({ stderr: resolvedStderr, logger: defaultLogger, colors, + handleErrors: makeErrorWrapper( + (err) => { + if (err instanceof Error) { + console.error(err.name) + console.error(err.message) + console.error(err.stack) + console.error(err.cause) + return Promise.resolve() + } + console.error("There was an unexpected error!") + return Promise.resolve() + }, + async (result) => { + if (!result.success) { + if (result.error instanceof Error) { + console.error(`Error Code: ${result.error.name}`) + console.error(`Error Message: ${result.error.message}`) + console.error(`Error Stack: ${result.error.stack}`) + console.error(`Error Cause: ${result.error.cause}`) + } else if (isRecordStringAny(result.error)) { + await renderInk( + React.createElement(ErrorTable, { data: result.error }), + ) + } else { + console.warn( + "Error was not an known error type! Could not parse a useful error message.", + ) + } + } + }, + defaultLogger, + ), } } +function isRecordStringAny(obj: unknown): obj is Record { + return typeof obj === "object" && obj !== null +} + function createRequester(store: Conf): EpccRequester { return async function requester( url: RequestInfo, diff --git a/packages/composable-cli/src/util/conf-store/store-credentials.ts b/packages/composable-cli/src/util/conf-store/store-credentials.ts index 1691f20b..bc8539f9 100644 --- a/packages/composable-cli/src/util/conf-store/store-credentials.ts +++ b/packages/composable-cli/src/util/conf-store/store-credentials.ts @@ -1,10 +1,9 @@ import Conf from "conf" import { UserProfile } from "../../lib/epcc-user-profile-schema" +import { Credentials } from "../../lib/authentication/credentials-schema" +import { UserStore } from "../../lib/stores/stores-schema" -export function storeCredentials( - store: Conf, - credentials: { accessToken: string; refreshToken: string; expires: number } -) { +export function storeCredentials(store: Conf, credentials: Credentials) { return store.set("credentials", credentials) } @@ -18,3 +17,7 @@ export function handleClearCredentials(store: Conf): void { export function storeUserProfile(store: Conf, userProfile: UserProfile) { return store.set("profile", userProfile) } + +export function storeUserStore(store: Conf, userStore: UserStore) { + return store.set("store", userStore) +} diff --git a/packages/composable-cli/src/util/error-handler.ts b/packages/composable-cli/src/util/error-handler.ts index 57dc4945..d7184868 100644 --- a/packages/composable-cli/src/util/error-handler.ts +++ b/packages/composable-cli/src/util/error-handler.ts @@ -1,23 +1,34 @@ -const makeErrorWrapper = - (errorHandler: (err: unknown) => T) => - (fn: (...a: A) => Promise) => +import { logging } from "@angular-devkit/core" +import { Result } from "../types/results" + +export type ErrorHandler = >( + fn: (...a: A) => Promise, +) => (...a: A) => Promise + +export const makeErrorWrapper = + >( + errorHandler: (err: unknown, logger: logging.Logger) => T, + resultHandler: (result: R) => Promise | void, + logger: logging.Logger, + ) => + (fn: (...a: A) => Promise) => async (...a: A): Promise => { try { const result = await fn(...a) if (isResultError(result)) { - return Promise.reject(result.error) + await errorHandler(result.error, logger) } - + await resultHandler(result) return Promise.resolve() } catch (err) { - await errorHandler(err) + await errorHandler(err, logger) return Promise.resolve() } } function isResultError( - result: any + result: any, ): result is { success: false; error: unknown } { return ( !!result && @@ -26,15 +37,3 @@ function isResultError( !result.success ) } - -export const handleErrors = makeErrorWrapper((err) => { - if (err instanceof Error) { - console.error(err.name) - console.error(err.message) - console.error(err.stack) - console.error(err.cause) - return Promise.resolve() - } - console.error("There was an unexpected error!") - return Promise.resolve() -}) diff --git a/packages/composable-cli/src/util/has-expired.test.ts b/packages/composable-cli/src/util/has-expired.test.ts index ef9f0351..5e2767f8 100644 --- a/packages/composable-cli/src/util/has-expired.test.ts +++ b/packages/composable-cli/src/util/has-expired.test.ts @@ -4,9 +4,8 @@ describe("hasExpiredWithThreshold", () => { // Test case 1: Token has expired it("hasExpiredWithThreshold returns true when the token has expired", () => { const unixTimestamp = 1695056417 - const expiresIn = 3600 const threshold = 300 - const result = hasExpiredWithThreshold(unixTimestamp, expiresIn, threshold) + const result = hasExpiredWithThreshold(unixTimestamp, threshold) expect(result).toBe(true) }) @@ -14,9 +13,8 @@ describe("hasExpiredWithThreshold", () => { it("hasExpiredWithThreshold returns false when the token is still valid within the threshold", () => { const currentTimestamp = Math.floor(Date.now() / 1000) const unixTimestamp = currentTimestamp + 1800 // Expires in 30 minutes - const expiresIn = 3600 - const threshold = 3600 // 1 hour threshold - const result = hasExpiredWithThreshold(unixTimestamp, expiresIn, threshold) + const threshold = 300 // 1 hour threshold + const result = hasExpiredWithThreshold(unixTimestamp, threshold) expect(result).toBe(false) }) @@ -24,9 +22,8 @@ describe("hasExpiredWithThreshold", () => { it("hasExpiredWithThreshold returns false when the token is still valid and outside the threshold", () => { const currentTimestamp = Math.floor(Date.now() / 1000) const unixTimestamp = currentTimestamp + 3600 // Expires in 1 hour - const expiresIn = 7200 // Expires in 2 hours const threshold = 1800 // 30 minutes threshold - const result = hasExpiredWithThreshold(unixTimestamp, expiresIn, threshold) + const result = hasExpiredWithThreshold(unixTimestamp, threshold) expect(result).toBe(false) }) }) diff --git a/packages/composable-cli/src/util/has-expired.ts b/packages/composable-cli/src/util/has-expired.ts index 406d7a96..19654172 100644 --- a/packages/composable-cli/src/util/has-expired.ts +++ b/packages/composable-cli/src/util/has-expired.ts @@ -1,15 +1,12 @@ /** * - * @param timestamp in unix time - * @param expiresIn in seconds + * @param expiresTimestamp in unix time * @param threshold in seconds */ export function hasExpiredWithThreshold( - timestamp: number, - expiresIn: number, - threshold: number + expiresTimestamp: number, + threshold: number, ): boolean { const currentTimestamp = Math.floor(Date.now() / 1000) // Convert current time to Unix timestamp - const expirationTimestamp = timestamp + expiresIn - return expirationTimestamp - threshold <= currentTimestamp + return expiresTimestamp - threshold <= currentTimestamp } diff --git a/packages/composable-cli/src/util/process-unknown-error.ts b/packages/composable-cli/src/util/process-unknown-error.ts new file mode 100644 index 00000000..bfdda13d --- /dev/null +++ b/packages/composable-cli/src/util/process-unknown-error.ts @@ -0,0 +1,15 @@ +export function processUnknownError(error: unknown): string { + let errorMessage = "An unknown error occurred" + + if (error instanceof Error) { + if (error.message) { + errorMessage += `: ${error.message}` + } + + if (error.stack) { + errorMessage += `\nStack Trace:\n${error.stack}` + } + } + + return errorMessage +} diff --git a/packages/composable-common/src/payment-gateways/check-gateway.ts b/packages/composable-common/src/payment-gateways/check-gateway.ts index 6e0e6274..306d95cc 100644 --- a/packages/composable-common/src/payment-gateways/check-gateway.ts +++ b/packages/composable-common/src/payment-gateways/check-gateway.ts @@ -3,7 +3,7 @@ import type { OperationResult } from "../types" export async function checkGateway( client: EpccClient, - gatewaySlug: string + gatewaySlug: string, ): Promise> { const gateways = await client.Gateways.All() const epPaymentGateway = gateways.data.find((x) => x.slug === gatewaySlug) diff --git a/packages/composable-common/src/payment-gateways/ep-payments/setup-epcc-ep-payment.ts b/packages/composable-common/src/payment-gateways/ep-payments/setup-epcc-ep-payment.ts index 185f3eae..9ac0a9cc 100644 --- a/packages/composable-common/src/payment-gateways/ep-payments/setup-epcc-ep-payment.ts +++ b/packages/composable-common/src/payment-gateways/ep-payments/setup-epcc-ep-payment.ts @@ -3,12 +3,11 @@ import type { EpPaymentGatewaySettings } from "./ep-payments-schema" import type { Gateway } from "@moltin/sdk" import { OperationResult } from "../../types" import { createEpccClient } from "../../integration-hub" -import { checkGateway } from "../check-gateway" import { updateEpPaymentGateway } from "./update-gateway" export async function setupEPPaymentsPaymentGateway( sourceInput: Omit, - logger: logging.LoggerApi + logger: logging.LoggerApi, ): Promise> { try { const { @@ -25,25 +24,12 @@ export async function setupEPPaymentsPaymentGateway( client_secret: epccClientSecret, }) - /** - * Check if EP payments is enabled and do nothing if it is - */ - const checkGatewayResult = await checkGateway( - epccClient, - "elastic_path_payments_stripe" - ) - - if (checkGatewayResult.success) { - logger.debug(`EP Payment gateway is already enabled`) - return checkGatewayResult - } - /** * Update ep payment gateway to be enabled with test mode on */ const updateResult = await updateEpPaymentGateway( epccClient, - epPaymentsStripeAccountId + epPaymentsStripeAccountId, ) if (!updateResult.success) { diff --git a/yarn.lock b/yarn.lock index 013b9965..f197c0c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11130,6 +11130,13 @@ ink-link@2: prop-types "^15.7.2" terminal-link "^2.1.1" +ink-table@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ink-table/-/ink-table-3.0.0.tgz#109fb2ce0709567f0e38d14b2b82f311277a3628" + integrity sha512-RtcYjenHKZWjnwVNQ6zSYWMOLKwkWscDAJsqUQXftyjkYho1gGrluGss87NOoIzss0IKr74lKasd6MtlQYALiA== + dependencies: + object-hash "^2.0.3" + ink@*: version "4.4.1" resolved "https://registry.yarnpkg.com/ink/-/ink-4.4.1.tgz#ae684a141e92524af3eccf740c38f03618b48028" @@ -14211,6 +14218,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.0.3: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-hash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"