From 17032a4f47e795e41370f41a71dcafd9270d72e3 Mon Sep 17 00:00:00 2001 From: Mateusz Kolasa Date: Mon, 7 Oct 2024 17:39:52 +0200 Subject: [PATCH] feat: revert quick buy to the OPF codebase --- core-libs/setup/tsconfig.spec.json | 15 + feature-libs/asm/tsconfig.schematics.json | 15 + feature-libs/cart/tsconfig.schematics.json | 15 + .../checkout/tsconfig.schematics.json | 15 + .../tsconfig.schematics.json | 15 + .../tsconfig.schematics.json | 15 + feature-libs/order/tsconfig.schematics.json | 15 + .../organization/tsconfig.schematics.json | 15 + .../pdf-invoices/tsconfig.schematics.json | 15 + .../pickup-in-store/tsconfig.schematics.json | 15 + .../tsconfig.schematics.json | 15 + .../tsconfig.schematics.json | 15 + feature-libs/product/tsconfig.schematics.json | 15 + .../qualtrics/tsconfig.schematics.json | 15 + feature-libs/quote/tsconfig.schematics.json | 15 + .../tsconfig.schematics.json | 15 + .../smartedit/tsconfig.schematics.json | 15 + .../storefinder/tsconfig.schematics.json | 15 + .../tracking/tsconfig.schematics.json | 15 + feature-libs/user/tsconfig.schematics.json | 15 + integration-libs/cdc/tsconfig.schematics.json | 15 + integration-libs/cdp/tsconfig.schematics.json | 15 + integration-libs/cds/tsconfig.schematics.json | 15 + .../cpq-quote/tsconfig.schematics.json | 15 + .../digital-payments/tsconfig.schematics.json | 15 + .../tsconfig.schematics.json | 15 + integration-libs/omf/tsconfig.schematics.json | 15 + integration-libs/opf/_index.scss | 6 +- .../opf/base/root/opf-base-root.module.ts | 11 +- .../opf/base/root/services/index.ts | 1 + .../opf-global-message.service.spec.ts | 1 + .../services/opf-global-message.service.ts | 65 ++ .../opf-global-functions.service.spec.ts | 7 +- integration-libs/opf/package.json | 1 + .../payment/root/model/opf-payment.model.ts | 2 - .../opf/quick-buy/components/ng-package.json | 6 + .../apple-pay-session.factory.spec.ts | 216 +++++ .../apple-pay-session.factory.ts | 83 ++ .../apple-pay/apple-pay-session/index.ts | 7 + .../apple-pay/apple-pay.component.html | 3 + .../apple-pay/apple-pay.component.spec.ts | 172 ++++ .../apple-pay/apple-pay.component.ts | 85 ++ .../apple-pay/apple-pay.module.ts | 17 + .../apple-pay/apple-pay.service.spec.ts | 170 ++++ .../apple-pay/apple-pay.service.ts | 450 ++++++++++ .../opf-quick-buy-buttons/apple-pay/index.ts | 9 + .../apple-pay-observable.factory.spec.ts | 500 ++++++++++++ .../apple-pay-observable.factory.ts | 149 ++++ .../apple-pay/observable/index.ts | 7 + .../google-pay/google-pay.component.html | 7 + .../google-pay/google-pay.component.spec.ts | 91 +++ .../google-pay/google-pay.component.ts | 50 ++ .../google-pay/google-pay.module.ts | 18 + .../google-pay/google-pay.service.spec.ts | 772 ++++++++++++++++++ .../google-pay/google-pay.service.ts | 496 +++++++++++ .../opf-quick-buy-buttons/google-pay/index.ts | 9 + .../components/opf-quick-buy-buttons/index.ts | 9 + .../opf-quick-buy-buttons.component.html | 16 + .../opf-quick-buy-buttons.component.spec.ts | 93 +++ .../opf-quick-buy-buttons.component.ts | 46 ++ .../opf-quick-buy-buttons.module.ts | 30 + .../opf-quick-buy-buttons.service.spec.ts | 267 ++++++ .../opf-quick-buy-buttons.service.ts | 85 ++ .../opf-quick-buy-components.module.ts | 13 + .../opf/quick-buy/components/public_api.ts | 7 + .../opf/quick-buy/core/connectors/index.ts | 8 + .../core/connectors/opf-quick-buy.adapter.ts | 21 + .../opf-quick-buy.connector.spec.ts | 74 ++ .../connectors/opf-quick-buy.connector.ts | 25 + .../quick-buy/core/facade/facade-providers.ts | 17 + .../opf/quick-buy/core/facade/index.ts | 7 + .../core/facade/opf-quick-buy.service.spec.ts | 110 +++ .../core/facade/opf-quick-buy.service.ts | 90 ++ .../opf/quick-buy/core/ng-package.json | 6 + .../core/opf-quick-buy-core.module.ts | 15 + .../opf/quick-buy/core/public_api.ts | 11 + .../opf/quick-buy/core/services/index.ts | 7 + .../opf-quick-buy-transaction.service.spec.ts | 471 +++++++++++ .../opf-quick-buy-transaction.service.ts | 177 ++++ .../opf/quick-buy/core/tokens/index.ts | 7 + .../opf/quick-buy/core/tokens/tokens.ts | 13 + .../opf/quick-buy/ng-package.json | 6 + .../opf/quick-buy/opf-api/adapters/index.ts | 7 + .../opf-api-quick-buy.adapter.spec.ts | 7 + .../adapters/opf-api-quick-buy.adapter.ts | 88 ++ .../default-opf-api-quick-buy-config.ts | 17 + .../opf/quick-buy/opf-api/model/index.ts | 7 + .../opf-api-quick-buy-endpoints.model.ts | 16 + .../opf/quick-buy/opf-api/ng-package.json | 6 + .../opf-api/opf-api-quick-buy.module.ts | 24 + .../opf/quick-buy/opf-api/public_api.ts | 8 + .../opf/quick-buy/opf-quick-buy.module.ts | 19 + integration-libs/opf/quick-buy/public_api.ts | 7 + .../opf/quick-buy/root/facade/index.ts | 7 + .../root/facade/opf-quick-buy.facade.ts | 34 + .../opf/quick-buy/root/feature-name.ts | 7 + .../model/augmented-opf-quick-buy.model.ts | 15 + .../opf/quick-buy/root/model/constants.ts | 8 + .../opf/quick-buy/root/model/index.ts | 10 + .../root/model/opf-apple-pay.model.ts | 64 ++ .../root/model/opf-quick-buy.model.ts | 52 ++ .../opf/quick-buy/root/ng-package.json | 6 + .../root/opf-quick-buy-root.module.ts | 27 + .../opf/quick-buy/root/public_api.ts | 10 + .../opf/quick-buy/styles/_index.scss | 1 + .../quick-buy/styles/components/_index.scss | 3 + .../styles/components/_opf-apple-pay.scss | 11 + .../styles/components/_opf-google-pay.scss | 14 + .../components/_opf-quick-buy-buttons.scss | 23 + integration-libs/opf/tsconfig.schematics.json | 15 + .../opps/tsconfig.schematics.json | 15 + .../s4-service/tsconfig.schematics.json | 15 + .../s4om/tsconfig.schematics.json | 15 + .../segment-refs/tsconfig.schematics.json | 15 + package-lock.json | 22 + package.json | 4 +- projects/schematics/src/dependencies.json | 3 + .../features/opf/opf-feature.module.ts | 9 + projects/storefrontapp/tsconfig.app.prod.json | 5 + projects/storefrontapp/tsconfig.server.json | 15 + .../storefrontapp/tsconfig.server.prod.json | 7 + tsconfig.compodoc.json | 17 +- tsconfig.json | 17 +- 123 files changed, 6108 insertions(+), 11 deletions(-) create mode 100644 integration-libs/opf/base/root/services/opf-global-message.service.spec.ts create mode 100644 integration-libs/opf/base/root/services/opf-global-message.service.ts create mode 100644 integration-libs/opf/quick-buy/components/ng-package.json create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/apple-pay-session.factory.spec.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/apple-pay-session.factory.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/index.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.html create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.spec.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.module.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.service.spec.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.service.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/index.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/apple-pay-observable.factory.spec.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/apple-pay-observable.factory.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/index.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.html create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.spec.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.module.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.service.spec.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.service.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/index.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/index.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.html create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.spec.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.module.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.service.spec.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.service.ts create mode 100644 integration-libs/opf/quick-buy/components/opf-quick-buy-components.module.ts create mode 100644 integration-libs/opf/quick-buy/components/public_api.ts create mode 100644 integration-libs/opf/quick-buy/core/connectors/index.ts create mode 100644 integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.adapter.ts create mode 100644 integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.connector.spec.ts create mode 100644 integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.connector.ts create mode 100644 integration-libs/opf/quick-buy/core/facade/facade-providers.ts create mode 100644 integration-libs/opf/quick-buy/core/facade/index.ts create mode 100644 integration-libs/opf/quick-buy/core/facade/opf-quick-buy.service.spec.ts create mode 100644 integration-libs/opf/quick-buy/core/facade/opf-quick-buy.service.ts create mode 100644 integration-libs/opf/quick-buy/core/ng-package.json create mode 100644 integration-libs/opf/quick-buy/core/opf-quick-buy-core.module.ts create mode 100644 integration-libs/opf/quick-buy/core/public_api.ts create mode 100644 integration-libs/opf/quick-buy/core/services/index.ts create mode 100644 integration-libs/opf/quick-buy/core/services/opf-quick-buy-transaction.service.spec.ts create mode 100644 integration-libs/opf/quick-buy/core/services/opf-quick-buy-transaction.service.ts create mode 100644 integration-libs/opf/quick-buy/core/tokens/index.ts create mode 100644 integration-libs/opf/quick-buy/core/tokens/tokens.ts create mode 100644 integration-libs/opf/quick-buy/ng-package.json create mode 100644 integration-libs/opf/quick-buy/opf-api/adapters/index.ts create mode 100644 integration-libs/opf/quick-buy/opf-api/adapters/opf-api-quick-buy.adapter.spec.ts create mode 100644 integration-libs/opf/quick-buy/opf-api/adapters/opf-api-quick-buy.adapter.ts create mode 100644 integration-libs/opf/quick-buy/opf-api/config/default-opf-api-quick-buy-config.ts create mode 100644 integration-libs/opf/quick-buy/opf-api/model/index.ts create mode 100644 integration-libs/opf/quick-buy/opf-api/model/opf-api-quick-buy-endpoints.model.ts create mode 100644 integration-libs/opf/quick-buy/opf-api/ng-package.json create mode 100644 integration-libs/opf/quick-buy/opf-api/opf-api-quick-buy.module.ts create mode 100644 integration-libs/opf/quick-buy/opf-api/public_api.ts create mode 100644 integration-libs/opf/quick-buy/opf-quick-buy.module.ts create mode 100644 integration-libs/opf/quick-buy/public_api.ts create mode 100644 integration-libs/opf/quick-buy/root/facade/index.ts create mode 100644 integration-libs/opf/quick-buy/root/facade/opf-quick-buy.facade.ts create mode 100644 integration-libs/opf/quick-buy/root/feature-name.ts create mode 100644 integration-libs/opf/quick-buy/root/model/augmented-opf-quick-buy.model.ts create mode 100644 integration-libs/opf/quick-buy/root/model/constants.ts create mode 100644 integration-libs/opf/quick-buy/root/model/index.ts create mode 100644 integration-libs/opf/quick-buy/root/model/opf-apple-pay.model.ts create mode 100644 integration-libs/opf/quick-buy/root/model/opf-quick-buy.model.ts create mode 100644 integration-libs/opf/quick-buy/root/ng-package.json create mode 100644 integration-libs/opf/quick-buy/root/opf-quick-buy-root.module.ts create mode 100644 integration-libs/opf/quick-buy/root/public_api.ts create mode 100644 integration-libs/opf/quick-buy/styles/_index.scss create mode 100644 integration-libs/opf/quick-buy/styles/components/_index.scss create mode 100644 integration-libs/opf/quick-buy/styles/components/_opf-apple-pay.scss create mode 100644 integration-libs/opf/quick-buy/styles/components/_opf-google-pay.scss create mode 100644 integration-libs/opf/quick-buy/styles/components/_opf-quick-buy-buttons.scss diff --git a/core-libs/setup/tsconfig.spec.json b/core-libs/setup/tsconfig.spec.json index cc54aa28f71..88548e99c31 100644 --- a/core-libs/setup/tsconfig.spec.json +++ b/core-libs/setup/tsconfig.spec.json @@ -678,6 +678,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/asm/tsconfig.schematics.json b/feature-libs/asm/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/asm/tsconfig.schematics.json +++ b/feature-libs/asm/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/cart/tsconfig.schematics.json b/feature-libs/cart/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/cart/tsconfig.schematics.json +++ b/feature-libs/cart/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/checkout/tsconfig.schematics.json b/feature-libs/checkout/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/checkout/tsconfig.schematics.json +++ b/feature-libs/checkout/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/customer-ticketing/tsconfig.schematics.json b/feature-libs/customer-ticketing/tsconfig.schematics.json index 51b5e52256a..2ca78695831 100644 --- a/feature-libs/customer-ticketing/tsconfig.schematics.json +++ b/feature-libs/customer-ticketing/tsconfig.schematics.json @@ -688,6 +688,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/estimated-delivery-date/tsconfig.schematics.json b/feature-libs/estimated-delivery-date/tsconfig.schematics.json index 51b5e52256a..2ca78695831 100644 --- a/feature-libs/estimated-delivery-date/tsconfig.schematics.json +++ b/feature-libs/estimated-delivery-date/tsconfig.schematics.json @@ -688,6 +688,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/order/tsconfig.schematics.json b/feature-libs/order/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/order/tsconfig.schematics.json +++ b/feature-libs/order/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/organization/tsconfig.schematics.json b/feature-libs/organization/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/organization/tsconfig.schematics.json +++ b/feature-libs/organization/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/pdf-invoices/tsconfig.schematics.json b/feature-libs/pdf-invoices/tsconfig.schematics.json index 51b5e52256a..2ca78695831 100644 --- a/feature-libs/pdf-invoices/tsconfig.schematics.json +++ b/feature-libs/pdf-invoices/tsconfig.schematics.json @@ -688,6 +688,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/pickup-in-store/tsconfig.schematics.json b/feature-libs/pickup-in-store/tsconfig.schematics.json index b99e67bab37..710df6b10a2 100644 --- a/feature-libs/pickup-in-store/tsconfig.schematics.json +++ b/feature-libs/pickup-in-store/tsconfig.schematics.json @@ -691,6 +691,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/product-configurator/tsconfig.schematics.json b/feature-libs/product-configurator/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/product-configurator/tsconfig.schematics.json +++ b/feature-libs/product-configurator/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/product-multi-dimensional/tsconfig.schematics.json b/feature-libs/product-multi-dimensional/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/product-multi-dimensional/tsconfig.schematics.json +++ b/feature-libs/product-multi-dimensional/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/product/tsconfig.schematics.json b/feature-libs/product/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/product/tsconfig.schematics.json +++ b/feature-libs/product/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/qualtrics/tsconfig.schematics.json b/feature-libs/qualtrics/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/qualtrics/tsconfig.schematics.json +++ b/feature-libs/qualtrics/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/quote/tsconfig.schematics.json b/feature-libs/quote/tsconfig.schematics.json index b99e67bab37..710df6b10a2 100644 --- a/feature-libs/quote/tsconfig.schematics.json +++ b/feature-libs/quote/tsconfig.schematics.json @@ -691,6 +691,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/requested-delivery-date/tsconfig.schematics.json b/feature-libs/requested-delivery-date/tsconfig.schematics.json index 51b5e52256a..2ca78695831 100644 --- a/feature-libs/requested-delivery-date/tsconfig.schematics.json +++ b/feature-libs/requested-delivery-date/tsconfig.schematics.json @@ -688,6 +688,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/smartedit/tsconfig.schematics.json b/feature-libs/smartedit/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/smartedit/tsconfig.schematics.json +++ b/feature-libs/smartedit/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/storefinder/tsconfig.schematics.json b/feature-libs/storefinder/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/storefinder/tsconfig.schematics.json +++ b/feature-libs/storefinder/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/tracking/tsconfig.schematics.json b/feature-libs/tracking/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/tracking/tsconfig.schematics.json +++ b/feature-libs/tracking/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/feature-libs/user/tsconfig.schematics.json b/feature-libs/user/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/feature-libs/user/tsconfig.schematics.json +++ b/feature-libs/user/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/cdc/tsconfig.schematics.json b/integration-libs/cdc/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/integration-libs/cdc/tsconfig.schematics.json +++ b/integration-libs/cdc/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/cdp/tsconfig.schematics.json b/integration-libs/cdp/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/integration-libs/cdp/tsconfig.schematics.json +++ b/integration-libs/cdp/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/cds/tsconfig.schematics.json b/integration-libs/cds/tsconfig.schematics.json index d19e21b34ac..6b1dcb79d55 100644 --- a/integration-libs/cds/tsconfig.schematics.json +++ b/integration-libs/cds/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/cpq-quote/tsconfig.schematics.json b/integration-libs/cpq-quote/tsconfig.schematics.json index 51b5e52256a..2ca78695831 100644 --- a/integration-libs/cpq-quote/tsconfig.schematics.json +++ b/integration-libs/cpq-quote/tsconfig.schematics.json @@ -688,6 +688,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/digital-payments/tsconfig.schematics.json b/integration-libs/digital-payments/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/integration-libs/digital-payments/tsconfig.schematics.json +++ b/integration-libs/digital-payments/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/epd-visualization/tsconfig.schematics.json b/integration-libs/epd-visualization/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/integration-libs/epd-visualization/tsconfig.schematics.json +++ b/integration-libs/epd-visualization/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/omf/tsconfig.schematics.json b/integration-libs/omf/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/integration-libs/omf/tsconfig.schematics.json +++ b/integration-libs/omf/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/opf/_index.scss b/integration-libs/opf/_index.scss index c94d32af805..ca35415047b 100644 --- a/integration-libs/opf/_index.scss +++ b/integration-libs/opf/_index.scss @@ -3,13 +3,17 @@ @import 'bootstrap/scss/functions'; @import 'bootstrap/scss/variables'; @import 'bootstrap/scss/_mixins'; +@import './base/styles/index'; @import './checkout/styles/index'; +@import './cta/styles/index'; +@import './quick-buy/styles/index'; @import './base/styles/index'; $opf-components-allowlist: cx-opf-checkout-payment-and-review, cx-opf-checkout-payments, cx-opf-checkout-billing-address-form, cx-opf-checkout-payment-wrapper, cx-opf-checkout-terms-and-conditions-alert, - cx-opf-error-modal, cx-opf-cta-element !default; + cx-opf-error-modal, cx-opf-cta-element, cx-opf-google-pay, cx-opf-apple-pay, + cx-opf-quick-buy-buttons !default; $skipComponentStyles: () !default; diff --git a/integration-libs/opf/base/root/opf-base-root.module.ts b/integration-libs/opf/base/root/opf-base-root.module.ts index c88fa02fb6e..db9dfca3387 100644 --- a/integration-libs/opf/base/root/opf-base-root.module.ts +++ b/integration-libs/opf/base/root/opf-base-root.module.ts @@ -5,10 +5,13 @@ */ import { APP_INITIALIZER, inject, NgModule } from '@angular/core'; -import { provideDefaultConfig } from '@spartacus/core'; +import { GlobalMessageService, provideDefaultConfig } from '@spartacus/core'; import { defaultOpfConfig } from './config/default-opf-config'; import { OpfEventModule } from './events/opf-event.module'; -import { OpfMetadataStatePersistanceService } from './services'; +import { + OpfGlobalMessageService, + OpfMetadataStatePersistanceService, +} from './services'; export function opfStatePersistenceFactory(): () => void { const opfStatePersistenceService = inject(OpfMetadataStatePersistanceService); @@ -22,6 +25,10 @@ export function opfStatePersistenceFactory(): () => void { useFactory: opfStatePersistenceFactory, multi: true, }, + { + provide: GlobalMessageService, + useExisting: OpfGlobalMessageService, + }, provideDefaultConfig(defaultOpfConfig), ], }) diff --git a/integration-libs/opf/base/root/services/index.ts b/integration-libs/opf/base/root/services/index.ts index e823f3cc6b1..ee67629ce76 100644 --- a/integration-libs/opf/base/root/services/index.ts +++ b/integration-libs/opf/base/root/services/index.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +export * from './opf-global-message.service'; export * from './opf-metadata-state-persistence.service'; export * from './opf-metadata-store.service'; export * from './opf-resource-loader.service'; diff --git a/integration-libs/opf/base/root/services/opf-global-message.service.spec.ts b/integration-libs/opf/base/root/services/opf-global-message.service.spec.ts new file mode 100644 index 00000000000..2854b34d38e --- /dev/null +++ b/integration-libs/opf/base/root/services/opf-global-message.service.spec.ts @@ -0,0 +1 @@ +// to be done diff --git a/integration-libs/opf/base/root/services/opf-global-message.service.ts b/integration-libs/opf/base/root/services/opf-global-message.service.ts new file mode 100644 index 00000000000..606641ce4e4 --- /dev/null +++ b/integration-libs/opf/base/root/services/opf-global-message.service.ts @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { + GlobalMessageService, + GlobalMessageType, + StateWithGlobalMessage, + Translatable, +} from '@spartacus/core'; +import { timer } from 'rxjs'; +import { take } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root', +}) +export class OpfGlobalMessageService extends GlobalMessageService { + protected isGlobalMessageDisabled = false; + protected disabledKeys: string[] = []; + protected defaultTimeout = 2000; + constructor(protected store: Store) { + super(store); + } + /** + * Add one message into store + * @param text: string | Translatable + * @param type: GlobalMessageType object + * @param timeout: number + */ + add( + text: string | Translatable, + type: GlobalMessageType, + timeout?: number + ): void { + if ( + this.isGlobalMessageDisabled && + this.disabledKeys?.length && + (text as Translatable)?.key && + this.disabledKeys.includes((text as Translatable).key as string) + ) { + return; + } + super.add(text, type, timeout); + } + + /** + * disable specific keys for a period of time + * @param keys: string[] + * @param timeout: number + */ + disableGlobalMessage(keys: string[], timeout?: number): void { + this.isGlobalMessageDisabled = true; + this.disabledKeys = keys; + timer(timeout ?? this.defaultTimeout) + .pipe(take(1)) + .subscribe(() => { + this.isGlobalMessageDisabled = false; + this.disabledKeys = []; + }); + } +} diff --git a/integration-libs/opf/global-functions/core/facade/opf-global-functions.service.spec.ts b/integration-libs/opf/global-functions/core/facade/opf-global-functions.service.spec.ts index 24c40ff9aaa..8d0ea60bc50 100644 --- a/integration-libs/opf/global-functions/core/facade/opf-global-functions.service.spec.ts +++ b/integration-libs/opf/global-functions/core/facade/opf-global-functions.service.spec.ts @@ -14,7 +14,8 @@ import { TestBed } from '@angular/core/testing'; import { WindowRef } from '@spartacus/core'; import { defaultErrorDialogOptions } from '@spartacus/opf/base/root'; import { GlobalFunctionsDomain } from '@spartacus/opf/global-functions/root'; -import { OpfPaymentFacade, PaymentMethod } from '@spartacus/opf/payment/root'; +import { OpfPaymentFacade } from '@spartacus/opf/payment/root'; +import { OpfProviderType } from '@spartacus/opf/quick-buy/root'; import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; import { EMPTY, Observable, of } from 'rxjs'; import { OpfGlobalFunctionsService } from './opf-global-functions.service'; @@ -119,7 +120,7 @@ describe('OpfGlobalFunctionsService', () => { submitSuccess, submitPending, submitFailure, - paymentMethod: PaymentMethod.APPLE_PAY, + paymentMethod: OpfProviderType.APPLE_PAY, }); expect(opfPaymentFacadeMock.submitPayment).toHaveBeenCalled(); }); @@ -145,7 +146,7 @@ describe('OpfGlobalFunctionsService', () => { submitSuccess, submitPending, submitFailure, - paymentMethod: PaymentMethod.APPLE_PAY, + paymentMethod: OpfProviderType.APPLE_PAY, }); expect(opfPaymentFacadeMock.submitCompletePayment).toHaveBeenCalled(); }); diff --git a/integration-libs/opf/package.json b/integration-libs/opf/package.json index 78e474a8df7..e1aad97a5f7 100644 --- a/integration-libs/opf/package.json +++ b/integration-libs/opf/package.json @@ -32,6 +32,7 @@ "@angular/platform-browser": "^17.0.5", "@angular/router": "^17.0.5", "@ng-select/ng-select": "^12.0.4", + "@ngrx/store": "^17.0.1", "@spartacus/cart": "2211.29.0-2", "@spartacus/checkout": "2211.29.0-2", "@spartacus/core": "2211.29.0-2", diff --git a/integration-libs/opf/payment/root/model/opf-payment.model.ts b/integration-libs/opf/payment/root/model/opf-payment.model.ts index ac4806b8028..49d438c5171 100644 --- a/integration-libs/opf/payment/root/model/opf-payment.model.ts +++ b/integration-libs/opf/payment/root/model/opf-payment.model.ts @@ -85,9 +85,7 @@ export enum SubmitStatus { DELAYED = 'DELAYED', } export enum PaymentMethod { - APPLE_PAY = 'APPLE_PAY', CREDIT_CARD = 'CREDIT_CARD', - GOOGLE_PAY = 'GOOGLE_PAY', } export interface SubmitResponse { cartId: string; diff --git a/integration-libs/opf/quick-buy/components/ng-package.json b/integration-libs/opf/quick-buy/components/ng-package.json new file mode 100644 index 00000000000..2a9c6221b0e --- /dev/null +++ b/integration-libs/opf/quick-buy/components/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/apple-pay-session.factory.spec.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/apple-pay-session.factory.spec.ts new file mode 100644 index 00000000000..f9a4e26eb46 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/apple-pay-session.factory.spec.ts @@ -0,0 +1,216 @@ +/* + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { WindowRef } from '@spartacus/core'; +import { ApplePaySessionFactory } from './apple-pay-session.factory'; + +class MockApplePaySession + extends EventTarget + implements Partial +{ + static readonly STATUS_SUCCESS: number = 2; + + static readonly STATUS_FAILURE: number = 3; + + oncancel: (event: ApplePayJS.Event) => void; + + onpaymentauthorized: ( + event: ApplePayJS.ApplePayPaymentAuthorizedEvent + ) => void; + + onpaymentmethodselected: ( + event: ApplePayJS.ApplePayPaymentMethodSelectedEvent + ) => void; + + onshippingcontactselected: ( + event: ApplePayJS.ApplePayShippingContactSelectedEvent + ) => void; + + onshippingmethodselected: ( + event: ApplePayJS.ApplePayShippingMethodSelectedEvent + ) => void; + + onvalidatemerchant: (event: ApplePayJS.ApplePayValidateMerchantEvent) => void; + + _stubConstructorArguments: Array; + + constructor( + version: number, + paymentRequest: ApplePayJS.ApplePayPaymentRequest + ) { + super(); + this._stubConstructorArguments = [version, paymentRequest]; + } + + static canMakePayments(): boolean { + return true; + } + + static canMakePaymentsWithActiveCard( + _merchantIdentifier: string + ): Promise { + return Promise.resolve(true); + } + + static openPaymentSetup(_merchantIdentifier: string): Promise { + return Promise.resolve(true); + } + + static supportsVersion(_dialogCloseversion: number): boolean { + return true; + } + + abort(): void {} + + begin(): void {} + + completeMerchantValidation(_eventmerchantSession: any): void {} + + completePayment( + _result: number | ApplePayJS.ApplePayPaymentAuthorizationResult + ): void {} + + completePaymentMethodSelection( + _newTotal: ApplePayJS.ApplePayLineItem, + _newLineItems: Array + ): void; + completePaymentMethodSelection( + _update: ApplePayJS.ApplePayPaymentMethodUpdate + ): void; + completePaymentMethodSelection(_newTotal: any, _newLineItems?: any): void {} + + completeShippingContactSelection( + _status: number, + _newShippingMethods: Array, + _newTotal: ApplePayJS.ApplePayLineItem, + _newLineItems: Array + ): void; + completeShippingContactSelection( + _update: ApplePayJS.ApplePayShippingContactUpdate + ): void; + completeShippingContactSelection( + _status: any, + _newShippingMethods?: any, + _newTotal?: any, + _newLineItems?: any + ): void {} + + completeShippingMethodSelection( + status: number, + newTotal: ApplePayJS.ApplePayLineItem, + newLineItems: Array + ): void; + completeShippingMethodSelection( + update: ApplePayJS.ApplePayShippingMethodUpdate + ): void; + completeShippingMethodSelection( + _status: any, + _newTotal?: any, + _openElementnewLineItems?: any + ): void {} +} +@Injectable() +class ApplePaySessionFactoryExt extends ApplePaySessionFactory { + setIsDeviceSupported(newValue: boolean) { + this.isDeviceSupported = newValue; + } +} + +describe('ApplePaySessionFactory', () => { + let applePaySessionFactory: ApplePaySessionFactoryExt; + let windowRef: WindowRef; + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: WindowRef, + useValue: { nativeWindow: { ApplePaySession: MockApplePaySession } }, + }, + ApplePaySessionFactoryExt, + ], + }); + + applePaySessionFactory = TestBed.inject(ApplePaySessionFactoryExt); + windowRef = TestBed.inject(WindowRef); + }); + + it('should be created', () => { + expect(applePaySessionFactory).toBeTruthy(); + }); + + it('should create ApplePaySession if available', () => { + const applePaySession = applePaySessionFactory['createApplePaySession'](); + expect(applePaySession).toBeDefined(); + }); + + it('should not create ApplePaySession if not available', () => { + (windowRef as any).nativeWindow['ApplePaySession'] = null; + const applePaySession = applePaySessionFactory['createApplePaySession'](); + expect(applePaySession).not.toBeDefined(); + }); + + it('should return STATUS_SUCCESS for statusSuccess when device is supported', () => { + applePaySessionFactory.setIsDeviceSupported(true); + expect(applePaySessionFactory.statusSuccess).toEqual( + MockApplePaySession.STATUS_SUCCESS + ); + }); + + it('should return 1 for statusSuccess when device is not supported', () => { + applePaySessionFactory.setIsDeviceSupported(false); + expect(applePaySessionFactory.statusSuccess).toEqual(1); + }); + + it('should return STATUS_FAILURE for statusFailure when device is supported', () => { + applePaySessionFactory.setIsDeviceSupported(true); + expect(applePaySessionFactory.statusFailure).toEqual( + MockApplePaySession.STATUS_FAILURE + ); + }); + + it('should return 1 for statusFailure when device is not supported', () => { + applePaySessionFactory.setIsDeviceSupported(false); + expect(applePaySessionFactory.statusFailure).toEqual(1); + }); + + it('should return true for isApplePaySupported$ when device is supported', (done: DoneFn) => { + applePaySessionFactory.setIsDeviceSupported(true); + const merchantId = 'merchantId'; + applePaySessionFactory + .isApplePaySupported$(merchantId) + .subscribe((result) => { + expect(result).toEqual(true); + done(); + }); + }); + it('should return false for isApplePaySupported$ when device is not supported', (done: DoneFn) => { + applePaySessionFactory.setIsDeviceSupported(false); + const merchantId = 'merchantId'; + applePaySessionFactory + .isApplePaySupported$(merchantId) + .subscribe((result) => { + expect(result).toEqual(false); + done(); + }); + }); + + it('should return ApplePaySession when device is supported', () => { + applePaySessionFactory.setIsDeviceSupported(true); + const startSession = applePaySessionFactory.startApplePaySession( + {} as ApplePayJS.ApplePayPaymentRequest + ); + expect(startSession).not.toEqual(undefined); + }); + + it('should not return ApplePaySession when device is not supported', () => { + applePaySessionFactory.setIsDeviceSupported(false); + const startSession = applePaySessionFactory.startApplePaySession( + {} as ApplePayJS.ApplePayPaymentRequest + ); + expect(startSession).toEqual(undefined); + }); +}); diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/apple-pay-session.factory.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/apple-pay-session.factory.ts new file mode 100644 index 00000000000..32088e1c73f --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/apple-pay-session.factory.ts @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +import { Injectable, inject } from '@angular/core'; +import { WindowRef } from '@spartacus/core'; +import { Observable, from, of } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplePaySessionFactory { + protected winRef = inject(WindowRef); + protected isDeviceSupported = false; + private applePaySession: typeof ApplePaySession; + protected applePayApiVersion = 3; + + constructor() { + // @ts-ignore + this.applePaySession = this.createApplePaySession() as ApplePaySession; + if (this.applePaySession) { + this.isDeviceSupported = this.applePaySession.canMakePayments(); + } + } + + private createApplePaySession(): ApplePaySession | undefined { + const window = this.winRef.nativeWindow as any; + if (!window['ApplePaySession']) { + return undefined; + } + return window['ApplePaySession'] as ApplePaySession; + } + + get statusSuccess(): number { + return this.isDeviceSupported ? this.applePaySession.STATUS_SUCCESS : 1; + } + + get statusFailure(): number { + return this.isDeviceSupported ? this.applePaySession.STATUS_FAILURE : 1; + } + + isApplePaySupported$(merchantIdentifier: string): Observable { + return this.isDeviceSupported && + this.supportsVersion(this.applePayApiVersion) + ? this.canMakePaymentsWithActiveCard(merchantIdentifier) + : of(false); + } + + protected supportsVersion(version: number): boolean { + try { + return ( + this.isDeviceSupported && this.applePaySession.supportsVersion(version) + ); + } catch (err) { + return false; + } + } + + protected canMakePayments(): boolean { + try { + return this.isDeviceSupported && this.applePaySession.canMakePayments(); + } catch (err) { + return false; + } + } + + protected canMakePaymentsWithActiveCard( + merchantId: string + ): Observable { + return this.isDeviceSupported + ? from(this.applePaySession.canMakePaymentsWithActiveCard(merchantId)) + : of(false); + } + + startApplePaySession(paymentRequest: any): any { + return this.isDeviceSupported + ? new this.applePaySession(this.applePayApiVersion, paymentRequest) + : undefined; + } +} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/index.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/index.ts new file mode 100644 index 00000000000..fe475c2d8c8 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay-session/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './apple-pay-session.factory'; diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.html b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.html new file mode 100644 index 00000000000..89e019cd8cd --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.html @@ -0,0 +1,3 @@ + +
+
diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.spec.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.spec.ts new file mode 100644 index 00000000000..02c7600384b --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.spec.ts @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Cart } from '@spartacus/cart/base/root'; +import { Product } from '@spartacus/core'; +import { ActiveConfiguration } from '@spartacus/opf/base/root'; +import { OpfPaymentErrorHandlerService } from '@spartacus/opf/payment/core'; +import { OpfQuickBuyTransactionService } from '@spartacus/opf/quick-buy/core'; +import { + OpfProviderType, + OpfQuickBuyDigitalWallet, + OpfQuickBuyLocation, +} from '@spartacus/opf/quick-buy/root'; +import { CurrentProductService } from '@spartacus/storefront'; +import { of } from 'rxjs'; +import { ApplePaySessionFactory } from './apple-pay-session'; +import { ApplePayComponent } from './apple-pay.component'; +import { ApplePayService } from './apple-pay.service'; + +const mockProduct: Product = { + name: 'mockProduct', + code: 'code1', + stock: { + stockLevel: 333, + stockLevelStatus: 'inStock', + }, +}; + +const mockCart: Cart = { + code: '123', +}; + +const mockActiveConfiguration: ActiveConfiguration = { + digitalWalletQuickBuy: [ + { + merchantId: 'merchant.com.adyen.upscale.test', + provider: OpfProviderType.APPLE_PAY, + countryCode: 'US', + }, + { merchantId: 'merchant.test.example' }, + ], +}; + +describe('ApplePayComponent', () => { + let component: ApplePayComponent; + let fixture: ComponentFixture; + let mockApplePayService: jasmine.SpyObj; + let mockCurrentProductService: jasmine.SpyObj; + let mockApplePaySessionFactory: jasmine.SpyObj; + let mockOpfPaymentErrorHandlerService: jasmine.SpyObj; + let mockOpfQuickBuyTransactionService: jasmine.SpyObj; + const mockCountryCode = 'US'; + + beforeEach(() => { + mockApplePayService = jasmine.createSpyObj('ApplePayService', ['start']); + mockCurrentProductService = jasmine.createSpyObj('CurrentProductService', [ + 'getProduct', + ]); + mockApplePaySessionFactory = jasmine.createSpyObj( + 'ApplePaySessionFactory', + ['isApplePaySupported$'] + ); + mockOpfPaymentErrorHandlerService = jasmine.createSpyObj( + 'OpfPaymentErrorHandlerService', + ['handlePaymentError'] + ); + mockOpfQuickBuyTransactionService = jasmine.createSpyObj( + 'OpfQuickBuyTransactionService', + ['getTransactionLocationContext', 'checkStableCart', 'getCurrentCart'] + ); + + TestBed.configureTestingModule({ + declarations: [ApplePayComponent], + providers: [ + { provide: ApplePayService, useValue: mockApplePayService }, + { provide: CurrentProductService, useValue: mockCurrentProductService }, + { + provide: ApplePaySessionFactory, + useValue: mockApplePaySessionFactory, + }, + { + provide: OpfPaymentErrorHandlerService, + useValue: mockOpfPaymentErrorHandlerService, + }, + { + provide: OpfQuickBuyTransactionService, + useValue: mockOpfQuickBuyTransactionService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ApplePayComponent); + component = fixture.componentInstance; + + const mockProductObservable = of(mockProduct); + const mockCartObservable = of(mockCart); + + mockCurrentProductService.getProduct.and.returnValue(mockProductObservable); + mockOpfQuickBuyTransactionService.getTransactionLocationContext.and.returnValue( + of(OpfQuickBuyLocation.PRODUCT) + ); + mockOpfQuickBuyTransactionService.getCurrentCart.and.returnValue( + mockCartObservable + ); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize isApplePaySupported$ provider is Apple pay', () => { + const digitalWallet: OpfQuickBuyDigitalWallet = { + provider: OpfProviderType.APPLE_PAY, + countryCode: mockCountryCode, + merchantId: 'merchant.com.adyen.upscale.test', + }; + component.activeConfiguration = { digitalWalletQuickBuy: [digitalWallet] }; + + const mockObservable = of(true); + mockApplePaySessionFactory.isApplePaySupported$.and.returnValue( + mockObservable + ); + + fixture.detectChanges(); + expect(component.isApplePaySupported$).toBe(mockObservable); + }); + + it('should not initialize isApplePaySupported$ provider is not Apple pay', () => { + const digitalWallet: OpfQuickBuyDigitalWallet = { + provider: OpfProviderType.GOOGLE_PAY, + countryCode: mockCountryCode, + merchantId: 'merchant.com.adyen.upscale.test', + }; + component.activeConfiguration = { digitalWalletQuickBuy: [digitalWallet] }; + + const mockObservable = of(true); + mockApplePaySessionFactory.isApplePaySupported$.and.returnValue( + mockObservable + ); + + fixture.detectChanges(); + expect( + mockApplePaySessionFactory.isApplePaySupported$ + ).not.toHaveBeenCalled(); + }); + + it('should start applePayService', () => { + mockApplePayService.start.and.returnValue( + of({ status: 1 }) + ); + component.activeConfiguration = { + digitalWalletQuickBuy: [ + { + provider: OpfProviderType.APPLE_PAY, + countryCode: mockCountryCode, + merchantId: 'merchant.com.adyen.upscale.test', + }, + ], + }; + component.activeConfiguration = mockActiveConfiguration; + fixture.detectChanges(); + + component.initTransaction(); + expect( + mockOpfPaymentErrorHandlerService.handlePaymentError + ).not.toHaveBeenCalled(); + }); +}); diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.ts new file mode 100644 index 00000000000..968cdc769b2 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.component.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + Component, + Input, + OnInit, + inject, +} from '@angular/core'; +import { Cart } from '@spartacus/cart/base/root'; +import { ActiveConfiguration } from '@spartacus/opf/base/root'; +import { OpfQuickBuyTransactionService } from '@spartacus/opf/quick-buy/core'; +import { + OpfProviderType, + OpfQuickBuyDigitalWallet, +} from '@spartacus/opf/quick-buy/root'; +import { CurrentProductService } from '@spartacus/storefront'; +import { Observable } from 'rxjs'; +import { switchMap, take } from 'rxjs/operators'; +import { ApplePaySessionFactory } from './apple-pay-session'; +import { ApplePayService } from './apple-pay.service'; + +@Component({ + selector: 'cx-opf-apple-pay', + templateUrl: './apple-pay.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ApplePayComponent implements OnInit { + @Input() activeConfiguration: ActiveConfiguration; + + protected applePayService = inject(ApplePayService); + protected currentProductService = inject(CurrentProductService); + protected opfQuickBuyTransactionService = inject( + OpfQuickBuyTransactionService + ); + protected applePaySession = inject(ApplePaySessionFactory); + + isApplePaySupported$: Observable; + applePayDigitalWallet?: OpfQuickBuyDigitalWallet; + + ngOnInit(): void { + this.applePayDigitalWallet = + this.activeConfiguration?.digitalWalletQuickBuy?.find( + (digitalWallet) => digitalWallet.provider === OpfProviderType.APPLE_PAY + ); + if ( + !this.applePayDigitalWallet?.merchantId || + !this.applePayDigitalWallet?.countryCode + ) { + return; + } + + this.isApplePaySupported$ = this.applePaySession.isApplePaySupported$( + this.applePayDigitalWallet.merchantId + ); + } + + initActiveCartTransaction(): Observable { + return this.opfQuickBuyTransactionService.getCurrentCart().pipe( + take(1), + switchMap((cart: Cart) => { + return this.applePayService.start({ + cart: cart, + countryCode: this.applePayDigitalWallet?.countryCode as string, + }); + }) + ); + } + + initTransaction(): void { + this.opfQuickBuyTransactionService + .getTransactionLocationContext() + .pipe( + take(1), + switchMap(() => { + return this.initActiveCartTransaction(); + }) + ) + .subscribe(); + } +} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.module.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.module.ts new file mode 100644 index 00000000000..6453df610fd --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.module.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ApplePayComponent } from './apple-pay.component'; + +@NgModule({ + imports: [CommonModule], + declarations: [ApplePayComponent], + exports: [ApplePayComponent], +}) +export class OpfApplePayModule {} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.service.spec.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.service.spec.ts new file mode 100644 index 00000000000..9f174446000 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.service.spec.ts @@ -0,0 +1,170 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TestBed } from '@angular/core/testing'; +import { Product, WindowRef } from '@spartacus/core'; +import { OpfPaymentService } from '@spartacus/opf/payment/core'; +import { OpfPaymentFacade } from '@spartacus/opf/payment/root'; +import { + OpfQuickBuyService, + OpfQuickBuyTransactionService, +} from '@spartacus/opf/quick-buy/core'; +import { OpfQuickBuyDeliveryType } from '@spartacus/opf/quick-buy/root'; +import { Subject, of, throwError } from 'rxjs'; +import { OpfQuickBuyButtonsService } from '../opf-quick-buy-buttons.service'; +import { ApplePaySessionFactory } from './apple-pay-session/apple-pay-session.factory'; +import { ApplePayService } from './apple-pay.service'; +import { ApplePayObservableFactory } from './observable/apple-pay-observable.factory'; + +const mockProduct: Product = { + name: 'Product Name', + code: 'PRODUCT_CODE', + images: { + PRIMARY: { + thumbnail: { + url: 'url', + altText: 'alt', + }, + }, + }, + price: { + formattedValue: '$1.500', + value: 1.5, + }, + priceRange: { + maxPrice: { + formattedValue: '$1.500', + }, + minPrice: { + formattedValue: '$1.000', + }, + }, +}; + +const MockWindowRef = { + nativeWindow: { + location: { + hostname: 'testHost', + }, + }, +}; + +describe('ApplePayService', () => { + let service: ApplePayService; + let opfPaymentFacadeMock: jasmine.SpyObj; + let opfQuickBuyTransactionServiceMock: jasmine.SpyObj; + let applePayObservableFactoryMock: jasmine.SpyObj; + let applePaySessionFactoryMock: jasmine.SpyObj; + let applePayObservableTestController: Subject; + let opfQuickBuyButtonsServiceMock: jasmine.SpyObj; + let opfQuickBuyServiceMock: jasmine.SpyObj; + + beforeEach(() => { + opfQuickBuyButtonsServiceMock = jasmine.createSpyObj( + 'OpfQuickBuyButtonsService', + ['getQuickBuyProviderConfig'] + ); + + applePaySessionFactoryMock = jasmine.createSpyObj( + 'ApplePaySessionFactory', + ['startApplePaySession'] + ); + opfPaymentFacadeMock = jasmine.createSpyObj('OpfPaymentService', [ + 'submitPayment', + ]); + opfQuickBuyServiceMock = jasmine.createSpyObj('OpfQuickBuyService', [ + 'getApplePayWebSession', + ]); + applePayObservableFactoryMock = jasmine.createSpyObj( + 'ApplePayObservableFactory', + ['initApplePayEventsHandler'] + ); + opfQuickBuyTransactionServiceMock = jasmine.createSpyObj( + 'OpfQuickBuyTransactionService', + [ + 'deleteUserAddresses', + 'getTransactionLocationContext', + 'getTransactionDeliveryInfo', + 'getMerchantName', + 'checkStableCart', + 'getSupportedDeliveryModes', + 'setDeliveryAddress', + 'setBillingAddress', + 'getDeliveryAddress', + 'getCurrentCart', + 'getCurrentCartId', + 'getCurrentCartTotalPrice', + 'setDeliveryMode', + 'getSelectedDeliveryMode', + 'deleteUserAddresses', + ] + ); + + TestBed.configureTestingModule({ + providers: [ + ApplePayService, + { provide: OpfPaymentFacade, useValue: opfPaymentFacadeMock }, + { provide: WindowRef, useValue: MockWindowRef }, + { + provide: OpfQuickBuyTransactionService, + useValue: opfQuickBuyTransactionServiceMock, + }, + { + provide: ApplePayObservableFactory, + useValue: applePayObservableFactoryMock, + }, + { + provide: ApplePaySessionFactory, + useValue: applePaySessionFactoryMock, + }, + { + provide: OpfQuickBuyButtonsService, + useValue: opfQuickBuyButtonsServiceMock, + }, + { + provide: OpfQuickBuyService, + useValue: opfQuickBuyServiceMock, + }, + ], + }); + service = TestBed.inject(ApplePayService); + + applePayObservableTestController = new Subject(); + applePayObservableFactoryMock.initApplePayEventsHandler.and.returnValue( + applePayObservableTestController + ); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should handle errors during ApplePay session start', () => { + service = TestBed.inject(ApplePayService); + const merchantNameMock = 'Nakano'; + opfQuickBuyTransactionServiceMock.getTransactionDeliveryInfo.and.returnValue( + of({ + type: OpfQuickBuyDeliveryType.SHIPPING, + }) + ); + + applePayObservableFactoryMock.initApplePayEventsHandler.and.returnValue( + throwError('Error') + ); + + opfQuickBuyTransactionServiceMock.getMerchantName.and.returnValue( + of(merchantNameMock) + ); + + service + .start({ product: mockProduct, quantity: 1, countryCode: 'us' }) + .subscribe({ + error: (err: any) => { + expect(err).toBe('Error'); + }, + }); + }); +}); diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.service.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.service.ts new file mode 100644 index 00000000000..4a447489c2a --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/apple-pay.service.ts @@ -0,0 +1,450 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +import { Injectable, inject } from '@angular/core'; +import { Address, WindowRef } from '@spartacus/core'; +import { Observable, forkJoin, of, throwError } from 'rxjs'; +import { + catchError, + finalize, + map, + switchMap, + take, + tap, +} from 'rxjs/operators'; + +import { Cart, DeliveryMode } from '@spartacus/cart/base/root'; +import { OpfPaymentFacade } from '@spartacus/opf/payment/root'; +import { OpfQuickBuyTransactionService } from '@spartacus/opf/quick-buy/core'; +import { + ApplePaySessionVerificationRequest, + ApplePaySessionVerificationResponse, + ApplePayTransactionInput, + OPF_QUICK_BUY_DEFAULT_MERCHANT_NAME, + OpfProviderType, + OpfQuickBuyDeliveryType, + OpfQuickBuyFacade, + OpfQuickBuyLocation, + QuickBuyTransactionDetails, +} from '@spartacus/opf/quick-buy/root'; +import { ApplePaySessionFactory } from './apple-pay-session/apple-pay-session.factory'; +import { ApplePayObservableFactory } from './observable/apple-pay-observable.factory'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplePayService { + protected opfPaymentFacade = inject(OpfPaymentFacade); + protected applePaySession = inject(ApplePaySessionFactory); + protected applePayObservable = inject(ApplePayObservableFactory); + protected winRef = inject(WindowRef); + protected opfQuickBuyTransactionService = inject( + OpfQuickBuyTransactionService + ); + protected opfQuickBuyFacade = inject(OpfQuickBuyFacade); + protected paymentInProgress = false; + + protected initialTransactionDetails: QuickBuyTransactionDetails = { + context: OpfQuickBuyLocation.PRODUCT, + product: undefined, + cart: undefined, + quantity: 0, + addressIds: [], + total: { + label: OPF_QUICK_BUY_DEFAULT_MERCHANT_NAME, + amount: '', + currency: '', + }, + deliveryInfo: { + type: OpfQuickBuyDeliveryType.SHIPPING, + pickupDetails: undefined, + }, + }; + + protected transactionDetails = this.initialTransactionDetails; + + protected initTransactionDetails( + transactionInput: ApplePayTransactionInput + ): QuickBuyTransactionDetails { + this.transactionDetails = { + ...this.initialTransactionDetails, + addressIds: [], + }; + + if (transactionInput?.cart) { + this.transactionDetails = { + ...this.transactionDetails, + context: OpfQuickBuyLocation.CART, + cart: transactionInput.cart, + total: { + amount: `${transactionInput.cart.totalPrice?.value}`, + label: `${transactionInput.cart.code}`, + currency: transactionInput.cart?.totalPrice?.currencyIso as string, + }, + }; + } + + if (transactionInput?.product && transactionInput?.quantity) { + const productPrice = transactionInput.product.price?.value as number; + const totalPrice = productPrice * transactionInput.quantity; + + this.transactionDetails = { + ...this.transactionDetails, + context: OpfQuickBuyLocation.PRODUCT, + product: transactionInput.product, + quantity: transactionInput.quantity, + total: { + amount: totalPrice.toString(), + label: `${transactionInput.product?.name as string}${ + transactionInput.quantity > 1 + ? ` x ${transactionInput.quantity}` + : '' + }`, + currency: transactionInput.product?.price?.currencyIso as string, + }, + }; + } + + return this.transactionDetails; + } + + start(transactionInput: ApplePayTransactionInput): any { + if (this.paymentInProgress) { + return throwError(() => new Error('Apple Pay is already in progress')); + } + this.paymentInProgress = true; + + return this.setApplePayRequestConfig(transactionInput).pipe( + switchMap((request: ApplePayJS.ApplePayPaymentRequest) => { + return this.applePayObservable.initApplePayEventsHandler({ + request, + validateMerchant: (event) => this.handleValidation(event), + shippingContactSelected: (event) => + this.handleShippingContactSelected(event), + paymentMethodSelected: (event) => + this.handlePaymentMethodSelected(event), + shippingMethodSelected: (event) => + this.handleShippingMethodSelected(event), + paymentAuthorized: (event) => this.handlePaymentAuthorized(event), + }); + }), + take(1), + catchError((error) => throwError(() => error)), + finalize(() => { + this.deleteUserAddresses(); + this.paymentInProgress = false; + }) + ); + } + + protected deleteUserAddresses() { + if (this.transactionDetails.addressIds.length) { + this.opfQuickBuyTransactionService.deleteUserAddresses([ + ...this.transactionDetails.addressIds, + ]); + this.transactionDetails.addressIds = []; + } + } + + private handleValidation( + event: ApplePayJS.ApplePayValidateMerchantEvent + ): Observable { + return this.validateOpfAppleSession(event); + } + + protected setApplePayRequestConfig( + transactionInput: ApplePayTransactionInput + ): Observable { + this.transactionDetails = this.initTransactionDetails(transactionInput); + const countryCode = transactionInput?.countryCode || ''; + const initialRequest: ApplePayJS.ApplePayPaymentRequest = { + currencyCode: this.transactionDetails.total.currency, + total: { + amount: this.transactionDetails.total.amount, + label: this.transactionDetails.total.label, + }, + shippingMethods: [], + merchantCapabilities: ['supports3DS'], + supportedNetworks: ['visa', 'masterCard', 'amex', 'discover'], + requiredShippingContactFields: ['email', 'name', 'postalAddress'], + requiredBillingContactFields: ['email', 'name', 'postalAddress'], + countryCode, + }; + + return forkJoin({ + deliveryInfo: + this.opfQuickBuyTransactionService.getTransactionDeliveryInfo(), + merchantName: this.opfQuickBuyTransactionService.getMerchantName(), + }).pipe( + switchMap(({ deliveryInfo, merchantName }) => { + this.transactionDetails.total.label = merchantName; + initialRequest.total.label = merchantName; + this.transactionDetails.deliveryInfo = deliveryInfo; + + return of(undefined); + }), + map((opfQuickBuyDeliveryInfo) => { + if (!opfQuickBuyDeliveryInfo) { + return initialRequest; + } + this.transactionDetails.deliveryInfo = opfQuickBuyDeliveryInfo; + return initialRequest; + }) + ); + } + + private validateOpfAppleSession( + event: ApplePayJS.ApplePayValidateMerchantEvent + ): Observable { + return this.opfQuickBuyTransactionService.getCurrentCartId().pipe( + switchMap((cartId: string) => { + const verificationRequest: ApplePaySessionVerificationRequest = { + validationUrl: event.validationURL, + initiative: 'web', + initiativeContext: (this.winRef?.nativeWindow as Window).location + ?.hostname, + cartId, + }; + return this.verifyApplePaySession(verificationRequest); + }) + ); + } + + private convertAppleToOpfAddress( + addr: ApplePayJS.ApplePayPaymentContact, + partial = false + ): Address { + const ADDRESS_FIELD_PLACEHOLDER = '[FIELD_NOT_SET]'; + return { + firstName: partial ? ADDRESS_FIELD_PLACEHOLDER : addr?.givenName, + lastName: partial ? ADDRESS_FIELD_PLACEHOLDER : addr?.familyName, + line1: partial ? ADDRESS_FIELD_PLACEHOLDER : addr?.addressLines?.[0], + line2: addr?.addressLines?.[1], + email: addr?.emailAddress, + town: addr?.locality, + district: addr?.administrativeArea, + postalCode: addr?.postalCode, + phone: addr?.phoneNumber, + country: { + isocode: addr?.countryCode, + name: addr?.country, + }, + defaultAddress: false, + }; + } + + private handleShippingContactSelected( + _event: ApplePayJS.ApplePayShippingContactSelectedEvent + ): Observable { + const partialAddress: Address = this.convertAppleToOpfAddress( + _event.shippingContact, + true + ); + + const result: ApplePayJS.ApplePayShippingContactUpdate = + this.updateApplePayForm({ ...this.transactionDetails.total }); + + return this.opfQuickBuyTransactionService + .setDeliveryAddress(partialAddress) + .pipe( + tap((addrId: string) => { + this.recordDeliveryAddress(addrId); + }), + switchMap(() => + this.opfQuickBuyTransactionService.getSupportedDeliveryModes() + ), + take(1), + map((modes: DeliveryMode[]) => { + if (!modes.length) { + return of({ + ...result, + errors: [ + this.updateApplePayFormWithError( + 'No shipment methods available for this delivery address' + ), + ], + }); + } + const newShippingMethods = modes.map((mode) => { + return { + identifier: mode.code as string, + label: mode.name as string, + amount: (mode.deliveryCost?.value as number).toFixed(2), + detail: mode.description ?? (mode.name as string), + }; + }); + result.newShippingMethods = newShippingMethods; + return result; + }), + switchMap(() => { + return this.opfQuickBuyTransactionService.getCurrentCartTotalPrice(); + }), + switchMap((price: number | undefined) => { + if (!price) { + return throwError(() => new Error('Total Price not available')); + } + this.transactionDetails.total.amount = price.toString(); + result.newTotal.amount = price.toString(); + return of(result); + }) + ); + } + + private handlePaymentMethodSelected( + _event: ApplePayJS.ApplePayPaymentMethodSelectedEvent + ): Observable { + const result: ApplePayJS.ApplePayPaymentMethodUpdate = + this.updateApplePayForm({ ...this.transactionDetails.total }); + return of(result); + } + + private handleShippingMethodSelected( + _event: ApplePayJS.ApplePayShippingMethodSelectedEvent + ): Observable { + const result: ApplePayJS.ApplePayShippingContactUpdate = + this.updateApplePayForm({ ...this.transactionDetails.total }); + + return this.opfQuickBuyTransactionService + .setDeliveryMode(_event.shippingMethod.identifier) + .pipe( + switchMap(() => this.opfQuickBuyTransactionService.getCurrentCart()), + take(1), + switchMap((cart: Cart) => { + if (!cart?.totalPrice?.value) { + return throwError(() => new Error('Total Price not available')); + } + result.newTotal.amount = cart.totalPrice.value.toString(); + this.transactionDetails.total.amount = + cart.totalPrice.value.toString(); + return of(result); + }) + ); + } + + private handlePaymentAuthorized( + event: ApplePayJS.ApplePayPaymentAuthorizedEvent + ): Observable { + const result: ApplePayJS.ApplePayPaymentAuthorizationResult = { + status: this.applePaySession.statusSuccess, + }; + let orderSuccess: boolean; + return this.placeOrderAfterPayment(event.payment).pipe( + map((success) => { + orderSuccess = success; + return orderSuccess + ? result + : { ...result, status: this.applePaySession.statusFailure }; + }), + catchError((error) => { + return of({ + ...result, + status: this.applePaySession.statusFailure, + errors: [ + this.updateApplePayFormWithError(error?.message ?? 'Payment error'), + ], + } as ApplePayJS.ApplePayPaymentAuthorizationResult); + }) + ); + } + + private verifyApplePaySession( + request: ApplePaySessionVerificationRequest + ): Observable { + return this.opfQuickBuyFacade.getApplePayWebSession(request); + } + + protected recordDeliveryAddress(addrId: string): void { + if (!this.transactionDetails.addressIds?.includes(addrId)) { + this.transactionDetails.addressIds?.push(addrId); + } + } + + private placeOrderAfterPayment( + applePayPayment: ApplePayJS.ApplePayPayment + ): Observable { + if (!applePayPayment) { + return of(false); + } + const { shippingContact, billingContact } = applePayPayment; + if (!billingContact) { + throw new Error('Error: empty billingContact'); + } + if ( + this.transactionDetails.deliveryInfo?.type === + OpfQuickBuyDeliveryType.SHIPPING && + !shippingContact + ) { + throw new Error('Error: empty shippingContact'); + } + + const deliveryTypeHandlingObservable: Observable = + this.transactionDetails.deliveryInfo?.type === + OpfQuickBuyDeliveryType.PICKUP + ? this.opfQuickBuyTransactionService + .setDeliveryMode(OpfQuickBuyDeliveryType.PICKUP.toLocaleLowerCase()) + .pipe( + switchMap(() => { + return this.opfQuickBuyTransactionService.setBillingAddress( + this.convertAppleToOpfAddress(billingContact) + ); + }) + ) + : this.opfQuickBuyTransactionService + .setDeliveryAddress( + this.convertAppleToOpfAddress( + shippingContact as ApplePayJS.ApplePayPaymentContact + ) + ) + .pipe( + tap((addrId: string) => { + this.recordDeliveryAddress(addrId); + }), + switchMap(() => { + return this.opfQuickBuyTransactionService.setBillingAddress( + this.convertAppleToOpfAddress(billingContact) + ); + }) + ); + + return deliveryTypeHandlingObservable.pipe( + switchMap(() => this.opfQuickBuyTransactionService.getCurrentCartId()), + switchMap((cartId: string) => { + const encryptedToken = btoa( + JSON.stringify(applePayPayment.token.paymentData) + ); + + return this.opfPaymentFacade.submitPayment({ + additionalData: [], + paymentSessionId: '', + callbackArray: [() => {}, () => {}, () => {}], + paymentMethod: OpfProviderType.APPLE_PAY as any, + encryptedToken, + cartId, + }); + }) + ); + } + + protected updateApplePayForm(total: { amount: string; label: string }) { + return { + newTotal: { + amount: total.amount, + label: total.label, + }, + }; + } + + protected updateApplePayFormWithError( + message: string, + code = 'unknown' + ): { code: string; message: string } { + return { + code, + message, + }; + } +} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/index.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/index.ts new file mode 100644 index 00000000000..75d1549ec3b --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './apple-pay.component'; +export * from './apple-pay.module'; +export * from './apple-pay.service'; diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/apple-pay-observable.factory.spec.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/apple-pay-observable.factory.spec.ts new file mode 100644 index 00000000000..31da26259a3 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/apple-pay-observable.factory.spec.ts @@ -0,0 +1,500 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TestBed } from '@angular/core/testing'; +import { ApplePayObservableConfig } from '@spartacus/opf/quick-buy/root'; +import { Observable, of, throwError } from 'rxjs'; +import { ApplePaySessionFactory } from '../apple-pay-session/apple-pay-session.factory'; +import { ApplePayObservableFactory } from './apple-pay-observable.factory'; + +class MockEventTarget implements EventTarget { + _stubEventListeners: Array<{ type: string; listener: Function }> = []; + + /** Method to call registered events of specified type with provided arguments */ + _stubMockEvent(type: string, ...args: Array): void { + this._stubEventListeners.forEach((listener) => { + if (listener.type === type) { + listener.listener(...args); + } + }); + } + + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + _options?: boolean | AddEventListenerOptions + ): void { + this._stubEventListeners.push({ type, listener: listener as Function }); + } + + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + _options?: boolean | EventListenerOptions + ): void { + const index = this._stubEventListeners.findIndex( + (registeredListener) => + registeredListener.type === type && + registeredListener.listener === listener + ); + + if (index > -1) { + this._stubEventListeners.splice(index, 1); + } + } + + dispatchEvent(_evt: Event): boolean { + return true; + } +} + +class MockApplePaySession + extends MockEventTarget + implements Partial +{ + static readonly STATUS_SUCCESS: number; + + static readonly STATUS_FAILURE: number; + + oncancel: (event: ApplePayJS.Event) => void; + + onpaymentauthorized: ( + event: ApplePayJS.ApplePayPaymentAuthorizedEvent + ) => void; + + onpaymentmethodselected: ( + event: ApplePayJS.ApplePayPaymentMethodSelectedEvent + ) => void; + + onshippingcontactselected: ( + event: ApplePayJS.ApplePayShippingContactSelectedEvent + ) => void; + + onshippingmethodselected: ( + event: ApplePayJS.ApplePayShippingMethodSelectedEvent + ) => void; + + onvalidatemerchant: (event: ApplePayJS.ApplePayValidateMerchantEvent) => void; + + _stubConstructorArguments: Array; + + constructor( + version: number, + paymentRequest: ApplePayJS.ApplePayPaymentRequest + ) { + super(); + this._stubConstructorArguments = [version, paymentRequest]; + } + + static canMakePayments(): boolean { + return true; + } + + static canMakePaymentsWithActiveCard( + _merchantIdentifier: string + ): Promise { + return Promise.resolve(true); + } + + static openPaymentSetup(_merchantIdentifier: string): Promise { + return Promise.resolve(true); + } + + static supportsVersion(_version: number): boolean { + return true; + } + + abort(): void {} + + begin(): void {} + + completeMerchantValidation(_merchantSession: any): void {} + + completePayment( + _result: number | ApplePayJS.ApplePayPaymentAuthorizationResult + ): void {} + + completePaymentMethodSelection( + newTotal: ApplePayJS.ApplePayLineItem, + newLineItems: Array + ): void; + completePaymentMethodSelection( + update: ApplePayJS.ApplePayPaymentMethodUpdate + ): void; + completePaymentMethodSelection(_newTotal: any, _newLineItems?: any): void {} + + completeShippingContactSelection( + _status: number, + _newShippingMethods: Array, + _newTotal: ApplePayJS.ApplePayLineItem, + _newLineItems: Array + ): void; + completeShippingContactSelection( + _update: ApplePayJS.ApplePayShippingContactUpdate + ): void; + completeShippingContactSelection( + _status: any, + _newShippingMethods?: any, + _newTotal?: any, + _newLineItems?: any + ): void {} + + completeShippingMethodSelection( + _status: number, + _newTotal: ApplePayJS.ApplePayLineItem, + _newLineItems: Array + ): void; + completeShippingMethodSelection( + _update: ApplePayJS.ApplePayShippingMethodUpdate + ): void; + completeShippingMethodSelection( + _status: any, + _newTotal?: any, + _newLineItems?: any + ): void {} +} + +interface ApplePayObservableConfigExt extends ApplePayObservableConfig { + [key: string]: any; +} + +describe('ApplePayObservableFactory', () => { + let factory: ApplePayObservableFactory; + let mockApplePaySessionFactory: jasmine.SpyObj; + let mockApplePaySession: MockApplePaySession; + + const mockRequest: ApplePayJS.ApplePayPaymentRequest = { + countryCode: '', + currencyCode: '', + merchantCapabilities: [], + supportedNetworks: [], + total: { + label: '', + amount: '', + }, + }; + const mockConfig: ApplePayObservableConfig = { + request: mockRequest, + validateMerchant: () => of({}), + paymentMethodSelected: () => + of({} as ApplePayJS.ApplePayPaymentMethodUpdate), + shippingContactSelected: () => + of({} as ApplePayJS.ApplePayShippingContactUpdate), + shippingMethodSelected: () => + of({} as ApplePayJS.ApplePayShippingMethodUpdate), + paymentAuthorized: () => + of({} as ApplePayJS.ApplePayPaymentAuthorizationResult), + }; + + beforeEach(() => { + const MockApplePaySessionFactory = jasmine.createSpyObj( + 'ApplePaySessionFactory', + ['startApplePaySession'] + ); + + TestBed.configureTestingModule({ + providers: [ + ApplePayObservableFactory, + { + provide: ApplePaySessionFactory, + useValue: MockApplePaySessionFactory, + }, + ], + }); + + factory = TestBed.inject(ApplePayObservableFactory); + mockApplePaySessionFactory = TestBed.inject( + ApplePaySessionFactory + ) as jasmine.SpyObj; + + mockApplePaySession = new MockApplePaySession( + 1, + {} as ApplePayJS.ApplePayPaymentRequest + ); + spyOn(mockApplePaySession, 'addEventListener').and.callThrough(); + }); + + it('should be created', () => { + expect(factory).toBeTruthy(); + }); + + it('should return an observable that creates an apple pay session', () => { + const actual = factory.initApplePayEventsHandler({ + ...mockConfig, + }); + + mockApplePaySessionFactory.startApplePaySession.and.returnValue( + mockApplePaySession + ); + expect(actual instanceof Observable).toBe(true); + expect( + mockApplePaySessionFactory.startApplePaySession + ).not.toHaveBeenCalled(); + + actual.subscribe(); + + expect( + mockApplePaySessionFactory.startApplePaySession + ).toHaveBeenCalledTimes(1); + expect( + mockApplePaySessionFactory.startApplePaySession + ).toHaveBeenCalledWith(mockRequest); + }); + + it('should bind config event handlers to ApplePaySession', () => { + mockApplePaySessionFactory.startApplePaySession.and.returnValue( + mockApplePaySession + ); + factory.initApplePayEventsHandler(mockConfig).subscribe(); + + expect(mockApplePaySession.addEventListener).toHaveBeenCalledTimes(6); + expect(mockApplePaySession.addEventListener).toHaveBeenCalledWith( + 'cancel', + jasmine.any(Function) + ); + expect(mockApplePaySession.addEventListener).toHaveBeenCalledWith( + 'paymentauthorized', + jasmine.any(Function) + ); + expect(mockApplePaySession.addEventListener).toHaveBeenCalledWith( + 'paymentmethodselected', + jasmine.any(Function) + ); + expect(mockApplePaySession.addEventListener).toHaveBeenCalledWith( + 'shippingcontactselected', + jasmine.any(Function) + ); + expect(mockApplePaySession.addEventListener).toHaveBeenCalledWith( + 'shippingmethodselected', + jasmine.any(Function) + ); + expect(mockApplePaySession.addEventListener).toHaveBeenCalledWith( + 'validatemerchant', + jasmine.any(Function) + ); + }); + + it('should complete if the session is cancelled', () => { + let complete = false; + let actualEmit: any; + let actualError: any; + spyOn(mockApplePaySession, 'abort').and.stub(); + + mockApplePaySessionFactory.startApplePaySession.and.returnValue( + mockApplePaySession + ); + factory.initApplePayEventsHandler(mockConfig).subscribe( + (next) => (actualEmit = next), + (error) => (actualError = error), + () => (complete = true) + ); + expect(complete).toBe(false); + expect(actualError).toEqual(undefined); + expect(actualEmit).toEqual(undefined); + + mockApplePaySession._stubMockEvent('cancel'); + + expect(complete).toBe(false); + expect(actualError).toEqual({ type: 'PAYMENT_CANCELLED' }); + expect(actualEmit).toEqual(undefined); + }); + + describe('callback behavior', () => { + let actual: Observable; + let configuration: ApplePayObservableConfigExt; + + let paymentAuthorizedReturnValue: ApplePayJS.ApplePayPaymentAuthorizationResult; + let validateMerchantReturnValue: Object; + let shippingContactSelectedReturnValue: ApplePayJS.ApplePayShippingContactUpdate; + let paymentMethodSelectedReturnValue: ApplePayJS.ApplePayPaymentMethodUpdate; + let shippingMethodSelectedReturnValue: ApplePayJS.ApplePayShippingMethodUpdate; + let actualEmit: any; + let actualError: any; + let actualComplete: boolean; + beforeEach(() => { + configuration = { + request: mockRequest, + paymentAuthorized: jasmine.createSpy('paymentAuthorized'), + validateMerchant: jasmine.createSpy('validateMerchant'), + shippingContactSelected: jasmine.createSpy('shippingContactSelected'), + paymentMethodSelected: jasmine.createSpy('paymentMethodSelected'), + shippingMethodSelected: jasmine.createSpy('shippingMethodSelected'), + }; + + paymentAuthorizedReturnValue = { status: 0 }; + validateMerchantReturnValue = {}; + shippingContactSelectedReturnValue = { + newTotal: { amount: '5.00', label: 'Shipping' }, + }; + paymentMethodSelectedReturnValue = { + newTotal: { amount: '5.00', label: 'Payment' }, + }; + shippingMethodSelectedReturnValue = { + newTotal: { amount: '5.00', label: 'Shipping' }, + }; + + (configuration.paymentAuthorized as jasmine.Spy).and.returnValue( + of(paymentAuthorizedReturnValue) + ); + (configuration.validateMerchant as jasmine.Spy).and.returnValue( + of(validateMerchantReturnValue) + ); + (configuration.shippingContactSelected as jasmine.Spy).and.returnValue( + of(shippingContactSelectedReturnValue) + ); + (configuration.paymentMethodSelected as jasmine.Spy).and.returnValue( + of(paymentMethodSelectedReturnValue) + ); + (configuration.shippingMethodSelected as jasmine.Spy).and.returnValue( + of(shippingMethodSelectedReturnValue) + ); + mockApplePaySessionFactory.startApplePaySession.and.returnValue( + mockApplePaySession + ); + actual = factory.initApplePayEventsHandler(configuration); + actualEmit = undefined; + actualError = undefined; + actualComplete = false; + actual.subscribe( + (next) => (actualEmit = next), + (error) => (actualError = error), + () => (actualComplete = true) + ); + + spyOn(mockApplePaySession, 'completeMerchantValidation').and.stub(); + spyOn(mockApplePaySession, 'completePayment').and.stub(); + spyOn(mockApplePaySession, 'completePaymentMethodSelection').and.stub(); + spyOn(mockApplePaySession, 'completeShippingContactSelection').and.stub(); + spyOn(mockApplePaySession, 'completeShippingMethodSelection').and.stub(); + }); + + it('should use validateMerchant callback to fill validateMerchant', () => { + const event = { + validationURL: '', + }; + + mockApplePaySession._stubMockEvent('validatemerchant', event); + expect(actualComplete).toBe(false); + expect(configuration.validateMerchant).toHaveBeenCalledWith(event); + expect( + mockApplePaySession.completeMerchantValidation + ).toHaveBeenCalledWith(validateMerchantReturnValue); + }); + + it('should use shippingContactSelected callback to fill shippingContactSelected', () => { + const event = { + shippingContact: {}, + }; + + mockApplePaySession._stubMockEvent('shippingcontactselected', event); + + expect(configuration.shippingContactSelected).toHaveBeenCalledWith(event); + expect( + mockApplePaySession.completeShippingContactSelection + ).toHaveBeenCalledWith(shippingContactSelectedReturnValue); + }); + + it('should use shippingMethodSelected callback to fill shippingMethodSelected', () => { + const event = { + shippingMethod: {}, + }; + + mockApplePaySession._stubMockEvent('shippingmethodselected', event); + + expect(configuration.shippingMethodSelected).toHaveBeenCalledWith(event); + expect( + mockApplePaySession.completeShippingMethodSelection + ).toHaveBeenCalledWith(shippingMethodSelectedReturnValue); + }); + + it('should use paymentMethodSelected callback to fill paymentMethodSelected', () => { + const event = { + paymentMethod: {}, + }; + + mockApplePaySession._stubMockEvent('paymentmethodselected', event); + + expect(configuration.paymentMethodSelected).toHaveBeenCalledWith(event); + expect( + mockApplePaySession.completePaymentMethodSelection + ).toHaveBeenCalledWith(paymentMethodSelectedReturnValue); + }); + + it('should use paymentAuthorized callback to fill completePayment', () => { + const event = { + payment: {}, + }; + mockApplePaySession._stubMockEvent('paymentauthorized', event); + + expect(configuration.paymentAuthorized).toHaveBeenCalledWith(event); + expect(mockApplePaySession.completePayment).toHaveBeenCalledWith( + paymentAuthorizedReturnValue + ); + }); + + describe('on callback error', () => { + it('should emit an error when validateMerchant failed', () => { + const event = { + validationURL: '', + }; + (configuration.validateMerchant as jasmine.Spy).and.returnValue( + throwError(new Error('Validation Error')) + ); + + mockApplePaySession._stubMockEvent('validatemerchant', event); + + expect(configuration.validateMerchant).toHaveBeenCalledWith(event); + expect( + mockApplePaySession.completeMerchantValidation + ).not.toHaveBeenCalled(); + expect(actualError).toBeDefined(); + }); + + it('should abort when paymentauthorized failed with errors', () => { + spyOn(mockApplePaySession, 'abort').and.stub(); + const event = { + payment: {}, + }; + const paymentAuthorizedReturnValue = { + status: 0, + errors: [{ message: 'paymentauthorized failed' }], + }; + (configuration.paymentAuthorized as jasmine.Spy).and.returnValue( + of(paymentAuthorizedReturnValue) + ); + + mockApplePaySession._stubMockEvent('paymentauthorized', event); + + expect(configuration.paymentAuthorized).toHaveBeenCalledWith(event); + expect(mockApplePaySession.abort).toHaveBeenCalled(); + }); + + [ + ['paymentAuthorized', 'paymentauthorized'], + ['paymentMethodSelected', 'paymentmethodselected'], + ['shippingContactSelected', 'shippingcontactselected'], + ['shippingMethodSelected', 'shippingmethodselected'], + ['validateMerchant', 'validatemerchant'], + ].forEach(([callback, eventType]) => { + it(`should abort the session on an error in ${callback}`, () => { + const event = {}; + const callbackError = new Error('Error'); + (configuration[callback] as jasmine.Spy).and.returnValue( + throwError(callbackError) + ); + spyOn(mockApplePaySession, 'abort').and.stub(); + + mockApplePaySession._stubMockEvent(eventType, event); + + expect(configuration[callback]).toHaveBeenCalledWith(event); + expect(mockApplePaySession.abort).toHaveBeenCalled(); + expect(actualEmit).toBeUndefined(); + expect(actualError).toBe(callbackError); + }); + }); + }); + }); +}); diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/apple-pay-observable.factory.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/apple-pay-observable.factory.ts new file mode 100644 index 00000000000..14571dcf90d --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/apple-pay-observable.factory.ts @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +import { Injectable, inject } from '@angular/core'; +import { PaymentErrorType } from '@spartacus/opf/payment/root'; +import { + ApplePayEvent, + ApplePayObservableConfig, + ApplePayShippingType, +} from '@spartacus/opf/quick-buy/root'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { ApplePaySessionFactory } from '../apple-pay-session/apple-pay-session.factory'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplePayObservableFactory { + protected applePaySessionFactory = inject(ApplePaySessionFactory); + + initApplePayEventsHandler(config: ApplePayObservableConfig): Observable { + return new Observable((observer) => { + let session: ApplePaySession; + try { + session = this.applePaySessionFactory.startApplePaySession( + config.request + ) as ApplePaySession; + } catch (err) { + observer.error(err); + return; + } + + const handleUnspecifiedError = (error: any): void => { + session.abort(); + observer.error(error); + }; + + session.addEventListener( + ApplePayEvent.VALIDATE_MERCHANT, + (event: Event) => { + config + .validateMerchant(event) + .pipe(take(1)) + .subscribe({ + next: (merchantSession) => { + session.completeMerchantValidation(merchantSession); + }, + error: handleUnspecifiedError, + }); + } + ); + + session.addEventListener(ApplePayEvent.CANCEL, () => { + observer.error({ type: PaymentErrorType.PAYMENT_CANCELLED }); + }); + + if (config.paymentMethodSelected) { + session.addEventListener( + ApplePayEvent.PAYMENT_METHOD_SELECTED, + (event: Event) => { + config + .paymentMethodSelected(event) + .pipe(take(1)) + .subscribe({ + next: (paymentMethodUpdate) => { + session.completePaymentMethodSelection(paymentMethodUpdate); + }, + error: handleUnspecifiedError, + }); + } + ); + } + + if ( + config.shippingContactSelected && + this.isShippingTypeNotPickup(config) + ) { + session.addEventListener( + ApplePayEvent.SHIPPING_CONTACT_SELECTED, + (event: Event) => { + config + .shippingContactSelected(event) + .pipe(take(1)) + .subscribe({ + next: (shippingContactUpdate) => { + session.completeShippingContactSelection( + shippingContactUpdate + ); + }, + error: handleUnspecifiedError, + }); + } + ); + } + + if ( + config.shippingMethodSelected && + this.isShippingTypeNotPickup(config) + ) { + session.addEventListener( + ApplePayEvent.SHIPPING_METHOD_SELECTED, + (event: Event) => { + config + .shippingMethodSelected(event) + .pipe(take(1)) + .subscribe({ + next: (shippingMethodUpdate) => { + session.completeShippingMethodSelection(shippingMethodUpdate); + }, + error: handleUnspecifiedError, + }); + } + ); + } + + session.addEventListener( + ApplePayEvent.PAYMENT_AUTHORIZED, + (event: Event) => { + config + .paymentAuthorized(event) + .pipe(take(1)) + .subscribe({ + next: (authResult) => { + session.completePayment(authResult); + if (!authResult?.errors?.length) { + observer.next(authResult); + observer.complete(); + } else { + handleUnspecifiedError({ + message: authResult?.errors[0]?.message, + }); + } + }, + error: handleUnspecifiedError, + }); + } + ); + session.begin(); + }); + } + + protected isShippingTypeNotPickup(config: any) { + return config.request.shippingType !== ApplePayShippingType.STORE_PICKUP; + } +} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/index.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/index.ts new file mode 100644 index 00000000000..3e782cc899d --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/apple-pay/observable/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './apple-pay-observable.factory'; diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.html b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.html new file mode 100644 index 00000000000..da16342873e --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.html @@ -0,0 +1,7 @@ + +
+
diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.spec.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.spec.ts new file mode 100644 index 00000000000..6614410c33b --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.spec.ts @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + ElementRef, +} from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { OpfGooglePayComponent } from './google-pay.component'; +import { OpfGooglePayService } from './google-pay.service'; + +class MockOpfGooglePayService { + loadProviderResources = jasmine + .createSpy() + .and.returnValue(Promise.resolve()); + initClient = jasmine.createSpy(); + isReadyToPay = jasmine + .createSpy() + .and.returnValue(Promise.resolve({ result: true })); + renderPaymentButton = jasmine.createSpy(); +} + +describe('OpfGooglePayComponent', () => { + let component: OpfGooglePayComponent; + let fixture: ComponentFixture; + let mockOpfGooglePayService: MockOpfGooglePayService; + let mockChangeDetectorRef: jasmine.SpyObj; + + beforeEach(async () => { + mockChangeDetectorRef = jasmine.createSpyObj('ChangeDetectorRef', [ + 'detectChanges', + ]); + mockOpfGooglePayService = new MockOpfGooglePayService(); + + TestBed.configureTestingModule({ + declarations: [OpfGooglePayComponent], + providers: [ + { provide: OpfGooglePayService, useValue: mockOpfGooglePayService }, + { provide: ChangeDetectorRef, useValue: mockChangeDetectorRef }, + ], + }) + .overrideComponent(OpfGooglePayComponent, { + set: { changeDetection: ChangeDetectionStrategy.OnPush }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(OpfGooglePayComponent); + component = fixture.componentInstance; + component.activeConfiguration = {}; + }); + + async function detectChanges() { + fixture.detectChanges(); + await fixture.whenStable(); + } + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize Google Pay client on init', async () => { + await detectChanges(); + + expect(mockOpfGooglePayService.loadProviderResources).toHaveBeenCalled(); + expect(mockOpfGooglePayService.initClient).toHaveBeenCalledWith( + component.activeConfiguration + ); + }); + + it('should update ready to pay state when Google Pay is ready', async () => { + await detectChanges(); + + expect(component.isReadyToPayState$.getValue()).toBe(true); + }); + + it('should render payment button when Google Pay is ready and container is available', async () => { + component.googlePayButtonContainer = new ElementRef( + document.createElement('div') + ); + + await detectChanges(); + + expect(mockOpfGooglePayService.renderPaymentButton).toHaveBeenCalledWith( + component.googlePayButtonContainer + ); + }); +}); diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.ts new file mode 100644 index 00000000000..6a4b6621df5 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.component.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnInit, + ViewChild, + inject, +} from '@angular/core'; +import { ActiveConfiguration } from '@spartacus/opf/base/root'; +import { BehaviorSubject } from 'rxjs'; +import { OpfGooglePayService } from './google-pay.service'; + +@Component({ + selector: 'cx-opf-google-pay', + templateUrl: './google-pay.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OpfGooglePayComponent implements OnInit { + protected opfGooglePayService = inject(OpfGooglePayService); + protected changeDetectionRef = inject(ChangeDetectorRef); + + @Input() activeConfiguration: ActiveConfiguration; + + @ViewChild('googlePayButtonContainer') googlePayButtonContainer: ElementRef; + + isReadyToPayState$: BehaviorSubject = new BehaviorSubject(false); + + ngOnInit(): void { + this.opfGooglePayService.loadProviderResources().then(() => { + this.opfGooglePayService.initClient(this.activeConfiguration); + this.opfGooglePayService.isReadyToPay().then((response: any) => { + this.isReadyToPayState$.next(response?.result); + this.changeDetectionRef.detectChanges(); + if (response.result && this.googlePayButtonContainer) { + this.opfGooglePayService.renderPaymentButton( + this.googlePayButtonContainer + ); + } + }); + }); + } +} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.module.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.module.ts new file mode 100644 index 00000000000..cbcb3ab7cdf --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.module.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { OpfGooglePayComponent } from './google-pay.component'; +import { OpfGooglePayService } from './google-pay.service'; + +@NgModule({ + declarations: [OpfGooglePayComponent], + exports: [OpfGooglePayComponent], + imports: [CommonModule], + providers: [OpfGooglePayService], +}) +export class OpfGooglePayModule {} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.service.spec.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.service.spec.ts new file mode 100644 index 00000000000..4497d5bf7f9 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.service.spec.ts @@ -0,0 +1,772 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ElementRef } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Cart } from '@spartacus/cart/base/root'; +import { Address, PriceType } from '@spartacus/core'; +import { OpfResourceLoaderService } from '@spartacus/opf/base/root'; +import { OpfPaymentFacade } from '@spartacus/opf/payment/root'; +import { OpfQuickBuyTransactionService } from '@spartacus/opf/quick-buy/core'; +import { + OPF_QUICK_BUY_ADDRESS_FIELD_PLACEHOLDER, + OpfProviderType, + OpfQuickBuyLocation, +} from '@spartacus/opf/quick-buy/root'; +import { CurrentProductService } from '@spartacus/storefront'; +import { of } from 'rxjs'; +import { OpfQuickBuyButtonsService } from '../opf-quick-buy-buttons.service'; +import { OpfGooglePayService } from './google-pay.service'; + +const mockGooglePayAddress = { + countryCode: 'CA', + locality: 'Toronto', + postalCode: 'A1B 2C3', + address1: '456 Elm St', + address2: '', + address3: '', +}; + +const mockConvertedAddress: Address = { + country: { isocode: 'CA' }, + town: 'Toronto', + district: undefined, + postalCode: 'A1B 2C3', + line1: '456 Elm St', + line2: ' ', +}; + +describe('OpfGooglePayService', () => { + const mockMerchantName = 'mockMerchantName'; + let service: OpfGooglePayService; + let mockResourceLoaderService: jasmine.SpyObj; + let mockCurrentProductService: jasmine.SpyObj; + let mockQuickBuyTransactionService: jasmine.SpyObj; + let mockPaymentFacade: jasmine.SpyObj; + let mockQuickBuyButtonsService: jasmine.SpyObj; + + beforeEach(() => { + mockResourceLoaderService = jasmine.createSpyObj( + 'OpfResourceLoaderService', + ['loadProviderResources'] + ); + mockCurrentProductService = jasmine.createSpyObj('CurrentProductService', [ + 'getProduct', + ]); + mockQuickBuyTransactionService = jasmine.createSpyObj( + 'OpfQuickBuyTransactionService', + [ + 'deleteUserAddresses', + 'checkStableCart', + 'getSupportedDeliveryModes', + 'setDeliveryAddress', + 'setBillingAddress', + 'getDeliveryAddress', + 'getCurrentCart', + 'getCurrentCartId', + 'getCurrentCartTotalPrice', + 'setDeliveryMode', + 'getSelectedDeliveryMode', + 'deleteUserAddresses', + 'getTransactionDeliveryInfo', + 'getTransactionLocationContext', + 'getMerchantName', + ] + ); + mockPaymentFacade = jasmine.createSpyObj('OpfPaymentFacade', [ + 'submitPayment', + ]); + mockQuickBuyButtonsService = jasmine.createSpyObj( + 'OpfQuickBuyButtonsService', + ['getQuickBuyProviderConfig'] + ); + + const googlePayApiMock = { + payments: { + api: { + PaymentsClient: jasmine.createSpy('PaymentsClient').and.returnValue({ + loadPaymentData: jasmine + .createSpy() + .and.returnValue(Promise.resolve({})), + isReadyToPay: jasmine + .createSpy() + .and.returnValue(Promise.resolve({ result: true })), + }), + }, + }, + }; + + window.google = googlePayApiMock as any; + + TestBed.configureTestingModule({ + providers: [ + OpfGooglePayService, + { + provide: OpfResourceLoaderService, + useValue: mockResourceLoaderService, + }, + + { provide: CurrentProductService, useValue: mockCurrentProductService }, + { + provide: OpfQuickBuyTransactionService, + useValue: mockQuickBuyTransactionService, + }, + { provide: OpfPaymentFacade, useValue: mockPaymentFacade }, + { + provide: OpfQuickBuyButtonsService, + useValue: mockQuickBuyButtonsService, + }, + ], + }); + + service = TestBed.inject(OpfGooglePayService); + service['updateGooglePaymentClient'](); + }); + + describe('getClient', () => { + it('should return the Google Payment client instance', () => { + const activeConfiguration = {}; + service.initClient(activeConfiguration); + + const client = service['getClient'](); + + expect(client).toBeDefined(); + }); + }); + + describe('getShippingOptionParameters', () => { + it('should transform delivery modes into shipping option parameters', (done) => { + const mockDeliveryModes = [ + { + code: 'STANDARD', + name: 'Standard Delivery', + description: 'Delivers in 3-5 days', + }, + ]; + mockQuickBuyTransactionService.getSupportedDeliveryModes.and.returnValue( + of(mockDeliveryModes) + ); + + service['getShippingOptionParameters']().subscribe((shippingOptions) => { + expect(shippingOptions).toBeDefined(); + expect(shippingOptions?.shippingOptions.length).toBe( + mockDeliveryModes.length + ); + expect(shippingOptions?.defaultSelectedOptionId).toBe( + mockDeliveryModes[0].code + ); + + mockDeliveryModes.forEach((mode, index) => { + const shippingOption = shippingOptions?.shippingOptions[index]; + expect(shippingOption?.id).toEqual(mode.code); + expect(shippingOption?.label).toEqual(mode.name); + expect(shippingOption?.description).toEqual(mode.description); + }); + + done(); + }); + }); + + it('should handle cases with no delivery modes available', (done) => { + mockQuickBuyTransactionService.getSupportedDeliveryModes.and.returnValue( + of([]) + ); + + service['getShippingOptionParameters']().subscribe((shippingOptions) => { + expect(shippingOptions).toBeDefined(); + expect(shippingOptions?.shippingOptions).toEqual([]); + expect(shippingOptions?.defaultSelectedOptionId).toBeUndefined(); + done(); + }); + }); + }); + + describe('loadProviderResources', () => { + it('should load the Google Pay JS API', async () => { + mockResourceLoaderService.loadProviderResources.and.returnValue( + Promise.resolve() + ); + + await service.loadProviderResources(); + + expect( + mockResourceLoaderService.loadProviderResources + ).toHaveBeenCalled(); + }); + + it('should handle errors when loading the Google Pay JS API', async () => { + mockResourceLoaderService.loadProviderResources.and.returnValue( + Promise.reject(new Error('Load failed')) + ); + + await expectAsync(service.loadProviderResources()).toBeRejectedWithError( + 'Load failed' + ); + }); + }); + + describe('isReadyToPay', () => { + it('should return info about readiness to pay from the Google Pay API', async () => { + const activeConfiguration = {}; + + service.initClient(activeConfiguration); + + await expectAsync(service.isReadyToPay()).toBeResolvedTo({ + result: true, + }); + }); + + it('should handle errors from the Google Pay API', async () => { + spyOn(service, 'isReadyToPay').and.returnValue( + Promise.reject(new Error('API error')) + ); + + await expectAsync(service.isReadyToPay()).toBeRejectedWithError( + 'API error' + ); + }); + }); + + describe('initClient', () => { + it('should initialize the Google Payment client with configurations', () => { + const activeConfiguration = {}; + service.initClient(activeConfiguration); + + const client = service['googlePaymentClient']; + + expect(client).toBeDefined(); + }); + }); + + describe('updateTransactionInfo', () => { + it('should update transaction info', () => { + const transactionInfo = { + totalPrice: '100.00', + currencyCode: 'USD', + totalPriceStatus: 'FINAL', + } as google.payments.api.TransactionInfo; + + service['updateTransactionInfo'](transactionInfo); + + const updatedTransactionInfo = + service['googlePaymentRequest'].transactionInfo; + + expect(updatedTransactionInfo).toEqual(transactionInfo); + }); + }); + + describe('setDeliveryAddress', () => { + it('should successfully set delivery address and return an address ID', async () => { + const mockAddress = { countryCode: 'US' } as google.payments.api.Address; + const mockAddressId = 'mockAddressId'; + mockQuickBuyTransactionService['setDeliveryAddress'].and.returnValue( + of(mockAddressId) + ); + + const addressId = + await service['setDeliveryAddress'](mockAddress).toPromise(); + + expect(addressId).toEqual(mockAddressId); + }); + + it('should correctly split name into first and last names and set delivery address', (done) => { + const mockAddress = { + name: 'John Doe', + countryCode: 'US', + }; + + mockQuickBuyTransactionService.setDeliveryAddress.and.returnValue( + of('addressId') + ); + + service['setDeliveryAddress']( + mockAddress as google.payments.api.Address + ).subscribe((addressId) => { + expect(addressId).toEqual('addressId'); + expect( + mockQuickBuyTransactionService.setDeliveryAddress + ).toHaveBeenCalledWith( + jasmine.objectContaining({ + firstName: 'John', + lastName: ' Doe', + country: { isocode: mockAddress.countryCode }, + }) + ); + done(); + }); + }); + }); + + describe('getNewTransactionInfo', () => { + it('should return transaction info for a given cart', () => { + const mockCart = { + totalPriceWithTax: { value: 100.0, currencyIso: 'USD' }, + } as Cart; + + const transactionInfo = service['getNewTransactionInfo'](mockCart); + + expect(transactionInfo).toBeDefined(); + expect(transactionInfo?.totalPrice).toBe('100'); + expect(transactionInfo?.currencyCode).toBe('USD'); + expect(transactionInfo?.totalPriceStatus).toBe('FINAL'); + }); + + it('should handle cart with missing price information', () => { + const mockCart = {} as Cart; + + const transactionInfo = service['getNewTransactionInfo'](mockCart); + + expect(transactionInfo).toBeUndefined(); + }); + + it('should return undefined for cart with a total price of zero', () => { + const mockCart = { + totalPriceWithTax: { value: 0, currencyIso: 'USD' }, + } as Cart; + + const transactionInfo = service['getNewTransactionInfo'](mockCart); + + expect(transactionInfo).toBeUndefined(); + }); + }); + + describe('setDeliveryMode', () => { + it('should successfully set delivery mode', async () => { + const mode = 'standard'; + mockQuickBuyTransactionService.setDeliveryMode.and.returnValue(of({})); + + const result = await service.setDeliveryMode(mode).toPromise(); + + expect(result).toBeDefined(); + }); + }); + + describe('setBillingAddress', () => { + it('should call setBillingAddress from cartHandlerService', async () => { + const address = { + ...mockGooglePayAddress, + ...{ name: 'John Doe' }, + }; + + mockQuickBuyTransactionService.setBillingAddress.and.returnValue( + of(true) + ); + + service['setBillingAddress'](address as any).subscribe((result) => { + expect(result).toBe(true); + expect( + mockQuickBuyTransactionService.setBillingAddress + ).toHaveBeenCalledWith({ + ...mockConvertedAddress, + ...{ + firstName: 'John', + lastName: ' Doe', + }, + }); + }); + }); + }); + + describe('convertAddress', () => { + it('should convert the address correctly when address is partially defined', () => { + const result = service['convertAddress'](mockGooglePayAddress as any); + + expect(result).toEqual({ + ...mockConvertedAddress, + ...{ + firstName: OPF_QUICK_BUY_ADDRESS_FIELD_PLACEHOLDER, + lastName: OPF_QUICK_BUY_ADDRESS_FIELD_PLACEHOLDER, + }, + }); + }); + + it('should convert the address correctly when address includes a name', () => { + const address = { + ...mockGooglePayAddress, + ...{ name: 'John Doe' }, + }; + const result = service['convertAddress'](address as any); + + expect(result).toEqual({ + ...mockConvertedAddress, + ...{ + firstName: 'John', + lastName: ' Doe', + }, + }); + }); + }); + + describe('verifyShippingOption', () => { + it('should return the mode if it is not "shipping_option_unselected"', () => { + const mode = 'standard_shipping'; + + expect(service['verifyShippingOption'](mode)).toBe(mode); + }); + + it('should return undefined if the mode is "shipping_option_unselected"', () => { + const mode = 'shipping_option_unselected'; + + expect(service['verifyShippingOption'](mode)).toBeUndefined(); + }); + + it('should return undefined if the mode is undefined', () => { + expect(service['verifyShippingOption'](undefined)).toBeUndefined(); + }); + }); + + describe('associateAddressId', () => { + it('should add a new address ID if not already present', () => { + const addressId = 'newAddressId'; + + service['associateAddressId'](addressId); + + expect(service['initialTransactionDetails']['addressIds']).toContain( + addressId + ); + }); + + it('should not add an address ID if it is already present', () => { + const addressId = 'existingAddressId'; + + service['associateAddressId'](addressId); + service['associateAddressId'](addressId); + + expect( + service['initialTransactionDetails']['addressIds'].filter( + (id: any) => id === addressId + ).length + ).toBe(1); + }); + }); + + describe('isAddressIdAssociated', () => { + it('should return true if address ID is already associated', () => { + const addressId = 'existingAddressId'; + service['initialTransactionDetails']['addressIds'].push(addressId); + + const result = service['isAddressIdAssociated'](addressId); + + expect(result).toBeTruthy(); + }); + + it('should return false if address ID is not associated', () => { + const addressId = 'newAddressId'; + service['initialTransactionDetails']['addressIds'] = ( + service as any + ).initialTransactionDetails.addressIds.filter( + (id: any) => id !== addressId + ); + + const result = service['isAddressIdAssociated'](addressId); + + expect(result).toBeFalsy(); + }); + }); + + describe('resetAssociatedAddresses', () => { + it('should clear all associated address IDs', () => { + service['initialTransactionDetails']['addressIds'] = [ + 'address1', + 'address2', + ]; + + service['resetAssociatedAddresses'](); + + expect(service['initialTransactionDetails']['addressIds']).toEqual([]); + expect(service['initialTransactionDetails']['addressIds'].length).toBe(0); + }); + }); + + describe('deleteAssociatedAddresses', () => { + it('should call deleteUserAddresses and reset associated addresses', () => { + service['initialTransactionDetails']['addressIds'] = [ + 'address1', + 'address2', + ]; + + service['deleteAssociatedAddresses'](); + + expect( + mockQuickBuyTransactionService.deleteUserAddresses + ).toHaveBeenCalledWith(['address1', 'address2']); + + expect(service['initialTransactionDetails']['addressIds']).toEqual([]); + }); + + it('should not call deleteUserAddresses if there are no associated addresses', () => { + (service as any).associatedShippingAddressIds = []; + + service['deleteAssociatedAddresses'](); + + expect( + mockQuickBuyTransactionService.deleteUserAddresses + ).not.toHaveBeenCalled(); + }); + }); + + describe('getFirstAndLastName', () => { + it('should correctly split first and last names', () => { + const name = 'John Doe'; + const result = service['getFirstAndLastName'](name); + expect(result.firstName).toBe('John'); + expect(result.lastName).toBe(' Doe'); + }); + + it('should handle a single name', () => { + const name = 'John'; + const result = service['getFirstAndLastName'](name); + expect(result.firstName).toBe('John'); + expect(result.lastName).toBe('John'); + }); + + it('should correctly handle multiple names', () => { + const name = 'John Michael Doe'; + const result = service['getFirstAndLastName'](name); + expect(result.firstName).toBe('John'); + expect(result.lastName).toBe(' Michael Doe'); + }); + }); + + describe('initTransaction', () => { + it('should initialize transaction for active cart context', (done: DoneFn) => { + spyOn(service, 'handleActiveCartTransaction').and.returnValue(of(null)); + + mockQuickBuyTransactionService.getTransactionLocationContext.and.returnValue( + of(OpfQuickBuyLocation.CART) + ); + + mockQuickBuyTransactionService.getMerchantName.and.returnValue( + of(mockMerchantName) + ); + + service.initTransaction(); + + expect(service['transactionDetails'].context).toBe( + OpfQuickBuyLocation.CART + ); + + setTimeout(() => { + expect(service.handleActiveCartTransaction).toHaveBeenCalled(); + + done(); + }, 0); + }); + }); + + describe('renderPaymentButton', () => { + let mockGooglePaymentClient: jasmine.SpyObj; + + beforeEach(() => { + mockGooglePaymentClient = jasmine.createSpyObj('PaymentsClient', [ + 'createButton', + ]); + service['googlePaymentClient'] = mockGooglePaymentClient; + + mockGooglePaymentClient.createButton.and.callFake((config: any) => { + const button = document.createElement('button'); + button.addEventListener('click', config.onClick); + + return button; + }); + }); + + it('should append a payment button to the container', () => { + const container = new ElementRef(document.createElement('div')); + + service.renderPaymentButton(container); + + expect(container.nativeElement.children.length).toBe(1); + expect(container.nativeElement.children[0].tagName).toBe('BUTTON'); + expect(mockGooglePaymentClient.createButton).toHaveBeenCalled(); + }); + + it('should attach the correct click handler to the button', () => { + const container = new ElementRef(document.createElement('div')); + spyOn(service, 'initTransaction'); + + service.renderPaymentButton(container); + + const button = container.nativeElement.children[0]; + (button as any).click(); + + expect(service.initTransaction).toHaveBeenCalled(); + }); + }); + + describe('handlePaymentCallbacks', () => { + const encodedMockToken = 'encodedMockToken'; + + beforeEach(() => { + spyOn(window, 'btoa').and.callFake(() => { + return encodedMockToken; + }); + }); + + it('should return valid payment data callbacks', () => { + const callbacks = service['handlePaymentCallbacks'](); + + expect(callbacks).toBeDefined(); + expect(callbacks.onPaymentAuthorized).toBeDefined(); + expect(callbacks.onPaymentDataChanged).toBeDefined(); + }); + + describe('onPaymentAuthorized', () => { + it('should handle payment authorization', (done) => { + const callbacks = service['handlePaymentCallbacks'](); + const mockCartId = 'cartId'; + const mockToken = 'mockToken'; + const paymentDataResponse = { + paymentMethodData: { + tokenizationData: { + token: mockToken, + }, + }, + } as google.payments.api.PaymentData; + + mockQuickBuyTransactionService.getCurrentCartId.and.returnValue( + of(mockCartId) + ); + mockPaymentFacade.submitPayment.and.returnValue(of(true)); + mockQuickBuyTransactionService.setBillingAddress.and.returnValue( + of(true) + ); + mockQuickBuyTransactionService.setDeliveryAddress.and.returnValue( + of('addressId') + ); + + if (callbacks.onPaymentAuthorized) { + ( + callbacks.onPaymentAuthorized(paymentDataResponse) as Promise + ).then((result) => { + const submitPaymentArgs = + mockPaymentFacade.submitPayment.calls.mostRecent().args[0]; + + expect(result).toBeDefined(); + expect(mockPaymentFacade.submitPayment).toHaveBeenCalled(); + expect(submitPaymentArgs.callbackArray.length).toBe(3); + submitPaymentArgs.callbackArray.forEach((callback) => { + expect(typeof callback).toBe('function'); + }); + expect(submitPaymentArgs.cartId).toBe(mockCartId); + expect(submitPaymentArgs.cartId).toBe(mockCartId); + expect(submitPaymentArgs.encryptedToken).toBe(encodedMockToken); + expect(submitPaymentArgs.paymentMethod).toBe( + OpfProviderType.GOOGLE_PAY + ); + expect( + mockQuickBuyTransactionService.getCurrentCartId + ).toHaveBeenCalled(); + expect(result).toEqual({ transactionState: 'SUCCESS' }); + done(); + }); + } + }); + }); + + describe('onPaymentDataChanged', () => { + it('should handle payment data changes', (done) => { + const callbacks = service['handlePaymentCallbacks'](); + const intermediatePaymentData = + {} as google.payments.api.IntermediatePaymentData; + + const selectedDeliveryMode = { + code: 'code', + deliveryCost: { + currencyIso: 'US', + formattedValue: '100.00', + maxQuantity: 2, + minQuantity: 1, + priceType: PriceType.BUY, + value: 100, + }, + description: 'description', + name: 'STANDARD DELIVERY', + }; + + mockQuickBuyTransactionService.getCurrentCartId.and.returnValue( + of('cartId') + ); + mockPaymentFacade.submitPayment.and.returnValue(of()); + mockQuickBuyTransactionService.setDeliveryAddress.and.returnValue( + of('addressId') + ); + mockQuickBuyTransactionService.setDeliveryMode.and.returnValue( + of({ + code: 'code', + }) + ); + mockQuickBuyTransactionService.getCurrentCart.and.returnValue(of({})); + mockQuickBuyTransactionService.getSelectedDeliveryMode.and.returnValue( + of(selectedDeliveryMode) + ); + mockQuickBuyTransactionService.getSupportedDeliveryModes.and.returnValue( + of([ + { + code: 'code', + deliveryCost: { + currencyIso: 'US', + formattedValue: '100.00', + maxQuantity: 2, + minQuantity: 1, + priceType: PriceType.BUY, + value: 100, + }, + description: 'description', + name: 'STANDARD DELIVERY', + }, + ]) + ); + + if (callbacks.onPaymentDataChanged) { + ( + callbacks.onPaymentDataChanged( + intermediatePaymentData + ) as Promise + ).then((paymentDataRequestUpdate) => { + expect( + paymentDataRequestUpdate.newShippingOptionParameters + .defaultSelectedOptionId + ).toEqual(selectedDeliveryMode.code); + expect( + paymentDataRequestUpdate.newShippingOptionParameters + .shippingOptions[0].id + ).toEqual(selectedDeliveryMode.code); + expect( + paymentDataRequestUpdate.newShippingOptionParameters + .shippingOptions[0].description + ).toEqual(selectedDeliveryMode.description); + expect( + paymentDataRequestUpdate.newShippingOptionParameters + .shippingOptions[0].label + ).toEqual(selectedDeliveryMode.name); + expect(paymentDataRequestUpdate).toBeDefined(); + expect( + mockQuickBuyTransactionService.setDeliveryAddress + ).toHaveBeenCalled(); + expect( + mockQuickBuyTransactionService.setDeliveryMode + ).toHaveBeenCalled(); + expect( + mockQuickBuyTransactionService.getCurrentCart + ).toHaveBeenCalled(); + expect( + mockQuickBuyTransactionService.getSelectedDeliveryMode + ).toHaveBeenCalledWith(); + + done(); + }); + } + }); + }); + }); + + afterEach(() => { + delete (window as any).google; + + TestBed.resetTestingModule(); + }); +}); diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.service.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.service.ts new file mode 100644 index 00000000000..47ca0e3b3c7 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/google-pay.service.ts @@ -0,0 +1,496 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +import { ElementRef, Injectable, inject } from '@angular/core'; +import { Cart, DeliveryMode } from '@spartacus/cart/base/root'; +import { Address } from '@spartacus/core'; + +import { + ActiveConfiguration, + OpfResourceLoaderService, +} from '@spartacus/opf/base/root'; +import { OpfPaymentFacade } from '@spartacus/opf/payment/root'; +import { OpfQuickBuyTransactionService } from '@spartacus/opf/quick-buy/core'; +import { + OPF_QUICK_BUY_ADDRESS_FIELD_PLACEHOLDER, + OPF_QUICK_BUY_DEFAULT_MERCHANT_NAME, + OpfProviderType, + OpfQuickBuyDeliveryType, + OpfQuickBuyLocation, + QuickBuyTransactionDetails, +} from '@spartacus/opf/quick-buy/root'; +import { CurrentProductService } from '@spartacus/storefront'; +import { Observable, forkJoin, lastValueFrom, of } from 'rxjs'; +import { catchError, map, switchMap, take, tap } from 'rxjs/operators'; +import { OpfQuickBuyButtonsService } from '../opf-quick-buy-buttons.service'; + +@Injectable({ + providedIn: 'root', +}) +export class OpfGooglePayService { + protected opfResourceLoaderService = inject(OpfResourceLoaderService); + protected currentProductService = inject(CurrentProductService); + protected opfPaymentFacade = inject(OpfPaymentFacade); + protected opfQuickBuyTransactionService = inject( + OpfQuickBuyTransactionService + ); + protected opfQuickBuyButtonsService = inject(OpfQuickBuyButtonsService); + + protected readonly GOOGLE_PAY_JS_URL = + 'https://pay.google.com/gp/p/js/pay.js'; + + private googlePaymentClient: google.payments.api.PaymentsClient; + + private googlePaymentClientOptions: google.payments.api.PaymentOptions = { + environment: 'TEST', + }; + + private initialGooglePaymentRequest: google.payments.api.PaymentDataRequest = + { + /** + * TODO: Move this part into configuration layer. + */ + apiVersion: 2, + apiVersionMinor: 0, + callbackIntents: [ + 'PAYMENT_AUTHORIZATION', + 'SHIPPING_ADDRESS', + 'SHIPPING_OPTION', + ], + + // @ts-ignore + merchantInfo: { + // merchantId: 'spartacusStorefront', + merchantName: OPF_QUICK_BUY_DEFAULT_MERCHANT_NAME, + }, + shippingOptionRequired: true, + shippingAddressRequired: true, + // @ts-ignore + shippingAddressParameters: { + phoneNumberRequired: false, + }, + }; + + private initialTransactionInfo: google.payments.api.TransactionInfo = { + totalPrice: '0.00', + totalPriceStatus: 'ESTIMATED', + currencyCode: 'USD', + }; + + protected initialTransactionDetails: QuickBuyTransactionDetails = { + context: OpfQuickBuyLocation.PRODUCT, + product: undefined, + cart: undefined, + quantity: 0, + deliveryInfo: { + type: OpfQuickBuyDeliveryType.SHIPPING, + pickupDetails: undefined, + }, + addressIds: [], + total: { + label: '', + amount: '', + currency: '', + }, + }; + + private googlePaymentRequest = this.initialGooglePaymentRequest; + + protected transactionDetails = this.initialTransactionDetails; + + protected updateGooglePaymentClient(): void { + this.googlePaymentClient = new google.payments.api.PaymentsClient( + this.googlePaymentClientOptions + ); + } + + protected setGooglePaymentRequestConfig( + deliveryType: OpfQuickBuyDeliveryType, + merchantName: string + ) { + if (deliveryType === OpfQuickBuyDeliveryType.PICKUP) { + this.googlePaymentClientOptions = { + ...this.googlePaymentClientOptions, + paymentDataCallbacks: { + onPaymentAuthorized: + this.handlePaymentCallbacks().onPaymentAuthorized, + }, + }; + this.googlePaymentRequest = { + ...this.initialGooglePaymentRequest, + shippingAddressRequired: false, + shippingOptionRequired: false, + callbackIntents: ['PAYMENT_AUTHORIZATION'], + }; + } else { + this.googlePaymentClientOptions = { + ...this.googlePaymentClientOptions, + paymentDataCallbacks: this.handlePaymentCallbacks(), + }; + this.googlePaymentRequest = this.initialGooglePaymentRequest; + } + this.googlePaymentRequest.merchantInfo.merchantName = merchantName; + this.updateGooglePaymentClient(); + } + + loadProviderResources(): Promise { + return this.opfResourceLoaderService.loadProviderResources([ + { url: this.GOOGLE_PAY_JS_URL }, + ]); + } + + initClient(activeConfiguration: ActiveConfiguration): void { + this.setAllowedPaymentMethodsConfig(activeConfiguration); + this.updateGooglePaymentClient(); + } + + private getClient(): google.payments.api.PaymentsClient { + return this.googlePaymentClient; + } + + isReadyToPay() { + return this.googlePaymentClient.isReadyToPay( + this.googlePaymentRequest + ) as any; + } + + private updateTransactionInfo( + transactionInfo: google.payments.api.TransactionInfo + ) { + this.googlePaymentRequest.transactionInfo = transactionInfo; + } + + private getShippingOptionParameters(): Observable< + google.payments.api.ShippingOptionParameters | undefined + > { + return this.opfQuickBuyTransactionService.getSupportedDeliveryModes().pipe( + take(1), + map((modes) => { + return { + defaultSelectedOptionId: modes[0]?.code, + shippingOptions: modes?.map((mode) => ({ + id: mode?.code, + label: mode?.name, + description: mode?.description, + })), + } as google.payments.api.ShippingOptionParameters; + }) + ); + } + + private getNewTransactionInfo( + cart: Cart + ): google.payments.api.TransactionInfo | undefined { + let transactionInfo: google.payments.api.TransactionInfo | undefined; + const priceInfo = cart?.totalPriceWithTax; + if (priceInfo && priceInfo.value && priceInfo.currencyIso) { + transactionInfo = { + totalPrice: priceInfo.value.toString(), + currencyCode: priceInfo.currencyIso.toString(), + totalPriceStatus: 'FINAL', + }; + } + + return transactionInfo; + } + + private setDeliveryAddress( + address: google.payments.api.Address | undefined + ): Observable { + const deliveryAddress = this.convertAddress(address); + + if ( + this.transactionDetails?.deliveryInfo?.type === + OpfQuickBuyDeliveryType.SHIPPING + ) { + return this.opfQuickBuyTransactionService + .setDeliveryAddress(deliveryAddress) + .pipe( + tap((addressId) => { + this.associateAddressId(addressId); + }) + ); + } else { + return of(OpfQuickBuyDeliveryType.PICKUP); + } + } + + private setBillingAddress( + address: google.payments.api.Address | undefined + ): Observable { + return this.opfQuickBuyTransactionService.setBillingAddress( + this.convertAddress(address) + ); + } + + setDeliveryMode( + mode?: string, + type?: OpfQuickBuyDeliveryType + ): Observable { + if (type === OpfQuickBuyDeliveryType.PICKUP) { + mode = OpfQuickBuyDeliveryType.PICKUP.toLocaleLowerCase(); + } + + if (!mode && type === OpfQuickBuyDeliveryType.SHIPPING) { + return of(undefined); + } + + return mode && this.verifyShippingOption(mode) + ? this.opfQuickBuyTransactionService.setDeliveryMode(mode) + : of(undefined); + } + + private convertAddress( + address: google.payments.api.Address | undefined + ): Address { + let convertedAddress: Address = { + firstName: OPF_QUICK_BUY_ADDRESS_FIELD_PLACEHOLDER, + lastName: OPF_QUICK_BUY_ADDRESS_FIELD_PLACEHOLDER, + country: { + isocode: address?.countryCode, + }, + town: address?.locality, + district: address?.administrativeArea, + postalCode: address?.postalCode, + line1: address?.address1 || OPF_QUICK_BUY_ADDRESS_FIELD_PLACEHOLDER, + line2: `${address?.address2} ${address?.address3}`, + }; + + if (address?.name) { + convertedAddress = { + ...convertedAddress, + ...this.getFirstAndLastName(address?.name), + }; + } + return convertedAddress; + } + + handleActiveCartTransaction(): Observable { + this.transactionDetails.context = OpfQuickBuyLocation.CART; + + return forkJoin({ + deliveryInfo: + this.opfQuickBuyTransactionService.getTransactionDeliveryInfo(), + merchantName: this.opfQuickBuyTransactionService.getMerchantName(), + }).pipe( + switchMap(({ deliveryInfo, merchantName }) => { + this.transactionDetails.deliveryInfo = deliveryInfo; + this.setGooglePaymentRequestConfig(deliveryInfo.type, merchantName); + + return this.setDeliveryMode(undefined, deliveryInfo.type).pipe( + switchMap(() => + this.opfQuickBuyTransactionService.getCurrentCart().pipe( + take(1), + tap((cart: Cart) => { + this.transactionDetails.cart = cart; + this.updateTransactionInfo({ + totalPrice: `${cart.totalPrice?.value}`, + currencyCode: + cart.totalPrice?.currencyIso || + this.initialTransactionInfo.currencyCode, + totalPriceStatus: + this.initialTransactionInfo.totalPriceStatus, + }); + }) + ) + ) + ); + }) + ); + } + + initTransaction(): void { + this.transactionDetails = { + ...this.initialTransactionDetails, + addressIds: [], + }; + + this.opfQuickBuyTransactionService + .getTransactionLocationContext() + .pipe( + switchMap((context: OpfQuickBuyLocation) => { + this.transactionDetails.context = context; + + return this.handleActiveCartTransaction(); + }) + ) + .subscribe(() => { + this.googlePaymentClient + .loadPaymentData(this.googlePaymentRequest) + .catch((err: any) => { + // If err.statusCode === 'CANCELED' it means that customer closed popup + if (err.statusCode === 'CANCELED') { + this.deleteAssociatedAddresses(); + } + }); + }); + } + + renderPaymentButton(container: ElementRef): void { + container.nativeElement.appendChild( + this.getClient().createButton({ + onClick: () => this.initTransaction(), + buttonSizeMode: 'fill', + }) + ); + } + + private handlePaymentCallbacks(): google.payments.api.PaymentDataCallbacks { + return { + onPaymentAuthorized: (paymentDataResponse: any) => { + return lastValueFrom( + this.opfQuickBuyTransactionService.getCurrentCartId().pipe( + switchMap((cartId) => + this.setDeliveryAddress(paymentDataResponse.shippingAddress).pipe( + switchMap(() => + this.setBillingAddress( + paymentDataResponse.paymentMethodData.info?.billingAddress + ) + ), + switchMap(() => { + const encryptedToken = btoa( + paymentDataResponse.paymentMethodData.tokenizationData.token + ); + + return this.opfPaymentFacade.submitPayment({ + additionalData: [], + paymentSessionId: '', + callbackArray: [() => {}, () => {}, () => {}], + paymentMethod: OpfProviderType.GOOGLE_PAY as any, + encryptedToken, + cartId, + }); + }) + ) + ), + catchError(() => { + return of(false); + }) + ) + ).then((isSuccess) => { + this.deleteAssociatedAddresses(); + return { transactionState: isSuccess ? 'SUCCESS' : 'ERROR' }; + }); + }, + + onPaymentDataChanged: (intermediatePaymentData: any) => { + return lastValueFrom( + this.setDeliveryAddress(intermediatePaymentData.shippingAddress).pipe( + switchMap(() => this.getShippingOptionParameters()), + switchMap((shippingOptions) => { + const selectedMode = + this.verifyShippingOption( + intermediatePaymentData.shippingOptionData?.id + ) ?? shippingOptions?.defaultSelectedOptionId; + + return this.setDeliveryMode(selectedMode).pipe( + switchMap(() => + forkJoin([ + this.opfQuickBuyTransactionService.getCurrentCart(), + this.opfQuickBuyTransactionService.getSelectedDeliveryMode(), + ]) + ), + switchMap(([cart, mode]) => { + const paymentDataRequestUpdate: google.payments.api.PaymentDataRequestUpdate = + { + newShippingOptionParameters: shippingOptions, + newTransactionInfo: this.getNewTransactionInfo(cart), + }; + + if ( + paymentDataRequestUpdate.newShippingOptionParameters + ?.defaultSelectedOptionId + ) { + paymentDataRequestUpdate.newShippingOptionParameters.defaultSelectedOptionId = + mode?.code; + } + + return of(paymentDataRequestUpdate); + }) + ); + }) + ) + ); + }, + }; + } + + protected verifyShippingOption(mode: string | undefined): string | undefined { + return mode === 'shipping_option_unselected' ? undefined : mode; + } + + protected associateAddressId(addressId: string): void { + if (!this.isAddressIdAssociated(addressId)) { + this.transactionDetails.addressIds.push(addressId); + } + } + + protected isAddressIdAssociated(addressId: string): boolean { + return this.transactionDetails.addressIds.includes(addressId); + } + + protected resetAssociatedAddresses(): void { + this.transactionDetails.addressIds = []; + } + + protected deleteAssociatedAddresses(): void { + if (this.transactionDetails.addressIds?.length) { + this.opfQuickBuyTransactionService.deleteUserAddresses( + this.transactionDetails.addressIds + ); + this.resetAssociatedAddresses(); + } + } + + protected getFirstAndLastName(name: string) { + const firstName = name?.split(' ')[0]; + const lastName = name?.substring(firstName?.length) || firstName; + + return { + firstName, + lastName, + }; + } + + protected setAllowedPaymentMethodsConfig( + activeConfiguration: ActiveConfiguration + ): void { + const googlePayConfig = + this.opfQuickBuyButtonsService.getQuickBuyProviderConfig( + OpfProviderType.GOOGLE_PAY, + activeConfiguration + ); + + this.googlePaymentRequest.allowedPaymentMethods = [ + { + parameters: { + allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], + allowedCardNetworks: [ + 'AMEX', + 'DISCOVER', + 'INTERAC', + 'JCB', + 'MASTERCARD', + 'VISA', + ], + billingAddressRequired: true, + billingAddressParameters: { + format: 'FULL', + }, + }, + tokenizationSpecification: { + parameters: { + gateway: String(googlePayConfig?.googlePayGateway), + gatewayMerchantId: String(activeConfiguration.merchantId), + }, + type: activeConfiguration.providerType as any, + }, + type: 'CARD', + }, + ]; + } +} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/index.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/index.ts new file mode 100644 index 00000000000..1aa81a7756d --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/google-pay/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './google-pay.component'; +export * from './google-pay.module'; +export * from './google-pay.service'; diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/index.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/index.ts new file mode 100644 index 00000000000..dea3f4ca18b --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './opf-quick-buy-buttons.component'; +export * from './opf-quick-buy-buttons.module'; +export * from './opf-quick-buy-buttons.service'; diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.html b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.html new file mode 100644 index 00000000000..f9034582f71 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.html @@ -0,0 +1,16 @@ + + + + + + diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.spec.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.spec.ts new file mode 100644 index 00000000000..781396a8db6 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.spec.ts @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterState, RoutingService } from '@spartacus/core'; +import { BehaviorSubject, of } from 'rxjs'; +import { OpfProviderType } from '../../root/model'; +import { OpfQuickBuyButtonsComponent } from './opf-quick-buy-buttons.component'; +import { OpfQuickBuyButtonsService } from './opf-quick-buy-buttons.service'; +import createSpy = jasmine.createSpy; + +const routerStateSubject = new BehaviorSubject({ + state: { + semanticRoute: 'cart', + }, +} as unknown as RouterState); + +class MockRoutingService implements Partial { + getRouterState = createSpy().and.returnValue( + routerStateSubject.asObservable() + ); +} + +describe('OpfQuickBuyButtonsComponent', () => { + let component: OpfQuickBuyButtonsComponent; + let fixture: ComponentFixture; + let opfQuickBuyButtonsServiceMock: any; + + beforeEach(async () => { + opfQuickBuyButtonsServiceMock = jasmine.createSpyObj('OpfQuickBuyService', [ + 'getPaymentGatewayConfiguration', + 'isUserGuestOrLoggedIn', + 'isQuickBuyProviderEnabled', + ]); + + await TestBed.configureTestingModule({ + declarations: [OpfQuickBuyButtonsComponent], + providers: [ + { + provide: OpfQuickBuyButtonsService, + useValue: opfQuickBuyButtonsServiceMock, + }, + { provide: RoutingService, useValie: MockRoutingService }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OpfQuickBuyButtonsComponent); + component = fixture.componentInstance; + + opfQuickBuyButtonsServiceMock.getPaymentGatewayConfiguration.and.returnValue( + of({}) + ); + opfQuickBuyButtonsServiceMock.isUserGuestOrLoggedIn.and.returnValue(of({})); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call getPaymentGatewayConfiguration on init', () => { + expect( + opfQuickBuyButtonsServiceMock.getPaymentGatewayConfiguration + ).toHaveBeenCalled(); + }); + + it('should call isUserGuestOrLoggedIn on init', () => { + expect( + opfQuickBuyButtonsServiceMock.isUserGuestOrLoggedIn + ).toHaveBeenCalled(); + }); + + it('should determine if a payment method is enabled', () => { + const provider = OpfProviderType.APPLE_PAY; + const activeConfiguration = {}; + opfQuickBuyButtonsServiceMock.isQuickBuyProviderEnabled.and.returnValue( + true + ); + + expect( + component.isPaymentMethodEnabled(provider, activeConfiguration) + ).toBeTruthy(); + expect( + opfQuickBuyButtonsServiceMock.isQuickBuyProviderEnabled + ).toHaveBeenCalledWith(provider, activeConfiguration); + }); +}); diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.ts new file mode 100644 index 00000000000..ec9108a0886 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.component.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + Component, + OnInit, + inject, +} from '@angular/core'; +import { ActiveConfiguration } from '@spartacus/opf/base/root'; +import { OpfProviderType } from '@spartacus/opf/quick-buy/root'; +import { Observable } from 'rxjs'; +import { OpfQuickBuyButtonsService } from './opf-quick-buy-buttons.service'; + +@Component({ + selector: 'cx-opf-quick-buy-buttons', + templateUrl: './opf-quick-buy-buttons.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OpfQuickBuyButtonsComponent implements OnInit { + protected opfQuickBuyButtonsService = inject(OpfQuickBuyButtonsService); + protected paymentGatewayConfig$: Observable; + protected isUserGuestOrLoggedIn$: Observable; + + PAYMENT_METHODS = OpfProviderType; + + ngOnInit(): void { + this.paymentGatewayConfig$ = + this.opfQuickBuyButtonsService.getPaymentGatewayConfiguration(); + this.isUserGuestOrLoggedIn$ = + this.opfQuickBuyButtonsService.isUserGuestOrLoggedIn(); + } + + isPaymentMethodEnabled( + provider: OpfProviderType, + activeConfiguration: ActiveConfiguration + ): boolean { + return this.opfQuickBuyButtonsService.isQuickBuyProviderEnabled( + provider, + activeConfiguration + ); + } +} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.module.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.module.ts new file mode 100644 index 00000000000..1b3400e9e5e --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.module.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { CmsConfig, provideDefaultConfig } from '@spartacus/core'; +import { OpfApplePayModule } from './apple-pay'; +import { OpfGooglePayModule } from './google-pay/google-pay.module'; +import { OpfQuickBuyButtonsComponent } from './opf-quick-buy-buttons.component'; +import { OpfQuickBuyButtonsService } from './opf-quick-buy-buttons.service'; + +@NgModule({ + declarations: [OpfQuickBuyButtonsComponent], + providers: [ + OpfQuickBuyButtonsService, + provideDefaultConfig({ + cmsComponents: { + OpfQuickBuyButtonsComponent: { + component: OpfQuickBuyButtonsComponent, + }, + }, + }), + ], + exports: [OpfQuickBuyButtonsComponent], + imports: [CommonModule, OpfApplePayModule, OpfGooglePayModule], +}) +export class OpfQuickBuyButtonsModule {} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.service.spec.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.service.spec.ts new file mode 100644 index 00000000000..9a8e3a1115e --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.service.spec.ts @@ -0,0 +1,267 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TestBed } from '@angular/core/testing'; +import { StoreModule } from '@ngrx/store'; +import { CheckoutConfig } from '@spartacus/checkout/base/root'; +import { + AuthService, + BaseSiteService, + QueryState, + RoutingService, +} from '@spartacus/core'; +import { + ActiveConfiguration, + OpfBaseFacade, + OpfPaymentProviderType, +} from '@spartacus/opf/base/root'; +import { of, throwError } from 'rxjs'; +import { OpfProviderType, OpfQuickBuyDigitalWallet } from '../../root/model'; +import { OpfQuickBuyButtonsService } from './opf-quick-buy-buttons.service'; + +describe('OpfQuickBuyService', () => { + let service: OpfQuickBuyButtonsService; + let opfBaseFacadeMock: jasmine.SpyObj; + let baseSiteServiceMock: jasmine.SpyObj; + let authServiceMock: jasmine.SpyObj; + let checkoutConfigMock: jasmine.SpyObj; + let routingServiceMock: jasmine.SpyObj; + + beforeEach(() => { + opfBaseFacadeMock = jasmine.createSpyObj('OpfBaseFacade', [ + 'getActiveConfigurationsState', + ]); + baseSiteServiceMock = jasmine.createSpyObj('BaseSiteService', ['get']); + authServiceMock = jasmine.createSpyObj('AuthService', ['isUserLoggedIn']); + checkoutConfigMock = jasmine.createSpyObj('CheckoutConfig', ['checkout']); + routingServiceMock = jasmine.createSpyObj('RoutingService', [ + 'getRouterState', + ]); + + TestBed.configureTestingModule({ + imports: [StoreModule.forRoot({})], + providers: [ + OpfQuickBuyButtonsService, + { provide: OpfBaseFacade, useValue: opfBaseFacadeMock }, + { provide: BaseSiteService, useValue: baseSiteServiceMock }, + { provide: AuthService, useValue: authServiceMock }, + { provide: CheckoutConfig, useValue: checkoutConfigMock }, + { provide: RoutingService, useValue: routingServiceMock }, + ], + }); + + service = TestBed.inject(OpfQuickBuyButtonsService); + }); + + describe('getPaymentGatewayConfiguration', () => { + it('should return the first PAYMENT_GATEWAY configuration when available', () => { + const mockConfigurations = [ + { providerType: OpfPaymentProviderType.PAYMENT_METHOD }, + { providerType: OpfPaymentProviderType.PAYMENT_GATEWAY }, + { providerType: OpfPaymentProviderType.PAYMENT_GATEWAY }, + ]; + opfBaseFacadeMock.getActiveConfigurationsState.and.returnValue( + of({ data: mockConfigurations } as QueryState) + ); + + service.getPaymentGatewayConfiguration().subscribe((result) => { + expect(result).toEqual(mockConfigurations[1]); + }); + }); + + it('should return undefined when there are no active configurations', () => { + opfBaseFacadeMock.getActiveConfigurationsState.and.returnValue( + of({ data: undefined } as QueryState) + ); + + service.getPaymentGatewayConfiguration().subscribe((result) => { + expect(result).toBeUndefined(); + }); + }); + + it('should return undefined when no configuration matches PAYMENT_GATEWAY type', () => { + const mockConfigurations = [{ providerType: 'SOME_OTHER_TYPE' }]; + opfBaseFacadeMock.getActiveConfigurationsState.and.returnValue( + of({ data: mockConfigurations } as QueryState) + ); + + service.getPaymentGatewayConfiguration().subscribe((result) => { + expect(result).toBeUndefined(); + }); + }); + + it('should handle errors when fetching configurations', () => { + const error = new Error('Network error'); + opfBaseFacadeMock.getActiveConfigurationsState.and.returnValue( + throwError(error) + ); + + service.getPaymentGatewayConfiguration().subscribe( + () => fail('Expected an error, not configurations'), + (err) => expect(err).toBe(error) + ); + }); + + it('should return an empty array when config.data is undefined', () => { + opfBaseFacadeMock.getActiveConfigurationsState.and.returnValue( + of({} as QueryState) + ); + + service.getPaymentGatewayConfiguration().subscribe((result) => { + expect(result).toBeUndefined(); + }); + }); + }); + + describe('isQuickBuyProviderEnabled', () => { + const provider = OpfProviderType.APPLE_PAY; + + it('should return true when the provider is enabled', () => { + const activeConfiguration = { + digitalWalletQuickBuy: [ + { provider: OpfProviderType.APPLE_PAY, enabled: true }, + ], + }; + + const result = service.isQuickBuyProviderEnabled( + provider, + activeConfiguration + ); + expect(result).toBeTruthy(); + }); + + it('should return false when the provider is disabled', () => { + const activeConfiguration = { + digitalWalletQuickBuy: [ + { provider: OpfProviderType.APPLE_PAY, enabled: false }, + ], + }; + + const result = service.isQuickBuyProviderEnabled( + provider, + activeConfiguration + ); + expect(result).toBeFalsy(); + }); + + it('should return false when the provider is not found', () => { + const activeConfiguration = { + digitalWalletQuickBuy: [ + { provider: 'otherProvider' as any, enabled: true }, + ], + }; + + const result = service.isQuickBuyProviderEnabled( + provider, + activeConfiguration + ); + expect(result).toBeFalsy(); + }); + + it('should return false when activeConfiguration is null', () => { + const result = service.isQuickBuyProviderEnabled(provider, null as any); + expect(result).toBeFalsy(); + }); + + it('should return false when activeConfiguration is undefined', () => { + const result = service.isQuickBuyProviderEnabled( + provider, + undefined as any + ); + expect(result).toBeFalsy(); + }); + + it('should return false when digitalWalletQuickBuy is null or empty', () => { + const provider = OpfProviderType.APPLE_PAY; + const activeConfiguration = { + digitalWalletQuickBuy: null as any, + }; + + const result = service.isQuickBuyProviderEnabled( + provider, + activeConfiguration + ); + expect(result).toBeFalsy(); + }); + }); + + describe('isUserGuestOrLoggedIn', () => { + it('should return true if the user is logged in', () => { + baseSiteServiceMock.get.and.returnValue( + of({ baseStore: { paymentProvider: 'providerWithGuestSupport' } }) + ); + authServiceMock.isUserLoggedIn.and.returnValue(of(true)); + + service.isUserGuestOrLoggedIn().subscribe((result) => { + expect(result).toBeTruthy(); + }); + }); + + it('should return true if the user is a guest and guest checkout is supported', () => { + baseSiteServiceMock.get.and.returnValue( + of({ baseStore: { paymentProvider: 'providerWithGuestSupport' } }) + ); + + authServiceMock.isUserLoggedIn.and.returnValue(of(false)); + checkoutConfigMock.checkout.flows = { + providerWithGuestSupport: { guest: true }, + }; + + service.isUserGuestOrLoggedIn().subscribe((result) => { + expect(result).toBeTruthy(); + }); + }); + + it('should return false if the user is not logged in and guest checkout is not supported', () => { + baseSiteServiceMock.get.and.returnValue( + of({ baseStore: { paymentProvider: 'providerWithoutGuestSupport' } }) + ); + authServiceMock.isUserLoggedIn.and.returnValue(of(false)); + checkoutConfigMock.checkout.flows = { + providerWithoutGuestSupport: { guest: false }, + }; + + service.isUserGuestOrLoggedIn().subscribe((result) => { + expect(result).toBeFalsy(); + }); + }); + + it('should handle errors appropriately', () => { + const error = new Error('Network error'); + baseSiteServiceMock.get.and.returnValue(throwError(error)); + + service.isUserGuestOrLoggedIn().subscribe( + () => fail('Expected an error, not a successful response'), + (err) => expect(err).toBe(error) + ); + }); + }); + + describe('getQuickBuyProviderConfig', () => { + const config: OpfQuickBuyDigitalWallet = { + provider: OpfProviderType.GOOGLE_PAY, + googlePayGateway: 'test', + merchantId: 'test', + merchantName: 'test', + enabled: true, + }; + + it('should return config for specific provider', () => { + const activeConfiguration = { + digitalWalletQuickBuy: [ + { provider: OpfProviderType.APPLE_PAY, enabled: true }, + config, + ], + }; + + const result = service.getQuickBuyProviderConfig( + OpfProviderType.GOOGLE_PAY, + activeConfiguration + ); + expect(result).toBe(config); + }); + }); +}); diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.service.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.service.ts new file mode 100644 index 00000000000..2ccc46cb68b --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-buttons/opf-quick-buy-buttons.service.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable, inject } from '@angular/core'; +import { CheckoutConfig } from '@spartacus/checkout/base/root'; +import { AuthService, BaseSiteService } from '@spartacus/core'; +import { + ActiveConfiguration, + OpfBaseFacade, + OpfPaymentProviderType, +} from '@spartacus/opf/base/root'; +import { + OpfProviderType, + OpfQuickBuyDigitalWallet, +} from '@spartacus/opf/quick-buy/root'; +import { Observable, of } from 'rxjs'; +import { map, switchMap, take } from 'rxjs/operators'; + +@Injectable() +export class OpfQuickBuyButtonsService { + protected opfBaseFacade = inject(OpfBaseFacade); + protected checkoutConfig = inject(CheckoutConfig); + protected baseSiteService = inject(BaseSiteService); + protected authService = inject(AuthService); + + getPaymentGatewayConfiguration(): Observable { + return this.opfBaseFacade + .getActiveConfigurationsState() + .pipe( + map( + (config) => + (config?.data || []).filter( + (item) => + item?.providerType === OpfPaymentProviderType.PAYMENT_GATEWAY + )[0] + ) + ); + } + + getQuickBuyProviderConfig( + provider: OpfProviderType, + activeConfiguration: ActiveConfiguration + ): OpfQuickBuyDigitalWallet | undefined { + let config; + if (activeConfiguration && activeConfiguration.digitalWalletQuickBuy) { + config = activeConfiguration?.digitalWalletQuickBuy.find( + (item) => item.provider === provider + ); + } + + return config; + } + + isQuickBuyProviderEnabled( + provider: OpfProviderType, + activeConfiguration: ActiveConfiguration + ): boolean { + let isEnabled = false; + if (activeConfiguration && activeConfiguration.digitalWalletQuickBuy) { + isEnabled = Boolean( + activeConfiguration?.digitalWalletQuickBuy.find( + (item) => item.provider === provider + )?.enabled + ); + } + + return isEnabled; + } + + isUserGuestOrLoggedIn(): Observable { + return this.baseSiteService.get().pipe( + take(1), + map((baseSite) => baseSite?.baseStore?.paymentProvider), + switchMap((paymentProviderName) => { + return paymentProviderName && + this.checkoutConfig.checkout?.flows?.[paymentProviderName]?.guest + ? of(true) + : this.authService.isUserLoggedIn(); + }) + ); + } +} diff --git a/integration-libs/opf/quick-buy/components/opf-quick-buy-components.module.ts b/integration-libs/opf/quick-buy/components/opf-quick-buy-components.module.ts new file mode 100644 index 00000000000..71ea7e1bcae --- /dev/null +++ b/integration-libs/opf/quick-buy/components/opf-quick-buy-components.module.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { OpfQuickBuyButtonsModule } from './opf-quick-buy-buttons/opf-quick-buy-buttons.module'; + +@NgModule({ + imports: [OpfQuickBuyButtonsModule], +}) +export class OpfQuickBuyComponentsModule {} diff --git a/integration-libs/opf/quick-buy/components/public_api.ts b/integration-libs/opf/quick-buy/components/public_api.ts new file mode 100644 index 00000000000..cbc0b00f2f2 --- /dev/null +++ b/integration-libs/opf/quick-buy/components/public_api.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './opf-quick-buy-components.module'; diff --git a/integration-libs/opf/quick-buy/core/connectors/index.ts b/integration-libs/opf/quick-buy/core/connectors/index.ts new file mode 100644 index 00000000000..5319ac93b18 --- /dev/null +++ b/integration-libs/opf/quick-buy/core/connectors/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './opf-quick-buy.adapter'; +export * from './opf-quick-buy.connector'; diff --git a/integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.adapter.ts b/integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.adapter.ts new file mode 100644 index 00000000000..20846828238 --- /dev/null +++ b/integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.adapter.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ApplePaySessionVerificationRequest, + ApplePaySessionVerificationResponse, +} from '@spartacus/opf/quick-buy/root'; +import { Observable } from 'rxjs'; + +export abstract class OpfQuickBuyAdapter { + /** + * Abstract method used to request an ApplePay session, used by QuickBuy functionality + */ + abstract getApplePayWebSession( + applePayRequest: ApplePaySessionVerificationRequest, + otpKey: string + ): Observable; +} diff --git a/integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.connector.spec.ts b/integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.connector.spec.ts new file mode 100644 index 00000000000..cc4bc3d389d --- /dev/null +++ b/integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.connector.spec.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TestBed } from '@angular/core/testing'; +import { + ApplePaySessionVerificationRequest, + ApplePaySessionVerificationResponse, +} from '@spartacus/opf/quick-buy/root'; +import { of } from 'rxjs'; +import { OpfQuickBuyAdapter } from './opf-quick-buy.adapter'; +import { OpfQuickBuyConnector } from './opf-quick-buy.connector'; +import createSpy = jasmine.createSpy; + +const mockGetApplePayWebSessionRequest: ApplePaySessionVerificationRequest = { + cartId: 'test', + validationUrl: 'test', + initiative: 'web', + initiativeContext: 'test', +}; + +const mockGetApplePayWebSessionResponse: ApplePaySessionVerificationResponse = { + epochTimestamp: 1, + expiresAt: 60000, + merchantSessionIdentifier: 'test', + nonce: 'test', + merchantIdentifier: 'test', + domainName: 'test', + displayName: 'test', + signature: 'test', +}; + +const mockAccessCode = 'accessCode'; + +class MockOpfQuickBuyAdapter implements OpfQuickBuyAdapter { + getApplePayWebSession = createSpy('getApplePayWebSession').and.callFake(() => + of(mockGetApplePayWebSessionResponse) + ); +} + +describe('OpfQuickBuyConnector', () => { + let service: OpfQuickBuyConnector; + let adapter: OpfQuickBuyAdapter; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + OpfQuickBuyConnector, + { provide: OpfQuickBuyAdapter, useClass: MockOpfQuickBuyAdapter }, + ], + }); + + service = TestBed.inject(OpfQuickBuyConnector); + adapter = TestBed.inject(OpfQuickBuyAdapter); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('getApplePayWebSession should call adapter', () => { + let result; + service + .getApplePayWebSession(mockGetApplePayWebSessionRequest, mockAccessCode) + .subscribe((res) => (result = res)); + expect(result).toEqual(mockGetApplePayWebSessionResponse); + expect(adapter.getApplePayWebSession).toHaveBeenCalledWith( + mockGetApplePayWebSessionRequest, + mockAccessCode + ); + }); +}); diff --git a/integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.connector.ts b/integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.connector.ts new file mode 100644 index 00000000000..416f63e782e --- /dev/null +++ b/integration-libs/opf/quick-buy/core/connectors/opf-quick-buy.connector.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { + ApplePaySessionVerificationRequest, + ApplePaySessionVerificationResponse, +} from '@spartacus/opf/quick-buy/root'; +import { Observable } from 'rxjs'; +import { OpfQuickBuyAdapter } from './opf-quick-buy.adapter'; + +@Injectable() +export class OpfQuickBuyConnector { + constructor(protected adapter: OpfQuickBuyAdapter) {} + + public getApplePayWebSession( + applePayWebRequest: ApplePaySessionVerificationRequest, + accessCode: string + ): Observable { + return this.adapter.getApplePayWebSession(applePayWebRequest, accessCode); + } +} diff --git a/integration-libs/opf/quick-buy/core/facade/facade-providers.ts b/integration-libs/opf/quick-buy/core/facade/facade-providers.ts new file mode 100644 index 00000000000..277cb27157c --- /dev/null +++ b/integration-libs/opf/quick-buy/core/facade/facade-providers.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Provider } from '@angular/core'; +import { OpfQuickBuyFacade } from '@spartacus/opf/quick-buy/root'; +import { OpfQuickBuyService } from './opf-quick-buy.service'; + +export const facadeProviders: Provider[] = [ + OpfQuickBuyService, + { + provide: OpfQuickBuyFacade, + useExisting: OpfQuickBuyService, + }, +]; diff --git a/integration-libs/opf/quick-buy/core/facade/index.ts b/integration-libs/opf/quick-buy/core/facade/index.ts new file mode 100644 index 00000000000..b71ebd71bf7 --- /dev/null +++ b/integration-libs/opf/quick-buy/core/facade/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './opf-quick-buy.service'; diff --git a/integration-libs/opf/quick-buy/core/facade/opf-quick-buy.service.spec.ts b/integration-libs/opf/quick-buy/core/facade/opf-quick-buy.service.spec.ts new file mode 100644 index 00000000000..a2ad6968c94 --- /dev/null +++ b/integration-libs/opf/quick-buy/core/facade/opf-quick-buy.service.spec.ts @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TestBed } from '@angular/core/testing'; +import { + ActiveCartFacade, + CartAccessCodeFacade, +} from '@spartacus/cart/base/root'; +import { UserIdService } from '@spartacus/core'; +import { + ApplePaySessionVerificationRequest, + ApplePaySessionVerificationResponse, +} from '@spartacus/opf/quick-buy/root'; +import { of } from 'rxjs'; +import { OpfQuickBuyConnector } from '../connectors'; +import { OpfQuickBuyService } from './opf-quick-buy.service'; + +const mockGetApplePayWebSessionRequest: ApplePaySessionVerificationRequest = { + cartId: 'test', + validationUrl: 'test', + initiative: 'web', + initiativeContext: 'test', +}; + +const mockGetApplePayWebSessionResponse: ApplePaySessionVerificationResponse = { + epochTimestamp: 1, + expiresAt: 60000, + merchantSessionIdentifier: 'test', + nonce: 'test', + merchantIdentifier: 'test', + domainName: 'test', + displayName: 'test', + signature: 'test', +}; + +const mockAccessCode = 'mockAccessCode'; + +describe('OpfQuickBuyService', () => { + let service: OpfQuickBuyService; + let opfQuickBuyConnector: jasmine.SpyObj; + let cartAccessCodeFacade: jasmine.SpyObj; + let activeCartFacade: jasmine.SpyObj; + let userIdService: jasmine.SpyObj; + + beforeEach(() => { + const opfQuickBuyConnectorSpy = jasmine.createSpyObj( + 'OpfQuickBuyConnector', + ['getApplePayWebSession'] + ); + const cartAccessCodeFacadeSpy = jasmine.createSpyObj( + 'CartAccessCodeFacade', + ['getCartAccessCode'] + ); + const activeCartFacadeSpy = jasmine.createSpyObj('ActiveCartFacade', [ + 'getActiveCartId', + ]); + const userIdServiceSpy = jasmine.createSpyObj('UserIdService', [ + 'getUserId', + ]); + + TestBed.configureTestingModule({ + providers: [ + OpfQuickBuyService, + { provide: OpfQuickBuyConnector, useValue: opfQuickBuyConnectorSpy }, + { provide: CartAccessCodeFacade, useValue: cartAccessCodeFacadeSpy }, + { provide: ActiveCartFacade, useValue: activeCartFacadeSpy }, + { provide: UserIdService, useValue: userIdServiceSpy }, + ], + }); + + service = TestBed.inject(OpfQuickBuyService); + opfQuickBuyConnector = TestBed.inject( + OpfQuickBuyConnector + ) as jasmine.SpyObj; + cartAccessCodeFacade = TestBed.inject( + CartAccessCodeFacade + ) as jasmine.SpyObj; + activeCartFacade = TestBed.inject( + ActiveCartFacade + ) as jasmine.SpyObj; + userIdService = TestBed.inject( + UserIdService + ) as jasmine.SpyObj; + }); + + it('should successfully get ApplePay web session', (done) => { + userIdService.getUserId.and.returnValue(of('mockUserId')); + activeCartFacade.getActiveCartId.and.returnValue(of('mockCartId')); + cartAccessCodeFacade.getCartAccessCode.and.returnValue( + of({ accessCode: mockAccessCode }) + ); + opfQuickBuyConnector.getApplePayWebSession.and.returnValue( + of(mockGetApplePayWebSessionResponse) + ); + + service + .getApplePayWebSession(mockGetApplePayWebSessionRequest) + .subscribe((response) => { + expect(response).toEqual(mockGetApplePayWebSessionResponse); + expect(opfQuickBuyConnector.getApplePayWebSession).toHaveBeenCalledWith( + mockGetApplePayWebSessionRequest, + mockAccessCode + ); + done(); + }); + }); +}); diff --git a/integration-libs/opf/quick-buy/core/facade/opf-quick-buy.service.ts b/integration-libs/opf/quick-buy/core/facade/opf-quick-buy.service.ts new file mode 100644 index 00000000000..779d71908fd --- /dev/null +++ b/integration-libs/opf/quick-buy/core/facade/opf-quick-buy.service.ts @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { + ActiveCartFacade, + CartAccessCodeFacade, +} from '@spartacus/cart/base/root'; +import { + backOff, + Command, + CommandService, + DEFAULT_AUTHORIZATION_ERROR_RETRIES_COUNT, + isAuthorizationError, + LoggerService, + tryNormalizeHttpError, + UserIdService, +} from '@spartacus/core'; +import { + ApplePaySessionVerificationRequest, + ApplePaySessionVerificationResponse, + OpfQuickBuyFacade, +} from '@spartacus/opf/quick-buy/root'; +import { + catchError, + combineLatest, + concatMap, + filter, + switchMap, + take, +} from 'rxjs'; +import { OpfQuickBuyConnector } from '../connectors'; + +@Injectable() +export class OpfQuickBuyService implements OpfQuickBuyFacade { + protected applePaySessionCommand: Command< + { + applePayWebSessionRequest: ApplePaySessionVerificationRequest; + }, + ApplePaySessionVerificationResponse + > = this.commandService.create((payload) => { + return combineLatest([ + this.userIdService.getUserId(), + this.activeCartFacade.getActiveCartId(), + ]).pipe( + filter( + ([userId, activeCartId]: [string, string]) => !!activeCartId && !!userId + ), + switchMap(([userId, activeCartId]: [string, string]) => { + return this.cartAccessCodeFacade.getCartAccessCode( + userId, + activeCartId + ); + }), + filter((response) => Boolean(response?.accessCode)), + take(1), + concatMap(({ accessCode: accessCode }) => { + return this.opfQuickBuyConnector.getApplePayWebSession( + payload.applePayWebSessionRequest, + accessCode + ); + }), + catchError((error) => { + throw tryNormalizeHttpError(error, this.logger); + }), + backOff({ + shouldRetry: isAuthorizationError, + maxTries: DEFAULT_AUTHORIZATION_ERROR_RETRIES_COUNT, + }) + ); + }); + + constructor( + protected commandService: CommandService, + protected opfQuickBuyConnector: OpfQuickBuyConnector, + protected cartAccessCodeFacade: CartAccessCodeFacade, + protected activeCartFacade: ActiveCartFacade, + protected userIdService: UserIdService, + protected logger: LoggerService + ) {} + + getApplePayWebSession( + applePayWebSessionRequest: ApplePaySessionVerificationRequest + ) { + return this.applePaySessionCommand.execute({ applePayWebSessionRequest }); + } +} diff --git a/integration-libs/opf/quick-buy/core/ng-package.json b/integration-libs/opf/quick-buy/core/ng-package.json new file mode 100644 index 00000000000..2a9c6221b0e --- /dev/null +++ b/integration-libs/opf/quick-buy/core/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/integration-libs/opf/quick-buy/core/opf-quick-buy-core.module.ts b/integration-libs/opf/quick-buy/core/opf-quick-buy-core.module.ts new file mode 100644 index 00000000000..d5672dd6a9f --- /dev/null +++ b/integration-libs/opf/quick-buy/core/opf-quick-buy-core.module.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { OpfQuickBuyConnector } from './connectors'; +import { facadeProviders } from './facade/facade-providers'; + +@NgModule({ + imports: [], + providers: [...facadeProviders, OpfQuickBuyConnector], +}) +export class OpfQuickBuyCoreModule {} diff --git a/integration-libs/opf/quick-buy/core/public_api.ts b/integration-libs/opf/quick-buy/core/public_api.ts new file mode 100644 index 00000000000..6f213071e9c --- /dev/null +++ b/integration-libs/opf/quick-buy/core/public_api.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './connectors/index'; +export * from './facade/index'; +export * from './opf-quick-buy-core.module'; +export * from './services/index'; +export * from './tokens/index'; diff --git a/integration-libs/opf/quick-buy/core/services/index.ts b/integration-libs/opf/quick-buy/core/services/index.ts new file mode 100644 index 00000000000..372f83fa89e --- /dev/null +++ b/integration-libs/opf/quick-buy/core/services/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './opf-quick-buy-transaction.service'; diff --git a/integration-libs/opf/quick-buy/core/services/opf-quick-buy-transaction.service.spec.ts b/integration-libs/opf/quick-buy/core/services/opf-quick-buy-transaction.service.spec.ts new file mode 100644 index 00000000000..a8b476f97d8 --- /dev/null +++ b/integration-libs/opf/quick-buy/core/services/opf-quick-buy-transaction.service.spec.ts @@ -0,0 +1,471 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fakeAsync, flush, TestBed } from '@angular/core/testing'; +import { StoreModule } from '@ngrx/store'; +import { + ActiveCartFacade, + Cart, + DeliveryMode, + MultiCartFacade, +} from '@spartacus/cart/base/root'; +import { + CheckoutBillingAddressFacade, + CheckoutDeliveryAddressFacade, + CheckoutDeliveryModesFacade, +} from '@spartacus/checkout/base/root'; +import { + Address, + BaseSiteService, + EventService, + RouterState, + RoutingService, + UserAddressService, + UserIdService, +} from '@spartacus/core'; +import { OpfGlobalMessageService } from '@spartacus/opf/base/root'; +import { + OPF_QUICK_BUY_DEFAULT_MERCHANT_NAME, + OpfQuickBuyLocation, +} from '@spartacus/opf/quick-buy/root'; +import { BehaviorSubject, of, throwError } from 'rxjs'; +import { OpfQuickBuyTransactionService } from './opf-quick-buy-transaction.service'; + +describe('OpfQuickBuyTransactionService', () => { + let service: OpfQuickBuyTransactionService; + let activeCartFacade: jasmine.SpyObj; + let checkoutDeliveryModesFacade: jasmine.SpyObj; + let checkoutDeliveryAddressFacade: jasmine.SpyObj; + let userAddressService: jasmine.SpyObj; + let multiCartFacade: jasmine.SpyObj; + let userIdService: jasmine.SpyObj; + let eventService: jasmine.SpyObj; + let checkoutBillingAddressFacade: jasmine.SpyObj; + let baseSiteService: jasmine.SpyObj; + let routingService: jasmine.SpyObj; + let opfGlobalMessageService: jasmine.SpyObj; + + beforeEach(() => { + activeCartFacade = jasmine.createSpyObj('ActiveCartFacade', [ + 'addEntry', + 'isStable', + 'takeActive', + 'getActive', + 'deleteCart', + 'takeActiveCartId', + 'getActiveCartId', + ]); + checkoutDeliveryModesFacade = jasmine.createSpyObj( + 'CheckoutDeliveryModesFacade', + [ + 'getSupportedDeliveryModes', + 'setDeliveryMode', + 'getSelectedDeliveryModeState', + ] + ); + checkoutDeliveryAddressFacade = jasmine.createSpyObj( + 'CheckoutDeliveryAddressFacade', + ['createAndSetAddress', 'getDeliveryAddressState'] + ); + userAddressService = jasmine.createSpyObj('UserAddressService', [ + 'deleteUserAddress', + ]); + multiCartFacade = jasmine.createSpyObj('MultiCartFacade', [ + 'deleteCart', + 'getCartIdByType', + 'createCart', + 'addEntry', + 'isStable', + 'loadCart', + 'getEntry', + 'removeEntry', + 'updateEntry', + ]); + userIdService = jasmine.createSpyObj('UserIdService', [ + 'getUserId', + 'takeUserId', + ]); + eventService = jasmine.createSpyObj('EventService', ['get']); + checkoutBillingAddressFacade = jasmine.createSpyObj( + 'CheckoutBillingAddressFacade', + ['setBillingAddress'] + ); + baseSiteService = jasmine.createSpyObj('BaseSiteService', ['get']); + routingService = jasmine.createSpyObj('RoutingService', ['getRouterState']); + opfGlobalMessageService = jasmine.createSpyObj('OpfGlobalMessageService', [ + 'disableGlobalMessage', + ]); + + TestBed.configureTestingModule({ + imports: [StoreModule.forRoot({})], + providers: [ + OpfQuickBuyTransactionService, + { provide: ActiveCartFacade, useValue: activeCartFacade }, + { + provide: CheckoutDeliveryModesFacade, + useValue: checkoutDeliveryModesFacade, + }, + { + provide: CheckoutDeliveryAddressFacade, + useValue: checkoutDeliveryAddressFacade, + }, + { provide: UserAddressService, useValue: userAddressService }, + { provide: MultiCartFacade, useValue: multiCartFacade }, + { provide: UserIdService, useValue: userIdService }, + { provide: EventService, useValue: eventService }, + { + provide: CheckoutBillingAddressFacade, + useValue: checkoutBillingAddressFacade, + }, + { provide: BaseSiteService, useValue: baseSiteService }, + { provide: RoutingService, useValue: routingService }, + { provide: OpfGlobalMessageService, useValue: opfGlobalMessageService }, + ], + }); + + service = TestBed.inject(OpfQuickBuyTransactionService); + }); + + describe('checkStableCart', () => { + it('should return true if the cart is stable', fakeAsync(() => { + activeCartFacade.isStable.and.returnValue(of(true)); + multiCartFacade.isStable.and.returnValue(of(true)); + + service.checkStableCart().subscribe((isStable) => { + expect(isStable).toBeTruthy(); + flush(); + }); + })); + }); + + describe('getSupportedDeliveryModes', () => { + it('should return an observable of delivery modes', (done) => { + const mockDeliveryModes: DeliveryMode[] = [ + { code: 'standard', name: 'Standard Delivery' }, + { code: 'express', name: 'Express Delivery' }, + ]; + + checkoutDeliveryModesFacade.getSupportedDeliveryModes.and.returnValue( + of(mockDeliveryModes) + ); + + service.getSupportedDeliveryModes().subscribe((deliveryModes) => { + expect(deliveryModes).toEqual(mockDeliveryModes); + done(); + }); + }); + }); + + describe('setDeliveryAddress', () => { + it('should set the delivery address and return its ID', (done) => { + const mockAddress: Address = {}; + const mockAddressId = 'addressId'; + + activeCartFacade.isStable.and.returnValue(of(true)); + checkoutDeliveryAddressFacade.createAndSetAddress.and.returnValue( + of(null) + ); + checkoutDeliveryAddressFacade.getDeliveryAddressState.and.returnValue( + of({ + loading: false, + error: false, + data: { id: mockAddressId }, + }) + ); + + service.setDeliveryAddress(mockAddress).subscribe((addressId) => { + expect(addressId).toEqual(mockAddressId); + expect( + checkoutDeliveryAddressFacade.createAndSetAddress + ).toHaveBeenCalledWith(mockAddress); + done(); + }); + }); + + it('should handle an undefined address', (done) => { + const mockAddress: Address = {}; + + checkoutDeliveryAddressFacade.createAndSetAddress.and.returnValue( + of(null) + ); + activeCartFacade.isStable.and.returnValue(of(true)); + checkoutDeliveryAddressFacade.getDeliveryAddressState.and.returnValue( + of({ + loading: false, + error: false, + data: undefined, + }) + ); + + service.setDeliveryAddress(mockAddress).subscribe((result) => { + expect(result).toEqual(''); + done(); + }); + }); + + it('should handle an address without an id', (done) => { + const mockAddress: Address = { + firstName: 'John', + lastName: 'Doe', + }; + + checkoutDeliveryAddressFacade.createAndSetAddress.and.returnValue( + of(null) + ); + activeCartFacade.isStable.and.returnValue(of(true)); + checkoutDeliveryAddressFacade.getDeliveryAddressState.and.returnValue( + of({ + loading: false, + error: false, + data: mockAddress, + }) + ); + + service.setDeliveryAddress(mockAddress).subscribe((result) => { + expect(result).toEqual(''); + done(); + }); + }); + }); + + describe('setBillingAddress', () => { + it('should set the billing address and check if the cart is stable', (done) => { + const mockAddress: Address = {}; + checkoutBillingAddressFacade.setBillingAddress.and.returnValue(of(true)); + + activeCartFacade.isStable.and.returnValue(of(true)); + + service.setBillingAddress(mockAddress).subscribe((result) => { + expect(result).toBeTruthy(); + expect( + checkoutBillingAddressFacade.setBillingAddress + ).toHaveBeenCalledWith(mockAddress); + done(); + }); + }); + + it('should return false if setting the billing address fails', (done) => { + const mockAddress: Address = {}; + + checkoutBillingAddressFacade.setBillingAddress.and.returnValue( + throwError(() => new Error('Setting address failed')) + ); + + service.setBillingAddress(mockAddress).subscribe( + (result) => { + expect(result).toBeFalsy(); + expect( + checkoutBillingAddressFacade.setBillingAddress + ).toHaveBeenCalledWith(mockAddress); + done(); + }, + (error) => { + expect(error).toBeDefined(); + done(); + } + ); + }); + }); + + describe('getCurrentCart', () => { + it('should return an observable of the current cart', (done) => { + const mockCart: Cart = { guid: 'testId' }; + + activeCartFacade.takeActive.and.returnValue(of(mockCart)); + + service.getCurrentCart().subscribe((cart) => { + expect(cart.guid).toEqual(mockCart.guid); + expect(activeCartFacade.takeActive).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('getCurrentCartId', () => { + it('should return an observable of the current cart ID', (done) => { + const mockCartId = '12345'; + activeCartFacade.takeActiveCartId.and.returnValue(of(mockCartId)); + + service.getCurrentCartId().subscribe((cartId) => { + expect(cartId).toEqual(mockCartId); + expect(activeCartFacade.takeActiveCartId).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('getCurrentCartTotalPrice', () => { + it('should return an observable of the current cart total price', (done) => { + const mockTotalPrice = 100.5; + const mockCart = { totalPrice: { value: mockTotalPrice } }; + + activeCartFacade.takeActive.and.returnValue(of(mockCart)); + + service.getCurrentCartTotalPrice().subscribe((totalPrice) => { + expect(totalPrice).toEqual(mockTotalPrice); + expect(activeCartFacade.takeActive).toHaveBeenCalled(); + done(); + }); + }); + + it('should return undefined if the cart does not have a total price', (done) => { + const mockCart = { totalPrice: undefined }; + + activeCartFacade.takeActive.and.returnValue(of(mockCart)); + + service.getCurrentCartTotalPrice().subscribe((totalPrice) => { + expect(totalPrice).toBeUndefined(); + expect(activeCartFacade.takeActive).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('setDeliveryMode', () => { + it('should set the delivery mode and return the selected mode', (done) => { + const mockMode = 'standard'; + const mockDeliveryMode: DeliveryMode = { + code: mockMode, + name: 'Standard Delivery', + }; + + checkoutDeliveryModesFacade.setDeliveryMode.and.returnValue( + of(mockDeliveryMode) + ); + + checkoutDeliveryModesFacade.getSelectedDeliveryModeState.and.returnValue( + of({ + loading: false, + error: false, + data: mockDeliveryMode, + }) + ); + + service.setDeliveryMode(mockMode).subscribe((selectedMode) => { + expect(selectedMode).toEqual(mockDeliveryMode); + expect( + checkoutDeliveryModesFacade.setDeliveryMode + ).toHaveBeenCalledWith(mockMode); + done(); + }); + }); + + it('should return undefined if setting the delivery mode fails', (done) => { + const mockMode = 'express'; + + checkoutDeliveryModesFacade.setDeliveryMode.and.returnValue( + throwError(new Error('Failed to set mode')) + ); + + service.setDeliveryMode(mockMode).subscribe( + (selectedMode) => { + expect(selectedMode).toBeUndefined(); + expect( + checkoutDeliveryModesFacade.setDeliveryMode + ).toHaveBeenCalledWith(mockMode); + done(); + }, + (error) => { + expect(error).toBeDefined(); + done(); + } + ); + }); + }); + + describe('getSelectedDeliveryMode', () => { + it('should return an observable of the selected delivery mode', (done) => { + const mockDeliveryMode: DeliveryMode = { + code: 'standard', + name: 'Standard Delivery', + }; + + checkoutDeliveryModesFacade.getSelectedDeliveryModeState.and.returnValue( + of({ + loading: false, + error: false, + data: mockDeliveryMode, + }) + ); + + service.getSelectedDeliveryMode().subscribe((selectedMode) => { + expect(selectedMode).toEqual(mockDeliveryMode); + done(); + }); + }); + + it('should return undefined if no delivery mode is selected', (done) => { + checkoutDeliveryModesFacade.getSelectedDeliveryModeState.and.returnValue( + of({ + loading: false, + error: false, + data: undefined, + }) + ); + + service.getSelectedDeliveryMode().subscribe((selectedMode) => { + expect(selectedMode).toBeUndefined(); + done(); + }); + }); + }); + + describe('deleteUserAddresses', () => { + it('should call deleteUserAddress for each address ID', () => { + const addrIds = ['addr1', 'addr2', 'addr3']; + + service.deleteUserAddresses(addrIds); + + addrIds.forEach((addrId) => { + expect(userAddressService.deleteUserAddress).toHaveBeenCalledWith( + addrId + ); + }); + }); + + it('should disable global messages for address deletion success', () => { + const addrIds = ['addr1', 'addr2']; + + service.deleteUserAddresses(addrIds); + + expect(opfGlobalMessageService.disableGlobalMessage).toHaveBeenCalledWith( + ['addressForm.userAddressDeleteSuccess'] + ); + }); + }); + + describe('getMerchantName', () => { + it('should return baseSite name', (done) => { + const mockName = 'Store'; + baseSiteService.get.and.returnValue(of({ name: mockName })); + service.getMerchantName().subscribe((merchantName) => { + expect(merchantName).toBe(mockName); + done(); + }); + }); + it('should return default MerchantName name when empty', (done) => { + const mockName = undefined; + baseSiteService.get.and.returnValue(of({ name: mockName })); + service.getMerchantName().subscribe((merchantName) => { + expect(merchantName).toBe(OPF_QUICK_BUY_DEFAULT_MERCHANT_NAME); + done(); + }); + }); + }); + + describe('getTransactionLocationContext', () => { + it('should return OpfQuickBuyLocation', () => { + const routerState = new BehaviorSubject({ + state: { semanticRoute: 'cart' }, + } as RouterState); + routingService.getRouterState.and.returnValue(routerState); + + service.getTransactionLocationContext().subscribe((context) => { + expect(context).toBe(OpfQuickBuyLocation.CART); + }); + }); + }); +}); diff --git a/integration-libs/opf/quick-buy/core/services/opf-quick-buy-transaction.service.ts b/integration-libs/opf/quick-buy/core/services/opf-quick-buy-transaction.service.ts new file mode 100644 index 00000000000..255712f38d4 --- /dev/null +++ b/integration-libs/opf/quick-buy/core/services/opf-quick-buy-transaction.service.ts @@ -0,0 +1,177 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { inject, Injectable } from '@angular/core'; +import { + ActiveCartFacade, + Cart, + DeliveryMode, +} from '@spartacus/cart/base/root'; +import { + CheckoutBillingAddressFacade, + CheckoutDeliveryAddressFacade, + CheckoutDeliveryModesFacade, +} from '@spartacus/checkout/base/root'; +import { + Address, + BaseSiteService, + QueryState, + RoutingService, + UserAddressService, +} from '@spartacus/core'; +import { OpfGlobalMessageService } from '@spartacus/opf/base/root'; +import { + OPF_QUICK_BUY_DEFAULT_MERCHANT_NAME, + OpfQuickBuyDeliveryInfo, + OpfQuickBuyDeliveryType, + OpfQuickBuyLocation, +} from '@spartacus/opf/quick-buy/root'; +import { Observable } from 'rxjs'; +import { filter, map, switchMap, take } from 'rxjs/operators'; +@Injectable({ + providedIn: 'root', +}) +export class OpfQuickBuyTransactionService { + protected baseSiteService = inject(BaseSiteService); + protected activeCartFacade = inject(ActiveCartFacade); + protected routingService = inject(RoutingService); + protected checkoutDeliveryModesFacade = inject(CheckoutDeliveryModesFacade); + protected checkoutDeliveryAddressFacade = inject( + CheckoutDeliveryAddressFacade + ); + protected checkoutBillingAddressFacade = inject(CheckoutBillingAddressFacade); + protected userAddressService = inject(UserAddressService); + protected opfGlobalMessageService = inject(OpfGlobalMessageService); + + getTransactionDeliveryType(): Observable { + return this.activeCartFacade.hasDeliveryItems().pipe( + take(1), + map((hasDeliveryItems: boolean) => + hasDeliveryItems + ? OpfQuickBuyDeliveryType.SHIPPING + : OpfQuickBuyDeliveryType.PICKUP + ) + ); + } + + getTransactionDeliveryInfo(): Observable { + const deliveryTypeObservable = this.getTransactionDeliveryType().pipe( + map((deliveryType) => { + return { + type: deliveryType, + } as OpfQuickBuyDeliveryInfo; + }) + ); + + return deliveryTypeObservable.pipe(take(1)); + } + + getTransactionLocationContext(): Observable { + return this.routingService.getRouterState().pipe( + take(1), + map( + (routerState) => + routerState?.state?.semanticRoute?.toLocaleUpperCase() as OpfQuickBuyLocation + ) + ); + } + + getMerchantName(): Observable { + return this.baseSiteService.get().pipe( + take(1), + map((baseSite) => baseSite?.name ?? OPF_QUICK_BUY_DEFAULT_MERCHANT_NAME) + ); + } + + checkStableCart(): Observable { + return this.activeCartFacade.isStable().pipe( + filter((isStable) => !!isStable), + take(1) + ); + } + + getSupportedDeliveryModes(): Observable { + return this.checkoutDeliveryModesFacade.getSupportedDeliveryModes(); + } + + setDeliveryAddress(address: Address): Observable { + this.opfGlobalMessageService.disableGlobalMessage([ + 'addressForm.userAddressAddSuccess', + ]); + return this.checkoutDeliveryAddressFacade.createAndSetAddress(address).pipe( + switchMap(() => this.checkStableCart()), + switchMap(() => + this.getDeliveryAddress().pipe( + map((addr: Address | undefined) => addr?.id ?? '') + ) + ) + ); + } + + setBillingAddress(address: Address): Observable { + return this.checkoutBillingAddressFacade + .setBillingAddress(address) + .pipe(switchMap(() => this.checkStableCart())); + } + + getDeliveryAddress(): Observable
{ + return this.checkoutDeliveryAddressFacade.getDeliveryAddressState().pipe( + filter((state: QueryState
) => !state.loading), + take(1), + map((state: QueryState
) => { + return state.data; + }) + ); + } + + getCurrentCart(): Observable { + return this.activeCartFacade.takeActive(); + } + + getCurrentCartId(): Observable { + return this.activeCartFacade.takeActiveCartId(); + } + + getCurrentCartTotalPrice(): Observable { + return this.activeCartFacade + .takeActive() + .pipe(map((cart: Cart) => cart.totalPrice?.value)); + } + + setDeliveryMode(mode: string): Observable { + return this.checkoutDeliveryModesFacade.setDeliveryMode(mode).pipe( + switchMap(() => + this.checkoutDeliveryModesFacade.getSelectedDeliveryModeState() + ), + filter( + (state: QueryState) => + !state.error && !state.loading + ), + take(1), + map((state: QueryState) => state.data) + ); + } + + getSelectedDeliveryMode(): Observable { + return this.checkoutDeliveryModesFacade.getSelectedDeliveryModeState().pipe( + filter( + (state: QueryState) => + !state.error && !state.loading + ), + take(1), + map((state: QueryState) => state.data) + ); + } + + deleteUserAddresses(addrIds: string[]): void { + this.opfGlobalMessageService.disableGlobalMessage([ + 'addressForm.userAddressDeleteSuccess', + ]); + addrIds.forEach((addrId) => { + this.userAddressService.deleteUserAddress(addrId); + }); + } +} diff --git a/integration-libs/opf/quick-buy/core/tokens/index.ts b/integration-libs/opf/quick-buy/core/tokens/index.ts new file mode 100644 index 00000000000..62a40ab0d42 --- /dev/null +++ b/integration-libs/opf/quick-buy/core/tokens/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './tokens'; diff --git a/integration-libs/opf/quick-buy/core/tokens/tokens.ts b/integration-libs/opf/quick-buy/core/tokens/tokens.ts new file mode 100644 index 00000000000..f14dcf1415f --- /dev/null +++ b/integration-libs/opf/quick-buy/core/tokens/tokens.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InjectionToken } from '@angular/core'; +import { Converter } from '@spartacus/core'; +import { ApplePaySessionVerificationResponse } from '@spartacus/opf/quick-buy/root'; + +export const OPF_APPLE_PAY_WEB_SESSION_NORMALIZER = new InjectionToken< + Converter +>('OpfApplePayWebSession'); diff --git a/integration-libs/opf/quick-buy/ng-package.json b/integration-libs/opf/quick-buy/ng-package.json new file mode 100644 index 00000000000..38e01ac17de --- /dev/null +++ b/integration-libs/opf/quick-buy/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/integration-libs/opf/quick-buy/opf-api/adapters/index.ts b/integration-libs/opf/quick-buy/opf-api/adapters/index.ts new file mode 100644 index 00000000000..b8040b7c01c --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-api/adapters/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './opf-api-quick-buy.adapter'; diff --git a/integration-libs/opf/quick-buy/opf-api/adapters/opf-api-quick-buy.adapter.spec.ts b/integration-libs/opf/quick-buy/opf-api/adapters/opf-api-quick-buy.adapter.spec.ts new file mode 100644 index 00000000000..75f3d3024ec --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-api/adapters/opf-api-quick-buy.adapter.spec.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// TODO: Add unit tests... diff --git a/integration-libs/opf/quick-buy/opf-api/adapters/opf-api-quick-buy.adapter.ts b/integration-libs/opf/quick-buy/opf-api/adapters/opf-api-quick-buy.adapter.ts new file mode 100644 index 00000000000..d4c326813f0 --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-api/adapters/opf-api-quick-buy.adapter.ts @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { + backOff, + ConverterService, + isServerError, + LoggerService, + tryNormalizeHttpError, +} from '@spartacus/core'; +import { OpfEndpointsService } from '@spartacus/opf/base/core'; +import { + OPF_CC_ACCESS_CODE_HEADER, + OPF_CC_PUBLIC_KEY_HEADER, + OpfConfig, +} from '@spartacus/opf/base/root'; +import { + OPF_APPLE_PAY_WEB_SESSION_NORMALIZER, + OpfQuickBuyAdapter, +} from '@spartacus/opf/quick-buy/core'; +import { + ApplePaySessionVerificationRequest, + ApplePaySessionVerificationResponse, +} from '@spartacus/opf/quick-buy/root'; +import { Observable } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +@Injectable() +export class OpfApiQuickBuyAdapter implements OpfQuickBuyAdapter { + protected logger = inject(LoggerService); + + constructor( + protected http: HttpClient, + protected converter: ConverterService, + protected opfEndpointsService: OpfEndpointsService, + protected config: OpfConfig + ) {} + + protected headerWithNoLanguage: { [name: string]: string } = { + accept: 'application/json', + 'Content-Type': 'application/json', + }; + + protected headerWithContentLanguage: { [name: string]: string } = { + ...this.headerWithNoLanguage, + 'Content-Language': 'en-us', + }; + + getApplePayWebSession( + applePayWebSessionRequest: ApplePaySessionVerificationRequest, + accessCode: string + ): Observable { + const headers = new HttpHeaders(this.headerWithContentLanguage) + .set( + OPF_CC_PUBLIC_KEY_HEADER, + this.config.opf?.commerceCloudPublicKey || '' + ) + .set(OPF_CC_ACCESS_CODE_HEADER, accessCode || ''); + + const url = this.getApplePayWebSessionEndpoint(); + + return this.http + .post( + url, + applePayWebSessionRequest, + { headers } + ) + .pipe( + catchError((error) => { + throw tryNormalizeHttpError(error, this.logger); + }), + backOff({ + shouldRetry: isServerError, + maxTries: 2, + }), + this.converter.pipeable(OPF_APPLE_PAY_WEB_SESSION_NORMALIZER) + ); + } + + protected getApplePayWebSessionEndpoint(): string { + return this.opfEndpointsService.buildUrl('getApplePayWebSession'); + } +} diff --git a/integration-libs/opf/quick-buy/opf-api/config/default-opf-api-quick-buy-config.ts b/integration-libs/opf/quick-buy/opf-api/config/default-opf-api-quick-buy-config.ts new file mode 100644 index 00000000000..0a4fabb33aa --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-api/config/default-opf-api-quick-buy-config.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OpfApiConfig } from '@spartacus/opf/base/root'; + +export const defaultOpfApiQuickBuyConfig: OpfApiConfig = { + backend: { + opfApi: { + endpoints: { + getApplePayWebSession: 'payments/apple-pay-web-sessions', + }, + }, + }, +}; diff --git a/integration-libs/opf/quick-buy/opf-api/model/index.ts b/integration-libs/opf/quick-buy/opf-api/model/index.ts new file mode 100644 index 00000000000..30a8f2f0853 --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-api/model/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './opf-api-quick-buy-endpoints.model'; diff --git a/integration-libs/opf/quick-buy/opf-api/model/opf-api-quick-buy-endpoints.model.ts b/integration-libs/opf/quick-buy/opf-api/model/opf-api-quick-buy-endpoints.model.ts new file mode 100644 index 00000000000..72106c06243 --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-api/model/opf-api-quick-buy-endpoints.model.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OpfApiEndpoint } from '@spartacus/opf/base/root'; + +declare module '@spartacus/opf/base/root' { + interface OpfApiEndpoints { + /** + * Endpoint to get ApplePay Web Session for QuickBuy functionality + */ + getApplePayWebSession?: string | OpfApiEndpoint; + } +} diff --git a/integration-libs/opf/quick-buy/opf-api/ng-package.json b/integration-libs/opf/quick-buy/opf-api/ng-package.json new file mode 100644 index 00000000000..2a9c6221b0e --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-api/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/integration-libs/opf/quick-buy/opf-api/opf-api-quick-buy.module.ts b/integration-libs/opf/quick-buy/opf-api/opf-api-quick-buy.module.ts new file mode 100644 index 00000000000..66dcfc623a9 --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-api/opf-api-quick-buy.module.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { provideDefaultConfig } from '@spartacus/core'; +import { OpfQuickBuyAdapter } from '@spartacus/opf/quick-buy/core'; +import { OpfApiQuickBuyAdapter } from './adapters/opf-api-quick-buy.adapter'; +import { defaultOpfApiQuickBuyConfig } from './config/default-opf-api-quick-buy-config'; + +@NgModule({ + imports: [CommonModule], + providers: [ + provideDefaultConfig(defaultOpfApiQuickBuyConfig), + { + provide: OpfQuickBuyAdapter, + useClass: OpfApiQuickBuyAdapter, + }, + ], +}) +export class OpfApiQuickBuyModule {} diff --git a/integration-libs/opf/quick-buy/opf-api/public_api.ts b/integration-libs/opf/quick-buy/opf-api/public_api.ts new file mode 100644 index 00000000000..46b610b8780 --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-api/public_api.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './model/index'; +export * from './opf-api-quick-buy.module'; diff --git a/integration-libs/opf/quick-buy/opf-quick-buy.module.ts b/integration-libs/opf/quick-buy/opf-quick-buy.module.ts new file mode 100644 index 00000000000..ad609e7683e --- /dev/null +++ b/integration-libs/opf/quick-buy/opf-quick-buy.module.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { OpfQuickBuyComponentsModule } from '@spartacus/opf/quick-buy/components'; +import { OpfQuickBuyCoreModule } from '@spartacus/opf/quick-buy/core'; +import { OpfApiQuickBuyModule } from '@spartacus/opf/quick-buy/opf-api'; + +@NgModule({ + imports: [ + OpfQuickBuyComponentsModule, + OpfQuickBuyCoreModule, + OpfApiQuickBuyModule, + ], +}) +export class OpfQuickBuyModule {} diff --git a/integration-libs/opf/quick-buy/public_api.ts b/integration-libs/opf/quick-buy/public_api.ts new file mode 100644 index 00000000000..71d3b23b87d --- /dev/null +++ b/integration-libs/opf/quick-buy/public_api.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './opf-quick-buy.module'; diff --git a/integration-libs/opf/quick-buy/root/facade/index.ts b/integration-libs/opf/quick-buy/root/facade/index.ts new file mode 100644 index 00000000000..75e7004272b --- /dev/null +++ b/integration-libs/opf/quick-buy/root/facade/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './opf-quick-buy.facade'; diff --git a/integration-libs/opf/quick-buy/root/facade/opf-quick-buy.facade.ts b/integration-libs/opf/quick-buy/root/facade/opf-quick-buy.facade.ts new file mode 100644 index 00000000000..61478f2cab9 --- /dev/null +++ b/integration-libs/opf/quick-buy/root/facade/opf-quick-buy.facade.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { facadeFactory } from '@spartacus/core'; +import { Observable } from 'rxjs'; +import { OPF_QUICK_BUY_FEATURE } from '../feature-name'; +import { + ApplePaySessionVerificationRequest, + ApplePaySessionVerificationResponse, +} from '../model'; + +@Injectable({ + providedIn: 'root', + useFactory: () => + facadeFactory({ + facade: OpfQuickBuyFacade, + feature: OPF_QUICK_BUY_FEATURE, + methods: ['getApplePayWebSession'], + }), +}) +export abstract class OpfQuickBuyFacade { + /** + * Abstract method to get ApplePay session for QuickBuy. + * + * @param applePayWebSessionRequest + */ + abstract getApplePayWebSession( + applePayWebSessionRequest: ApplePaySessionVerificationRequest + ): Observable; +} diff --git a/integration-libs/opf/quick-buy/root/feature-name.ts b/integration-libs/opf/quick-buy/root/feature-name.ts new file mode 100644 index 00000000000..0afcf7b7e5a --- /dev/null +++ b/integration-libs/opf/quick-buy/root/feature-name.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const OPF_QUICK_BUY_FEATURE = 'opfQuickBuy'; diff --git a/integration-libs/opf/quick-buy/root/model/augmented-opf-quick-buy.model.ts b/integration-libs/opf/quick-buy/root/model/augmented-opf-quick-buy.model.ts new file mode 100644 index 00000000000..97d3c0fd426 --- /dev/null +++ b/integration-libs/opf/quick-buy/root/model/augmented-opf-quick-buy.model.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import '@spartacus/opf/base/root'; +import '@spartacus/opf/payment/root'; +import { OpfQuickBuyDigitalWallet } from './opf-quick-buy.model'; + +declare module '@spartacus/opf/base/root' { + interface ActiveConfiguration { + digitalWalletQuickBuy?: OpfQuickBuyDigitalWallet[]; + } +} diff --git a/integration-libs/opf/quick-buy/root/model/constants.ts b/integration-libs/opf/quick-buy/root/model/constants.ts new file mode 100644 index 00000000000..da073ccd573 --- /dev/null +++ b/integration-libs/opf/quick-buy/root/model/constants.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const OPF_QUICK_BUY_DEFAULT_MERCHANT_NAME: string = 'Store'; +export const OPF_QUICK_BUY_ADDRESS_FIELD_PLACEHOLDER = '[FIELD_NOT_SET]'; diff --git a/integration-libs/opf/quick-buy/root/model/index.ts b/integration-libs/opf/quick-buy/root/model/index.ts new file mode 100644 index 00000000000..8ca6e243e9b --- /dev/null +++ b/integration-libs/opf/quick-buy/root/model/index.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import './augmented-opf-quick-buy.model'; +export * from './constants'; +export * from './opf-apple-pay.model'; +export * from './opf-quick-buy.model'; diff --git a/integration-libs/opf/quick-buy/root/model/opf-apple-pay.model.ts b/integration-libs/opf/quick-buy/root/model/opf-apple-pay.model.ts new file mode 100644 index 00000000000..efcde27e7c1 --- /dev/null +++ b/integration-libs/opf/quick-buy/root/model/opf-apple-pay.model.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Cart } from '@spartacus/cart/base/root'; +import { Product } from '@spartacus/core'; +import { Observable } from 'rxjs'; + +export interface ApplePaySessionVerificationRequest { + cartId: string; + validationUrl: string; + initiative: 'web'; + initiativeContext: string; +} + +export interface ApplePaySessionVerificationResponse { + epochTimestamp: number; + expiresAt: number; + merchantSessionIdentifier: string; + nonce: string; + merchantIdentifier: string; + domainName: string; + displayName: string; + signature: string; +} + +export interface ApplePayAuthorizationResult { + authResult: any; + payment: any; +} + +export interface ApplePayTransactionInput { + product?: Product; + cart?: Cart; + quantity?: number; + countryCode?: string; +} + +export interface ApplePayObservableConfig { + request: any; + validateMerchant: (event: any) => Observable; + shippingContactSelected: (event: any) => Observable; + paymentMethodSelected: (event: any) => Observable; + shippingMethodSelected: (event: any) => Observable; + paymentAuthorized: (event: any) => Observable; +} + +export enum ApplePayEvent { + VALIDATE_MERCHANT = 'validatemerchant', + CANCEL = 'cancel', + PAYMENT_METHOD_SELECTED = 'paymentmethodselected', + SHIPPING_CONTACT_SELECTED = 'shippingcontactselected', + SHIPPING_METHOD_SELECTED = 'shippingmethodselected', + PAYMENT_AUTHORIZED = 'paymentauthorized', +} + +export enum ApplePayShippingType { + SHIPPING = 'shipping', + DELIVERY = 'delivery', + STORE_PICKUP = 'storePickup', + SERVICE_PICKUP = 'servicePickup', +} diff --git a/integration-libs/opf/quick-buy/root/model/opf-quick-buy.model.ts b/integration-libs/opf/quick-buy/root/model/opf-quick-buy.model.ts new file mode 100644 index 00000000000..9e25b4dbed9 --- /dev/null +++ b/integration-libs/opf/quick-buy/root/model/opf-quick-buy.model.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Cart } from '@spartacus/cart/base/root'; +import { PointOfService, Product } from '@spartacus/core'; + +export interface OpfQuickBuyDigitalWallet { + description?: string; + provider?: OpfProviderType; + enabled?: boolean; + merchantId?: string; + merchantName?: string; + countryCode?: string; + googlePayGateway?: string; +} + +export interface OpfQuickBuyDeliveryInfo { + type: OpfQuickBuyDeliveryType; + pickupDetails?: PointOfService; +} + +export interface QuickBuyTransactionDetails { + context?: OpfQuickBuyLocation; + cart?: Cart; + product?: Product; + quantity?: number; + deliveryInfo?: OpfQuickBuyDeliveryInfo; + addressIds: string[]; + total: { + amount: string; + label: string; + currency: string; + }; +} + +export enum OpfQuickBuyLocation { + CART = 'CART', + PRODUCT = 'PRODUCT', +} + +export enum OpfQuickBuyDeliveryType { + SHIPPING = 'SHIPPING', + PICKUP = 'PICKUP', +} + +export enum OpfProviderType { + APPLE_PAY = 'APPLE_PAY', + GOOGLE_PAY = 'GOOGLE_PAY', +} diff --git a/integration-libs/opf/quick-buy/root/ng-package.json b/integration-libs/opf/quick-buy/root/ng-package.json new file mode 100644 index 00000000000..2a9c6221b0e --- /dev/null +++ b/integration-libs/opf/quick-buy/root/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/integration-libs/opf/quick-buy/root/opf-quick-buy-root.module.ts b/integration-libs/opf/quick-buy/root/opf-quick-buy-root.module.ts new file mode 100644 index 00000000000..799e1a17ae2 --- /dev/null +++ b/integration-libs/opf/quick-buy/root/opf-quick-buy-root.module.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CmsConfig, provideDefaultConfigFactory } from '@spartacus/core'; +import { OPF_QUICK_BUY_FEATURE } from './feature-name'; + +export function defaultOpfQuickBuyCmsComponentsConfig(): CmsConfig { + const config: CmsConfig = { + featureModules: { + [OPF_QUICK_BUY_FEATURE]: { + cmsComponents: ['OpfQuickBuyButtonsComponent'], + }, + }, + }; + return config; +} + +@NgModule({ + providers: [ + provideDefaultConfigFactory(defaultOpfQuickBuyCmsComponentsConfig), + ], +}) +export class OpfQuickBuyRootModule {} diff --git a/integration-libs/opf/quick-buy/root/public_api.ts b/integration-libs/opf/quick-buy/root/public_api.ts new file mode 100644 index 00000000000..794e9f4a3c5 --- /dev/null +++ b/integration-libs/opf/quick-buy/root/public_api.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './facade/index'; +export * from './feature-name'; +export * from './model/index'; +export * from './opf-quick-buy-root.module'; diff --git a/integration-libs/opf/quick-buy/styles/_index.scss b/integration-libs/opf/quick-buy/styles/_index.scss new file mode 100644 index 00000000000..192091fb04e --- /dev/null +++ b/integration-libs/opf/quick-buy/styles/_index.scss @@ -0,0 +1 @@ +@import './components/index'; diff --git a/integration-libs/opf/quick-buy/styles/components/_index.scss b/integration-libs/opf/quick-buy/styles/components/_index.scss new file mode 100644 index 00000000000..a164d47a3a4 --- /dev/null +++ b/integration-libs/opf/quick-buy/styles/components/_index.scss @@ -0,0 +1,3 @@ +@import './opf-google-pay'; +@import './opf-apple-pay'; +@import './opf-quick-buy-buttons'; diff --git a/integration-libs/opf/quick-buy/styles/components/_opf-apple-pay.scss b/integration-libs/opf/quick-buy/styles/components/_opf-apple-pay.scss new file mode 100644 index 00000000000..be4e0763c88 --- /dev/null +++ b/integration-libs/opf/quick-buy/styles/components/_opf-apple-pay.scss @@ -0,0 +1,11 @@ +%cx-opf-apple-pay { + .apple-pay-button { + -webkit-appearance: -apple-pay-button; + -apple-pay-button-type: buy; + -apple-pay-button-style: black; + border-radius: var(--cx-buttons-border-radius); + margin-top: 10px; + height: 48px; + cursor: pointer; + } +} diff --git a/integration-libs/opf/quick-buy/styles/components/_opf-google-pay.scss b/integration-libs/opf/quick-buy/styles/components/_opf-google-pay.scss new file mode 100644 index 00000000000..9faa0fcb666 --- /dev/null +++ b/integration-libs/opf/quick-buy/styles/components/_opf-google-pay.scss @@ -0,0 +1,14 @@ +%cx-opf-google-pay { + .cx-opf-google-pay-button { + margin: 10px 0; + height: 48px; + @include media-breakpoint-down(md) { + padding: 0; + } + .gpay-button, + .gpay-card-info-container { + border-radius: var(--cx-buttons-border-radius); + min-width: auto; + } + } +} diff --git a/integration-libs/opf/quick-buy/styles/components/_opf-quick-buy-buttons.scss b/integration-libs/opf/quick-buy/styles/components/_opf-quick-buy-buttons.scss new file mode 100644 index 00000000000..7efd0a4f9d7 --- /dev/null +++ b/integration-libs/opf/quick-buy/styles/components/_opf-quick-buy-buttons.scss @@ -0,0 +1,23 @@ +%cx-opf-quick-buy-buttons { + padding-inline-end: 0; + padding-inline-start: 3rem; + padding-top: 0; + + @include media-breakpoint-up(lg) { + flex: none; + } + + @include media-breakpoint-down(md) { + align-self: flex-end; + padding-inline-start: 0; + order: 5; + } + + @include media-breakpoint-down(sm) { + margin-bottom: -2rem; + padding: 2rem 0 0; + max-width: 100%; + padding-inline-end: 0; + padding-inline-start: 0; + } +} diff --git a/integration-libs/opf/tsconfig.schematics.json b/integration-libs/opf/tsconfig.schematics.json index b99e67bab37..710df6b10a2 100644 --- a/integration-libs/opf/tsconfig.schematics.json +++ b/integration-libs/opf/tsconfig.schematics.json @@ -691,6 +691,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/opps/tsconfig.schematics.json b/integration-libs/opps/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/integration-libs/opps/tsconfig.schematics.json +++ b/integration-libs/opps/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/s4-service/tsconfig.schematics.json b/integration-libs/s4-service/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/integration-libs/s4-service/tsconfig.schematics.json +++ b/integration-libs/s4-service/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/s4om/tsconfig.schematics.json b/integration-libs/s4om/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/integration-libs/s4om/tsconfig.schematics.json +++ b/integration-libs/s4om/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/integration-libs/segment-refs/tsconfig.schematics.json b/integration-libs/segment-refs/tsconfig.schematics.json index 60ba3703078..cd458bc746b 100644 --- a/integration-libs/segment-refs/tsconfig.schematics.json +++ b/integration-libs/segment-refs/tsconfig.schematics.json @@ -675,6 +675,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/package-lock.json b/package-lock.json index 2498698485d..289af9d3170 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,9 @@ "@ngrx/effects": "^17.0.1", "@ngrx/router-store": "^17.0.1", "@ngrx/store": "^17.0.1", + "@types/applepayjs": "^14.0.3", "@types/google.maps": "^3.54.0", + "@types/googlepay": "^0.7.4", "angular-oauth2-oidc": "^17.0.1", "bootstrap": "^4.6.2", "comment-json": "^4.2.3", @@ -8219,6 +8221,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@types/applepayjs": { + "version": "14.0.8", + "resolved": "https://registry.npmjs.org/@types/applepayjs/-/applepayjs-14.0.8.tgz", + "integrity": "sha512-Yzf5OSitdS+/G8cjaAkPJ0+pBOEf9Vik1XUCdw6ul7Qh6Xb18wTlG/sWA5jKIme3x4fbyTGlSd4mfkvdtP9mRw==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -8388,6 +8395,11 @@ "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.54.10.tgz", "integrity": "sha512-N6gwM01mKhooXaw+IKbUH7wJcIJCn8U60VoaVvom5EiQjmfgevhQ+0+/r17beXW5j8ad2x+WPr0iyOUodCw4/w==" }, + "node_modules/@types/googlepay": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.6.tgz", + "integrity": "sha512-5003wG+qvf4Ktf1hC9IJuRakNzQov00+Xf09pAWGJLpdOjUrq0SSLCpXX7pwSeTG9r5hrdzq1iFyZcW7WVyr4g==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -29693,6 +29705,11 @@ } } }, + "@types/applepayjs": { + "version": "14.0.8", + "resolved": "https://registry.npmjs.org/@types/applepayjs/-/applepayjs-14.0.8.tgz", + "integrity": "sha512-Yzf5OSitdS+/G8cjaAkPJ0+pBOEf9Vik1XUCdw6ul7Qh6Xb18wTlG/sWA5jKIme3x4fbyTGlSd4mfkvdtP9mRw==" + }, "@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -29862,6 +29879,11 @@ "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.54.10.tgz", "integrity": "sha512-N6gwM01mKhooXaw+IKbUH7wJcIJCn8U60VoaVvom5EiQjmfgevhQ+0+/r17beXW5j8ad2x+WPr0iyOUodCw4/w==" }, + "@types/googlepay": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.6.tgz", + "integrity": "sha512-5003wG+qvf4Ktf1hC9IJuRakNzQov00+Xf09pAWGJLpdOjUrq0SSLCpXX7pwSeTG9r5hrdzq1iFyZcW7WVyr4g==" + }, "@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", diff --git a/package.json b/package.json index 83d0a178b0e..c77f54f538f 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,9 @@ "@ngrx/effects": "^17.0.1", "@ngrx/router-store": "^17.0.1", "@ngrx/store": "^17.0.1", + "@types/applepayjs": "^14.0.3", "@types/google.maps": "^3.54.0", + "@types/googlepay": "^0.7.4", "angular-oauth2-oidc": "^17.0.1", "bootstrap": "^4.6.2", "comment-json": "^4.2.3", @@ -244,4 +246,4 @@ "webpack": "~5.94.0", "webpack-cli": "^5.0.0" } -} \ No newline at end of file +} diff --git a/projects/schematics/src/dependencies.json b/projects/schematics/src/dependencies.json index a0be0491f38..63729258a33 100644 --- a/projects/schematics/src/dependencies.json +++ b/projects/schematics/src/dependencies.json @@ -424,6 +424,7 @@ "@angular/platform-browser": "^17.0.5", "@angular/router": "^17.0.5", "@ng-select/ng-select": "^12.0.4", + "@ngrx/store": "^17.0.1", "@spartacus/cart": "2211.29.0-2", "@spartacus/checkout": "2211.29.0-2", "@spartacus/core": "2211.29.0-2", @@ -499,7 +500,9 @@ "@ngrx/effects": "^17.0.1", "@ngrx/router-store": "^17.0.1", "@ngrx/store": "^17.0.1", + "@types/applepayjs": "^14.0.3", "@types/google.maps": "^3.54.0", + "@types/googlepay": "^0.7.4", "angular-oauth2-oidc": "^17.0.1", "bootstrap": "^4.6.2", "comment-json": "^4.2.3", diff --git a/projects/storefrontapp/src/app/spartacus/features/opf/opf-feature.module.ts b/projects/storefrontapp/src/app/spartacus/features/opf/opf-feature.module.ts index a390d62dceb..dee58f54885 100644 --- a/projects/storefrontapp/src/app/spartacus/features/opf/opf-feature.module.ts +++ b/projects/storefrontapp/src/app/spartacus/features/opf/opf-feature.module.ts @@ -35,6 +35,10 @@ import { OPF_PAYMENT_FEATURE, OpfPaymentRootModule, } from '@spartacus/opf/payment/root'; +import { + OPF_QUICK_BUY_FEATURE, + OpfQuickBuyRootModule, +} from '@spartacus/opf/quick-buy/root'; import { environment } from '../../../../environments/environment'; const extensionProviders: Provider[] = []; @@ -51,6 +55,7 @@ if (environment.b2b) { OpfCheckoutRootModule, OpfCtaRootModule, OpfGlobalFunctionsRootModule, + OpfQuickBuyRootModule, ], providers: [ provideConfig({ @@ -77,6 +82,10 @@ if (environment.b2b) { (m) => m.OpfGlobalFunctionsModule ), }, + [OPF_QUICK_BUY_FEATURE]: { + module: () => + import('@spartacus/opf/quick-buy').then((m) => m.OpfQuickBuyModule), + }, }, }), diff --git a/projects/storefrontapp/tsconfig.app.prod.json b/projects/storefrontapp/tsconfig.app.prod.json index aff874b7217..df2a46f10b3 100644 --- a/projects/storefrontapp/tsconfig.app.prod.json +++ b/projects/storefrontapp/tsconfig.app.prod.json @@ -430,6 +430,11 @@ "@spartacus/opf/payment": ["dist/opf/payment"], "@spartacus/opf/payment/opf-api": ["dist/opf/payment/opf-api"], "@spartacus/opf/payment/root": ["dist/opf/payment/root"], + "@spartacus/opf/quick-buy/components": ["dist/opf/quick-buy/components"], + "@spartacus/opf/quick-buy/core": ["dist/opf/quick-buy/core"], + "@spartacus/opf/quick-buy": ["dist/opf/quick-buy"], + "@spartacus/opf/quick-buy/opf-api": ["dist/opf/quick-buy/opf-api"], + "@spartacus/opf/quick-buy/root": ["dist/opf/quick-buy/root"], "@spartacus/opps": ["dist/opps"], "@spartacus/opps/root": ["dist/opps/root"], "@spartacus/s4-service/assets": ["dist/s4-service/assets"], diff --git a/projects/storefrontapp/tsconfig.server.json b/projects/storefrontapp/tsconfig.server.json index 9eaada3da41..8406663b49d 100644 --- a/projects/storefrontapp/tsconfig.server.json +++ b/projects/storefrontapp/tsconfig.server.json @@ -678,6 +678,21 @@ "@spartacus/opf/payment/root": [ "../../integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "../../integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "../../integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "../../integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "../../integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "../../integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": ["../../integration-libs/opps/public_api"], "@spartacus/opps/root": ["../../integration-libs/opps/root/public_api"], "@spartacus/s4-service/assets": [ diff --git a/projects/storefrontapp/tsconfig.server.prod.json b/projects/storefrontapp/tsconfig.server.prod.json index 097e1702e8c..95f512dee0a 100644 --- a/projects/storefrontapp/tsconfig.server.prod.json +++ b/projects/storefrontapp/tsconfig.server.prod.json @@ -489,6 +489,13 @@ "@spartacus/opf/payment": ["../../dist/opf/payment"], "@spartacus/opf/payment/opf-api": ["../../dist/opf/payment/opf-api"], "@spartacus/opf/payment/root": ["../../dist/opf/payment/root"], + "@spartacus/opf/quick-buy/components": [ + "../../dist/opf/quick-buy/components" + ], + "@spartacus/opf/quick-buy/core": ["../../dist/opf/quick-buy/core"], + "@spartacus/opf/quick-buy": ["../../dist/opf/quick-buy"], + "@spartacus/opf/quick-buy/opf-api": ["../../dist/opf/quick-buy/opf-api"], + "@spartacus/opf/quick-buy/root": ["../../dist/opf/quick-buy/root"], "@spartacus/opps": ["../../dist/opps"], "@spartacus/opps/root": ["../../dist/opps/root"], "@spartacus/s4-service/assets": ["../../dist/s4-service/assets"], diff --git a/tsconfig.compodoc.json b/tsconfig.compodoc.json index d95c7ccd596..e25047c7be2 100644 --- a/tsconfig.compodoc.json +++ b/tsconfig.compodoc.json @@ -785,6 +785,21 @@ "@spartacus/opf/payment/root": [ "integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": [ "integration-libs/opps/public_api" ], @@ -839,4 +854,4 @@ "./projects/storefrontstyles/**/*.*", "./projects/vendor/**/*.*" ] -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index fdaeaef0b30..c298410e2ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -789,6 +789,21 @@ "@spartacus/opf/payment/root": [ "integration-libs/opf/payment/root/public_api" ], + "@spartacus/opf/quick-buy/components": [ + "integration-libs/opf/quick-buy/components/public_api" + ], + "@spartacus/opf/quick-buy/core": [ + "integration-libs/opf/quick-buy/core/public_api" + ], + "@spartacus/opf/quick-buy": [ + "integration-libs/opf/quick-buy/public_api" + ], + "@spartacus/opf/quick-buy/opf-api": [ + "integration-libs/opf/quick-buy/opf-api/public_api" + ], + "@spartacus/opf/quick-buy/root": [ + "integration-libs/opf/quick-buy/root/public_api" + ], "@spartacus/opps": [ "integration-libs/opps/public_api" ], @@ -850,4 +865,4 @@ "strictTemplates": true, "strictInputAccessModifiers": true } -} +} \ No newline at end of file