Skip to content

Commit

Permalink
Merge pull request #27 from Adyen/feature/AD-334
Browse files Browse the repository at this point in the history
Feature/ad 334
  • Loading branch information
kpieloch authored Nov 25, 2024
2 parents 068e75f + d93c624 commit b219a27
Show file tree
Hide file tree
Showing 17 changed files with 407 additions and 47 deletions.
1 change: 1 addition & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
},
"configurations": {
"production": {
"sourceMap": true,
"budgets": [
{
"type": "initial",
Expand Down
2 changes: 1 addition & 1 deletion projects/adyen/adyen-spartacus/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adyen/adyen-spartacus",
"version": "1.0.2",
"version": "1.0.5",
"schematics": "./schematics/collection.json",
"peerDependencies": {
"@angular/common": "^17.3.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {Component, OnInit} from '@angular/core';
import {RoutingService} from '@spartacus/core';
import {AdyenOrderService} from "../service/adyen-order.service";
import {AdyenExpressOrderService} from "../service/adyen-express-order.service";

@Component({
selector: 'adyen-redirect-success',
templateUrl: './adyen-redirect.component.html',
})
export class AdyenRedirectSuccessComponent implements OnInit {

constructor(protected adyenOrderService: AdyenOrderService,
constructor(protected adyenOrderService: AdyenExpressOrderService,
protected routingService: RoutingService) {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
<cx-google-express-payment></cx-google-express-payment>
<cx-apple-express-payment></cx-apple-express-payment>
<p></p>
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ import {CommonModule} from '@angular/common';
import {ConfigModule} from '@spartacus/core';
import {ExpressCheckoutCartComponent} from "./express-checkout-cart.component";
import {GoogleExpressPaymentComponent} from "../../../express/google-express-payment/google-express-payment.component";
import {AppleExpressPaymentComponent} from "../../../express/apple-express-payment/apple-express-payment.component";


@NgModule({
imports: [
CommonModule,
GoogleExpressPaymentComponent,
ConfigModule.withConfig({
cmsComponents: {
AdyenSpaExpressCheckoutCartPageComponent: {
component: ExpressCheckoutCartComponent
}
}
})
],
imports: [
CommonModule,
GoogleExpressPaymentComponent,
ConfigModule.withConfig({
cmsComponents: {
AdyenSpaExpressCheckoutCartPageComponent: {
component: ExpressCheckoutCartComponent
}
}
}),
AppleExpressPaymentComponent
],
declarations: [ExpressCheckoutCartComponent],
exports: [ExpressCheckoutCartComponent]
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import {GooglePayExpressCartRequest, PlaceOrderRequest, PlaceOrderResponse} from "../models/occ.order.models";
import {
ApplePayExpressRequest,
GooglePayExpressCartRequest,
PlaceOrderRequest,
PlaceOrderResponse
} from "../models/occ.order.models";
import {OccAdyenOrderAdapter} from "../occ/adapters/occ-adyen-order.adapter";

@Injectable()
Expand All @@ -18,4 +23,8 @@ export class AdyenOrderConnector {
placeGoogleExpressOrderCart(userId: string, cartId: string, request: GooglePayExpressCartRequest): Observable<PlaceOrderResponse> {
return this.adapter.placeGoogleExpressOrderCart(userId, cartId, request);
}

placeAppleExpressOrder(userId: string, cartId: string, request: ApplePayExpressRequest): Observable<PlaceOrderResponse> {
return this.adapter.placeAppleExpressOrder(userId, cartId, request);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { PaymentAction } from "@adyen/adyen-web";
import { Order } from '@spartacus/order/root';

export interface ApplePayExpressRequest {
applePayDetails: any;
addressData: any;
productCode?: string;
}

export interface GooglePayExpressCartRequest {
googlePayDetails: any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {OccEndpointsService} from '@spartacus/core';
import {Observable} from 'rxjs';
import {GooglePayExpressCartRequest, PlaceOrderRequest, PlaceOrderResponse} from "../../models/occ.order.models";
import {
ApplePayExpressRequest,
GooglePayExpressCartRequest,
PlaceOrderRequest,
PlaceOrderResponse
} from "../../models/occ.order.models";

@Injectable()
export class OccAdyenOrderAdapter {
Expand Down Expand Up @@ -62,4 +67,26 @@ export class OccAdyenOrderAdapter {
});
}

public placeAppleExpressOrder(userId: string, cartId: string, orderData: ApplePayExpressRequest): Observable<PlaceOrderResponse> {
return this.http.post<PlaceOrderResponse>(orderData.productCode ? this.getPlaceAppleExpressOrderEndpointProduct(userId, cartId) : this.getPlaceAppleExpressOrderEndpointCart(userId, cartId), orderData);
}

protected getPlaceAppleExpressOrderEndpointCart(userId: string, cartId: string): string {
return this.occEndpoints.buildUrl('users/${userId}/carts/${cartId}/adyen/express-checkout/apple/cart', {
urlParams: {
userId,
cartId,
}
});
}

protected getPlaceAppleExpressOrderEndpointProduct(userId: string, cartId: string): string {
return this.occEndpoints.buildUrl('users/${userId}/carts/${cartId}/adyen/express-checkout/apple/PDP', {
urlParams: {
userId,
cartId,
}
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<div id="apple-pay-button"></div>

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { AppleExpressPaymentComponent } from './apple-express-payment.component';
import { CheckoutAdyenConfigurationService } from '../../service/checkout-adyen-configuration.service';
import { AdyenExpressOrderService } from '../../service/adyen-express-order.service';
import { RoutingService, QueryState } from '@spartacus/core';
import { AdyenConfigData } from '../../core/models/occ.config.models';

describe('AppleExpressPaymentComponent', () => {
let component: AppleExpressPaymentComponent;
let fixture: ComponentFixture<AppleExpressPaymentComponent>;
let mockCheckoutAdyenConfigurationService: jasmine.SpyObj<CheckoutAdyenConfigurationService>;
let mockAdyenExpressOrderService: jasmine.SpyObj<AdyenExpressOrderService>;
let mockRoutingService: jasmine.SpyObj<RoutingService>;

const mockAdyenConfigData: AdyenConfigData = {
amountDecimal: 12.99,
paymentMethods: [],
connectedTerminalList: [],
storedPaymentMethodList: [],
issuerLists: new Map<string, string>(),
creditCardLabel: 'Credit Card',
allowedCards: [],
amount: { value: 1000, currency: 'USD' },
adyenClientKey: 'mockClientKey',
adyenPaypalMerchantId: 'mockPaypalMerchantId',
deviceFingerPrintUrl: 'mockDeviceFingerPrintUrl',
sessionData: { id: 'mockSessionId', sessionData: 'mockSessionData' },
selectedPaymentMethod: 'mockPaymentMethod',
showRememberTheseDetails: true,
checkoutShopperHost: 'mockShopperHost',
environmentMode: 'test',
shopperLocale: 'en-US',
openInvoiceMethods: [],
showSocialSecurityNumber: false,
showBoleto: false,
showComboCard: false,
showPos: false,
immediateCapture: false,
countryCode: 'US',
cardHolderNameRequired: true,
sepaDirectDebit: false
};

beforeEach(async () => {
mockCheckoutAdyenConfigurationService = jasmine.createSpyObj('CheckoutAdyenConfigurationService', ['getCheckoutConfigurationState']);
mockAdyenExpressOrderService = jasmine.createSpyObj('AdyenExpressOrderService', ['adyenPlaceOrder']);
mockRoutingService = jasmine.createSpyObj('RoutingService', ['go']);

mockCheckoutAdyenConfigurationService = jasmine.createSpyObj('CheckoutAdyenConfigurationService', ['getCheckoutConfigurationState']);
mockCheckoutAdyenConfigurationService.getCheckoutConfigurationState.and.returnValue(
of({ loading: false, error: false, data: mockAdyenConfigData } as QueryState<AdyenConfigData>)
);

await TestBed.configureTestingModule({
imports: [AppleExpressPaymentComponent],
providers: [
{ provide: CheckoutAdyenConfigurationService, useValue: mockCheckoutAdyenConfigurationService },
{ provide: AdyenExpressOrderService, useValue: mockAdyenExpressOrderService },
{ provide: RoutingService, useValue: mockRoutingService }
]
}).compileComponents();

fixture = TestBed.createComponent(AppleExpressPaymentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {filter, map, switchMap, take} from 'rxjs/operators';
import {CommonModule} from '@angular/common';
import { ApplePay, CoreConfiguration, SubmitData, UIElement} from "@adyen/adyen-web";
import {AdyenCheckout, AdyenCheckoutError} from '@adyen/adyen-web/auto';
import {CheckoutAdyenConfigurationService} from "../../service/checkout-adyen-configuration.service";
import {AdyenConfigData} from "../../core/models/occ.config.models";
import {AdyenExpressOrderService} from "../../service/adyen-express-order.service";
import {EventService, Product, RoutingService, UserIdService,} from '@spartacus/core';
import {of, Subscription} from 'rxjs';
import {ActiveCartFacade} from '@spartacus/cart/base/root';
import {CheckoutAdyenConfigurationReloadEvent} from "../../events/checkout-adyen.events";

@Component({
selector: 'cx-apple-express-payment',
standalone: true,
imports: [CommonModule],
templateUrl: './apple-express-payment.component.html',
styleUrls: ['./apple-express-payment.component.css']
})
export class AppleExpressPaymentComponent implements OnInit, OnDestroy{

protected subscriptions = new Subscription();

@Input()
product: Product;

applePay: ApplePay;

private authorizedPaymentData: any;

constructor(
protected checkoutAdyenConfigurationService: CheckoutAdyenConfigurationService,
protected adyenOrderService: AdyenExpressOrderService,
protected routingService: RoutingService,
protected eventService: EventService,
protected activeCartFacade: ActiveCartFacade,
private userIdService: UserIdService,
) {}

ngOnInit(): void {

this.eventService.dispatch(
new CheckoutAdyenConfigurationReloadEvent()
);

this.subscriptions.add(
this.eventService.get(CheckoutAdyenConfigurationReloadEvent).subscribe(event => {
this.handleConfigurationReload(event);
})
);

this.initializeApplePay();
}

ngOnDestroy(): void {
this.subscriptions.unsubscribe();
if(this.applePay) this.applePay.unmount();
}

private initializeApplePay() {
this.checkoutAdyenConfigurationService.getCheckoutConfigurationState()
.pipe(
filter((state) => !state.loading),
take(1),
map((state) => state.data),
switchMap((config) => config ? this.setupAdyenCheckout(config) : of(null))
)
.subscribe({
error: (error) => console.error('Error initializing Apple Pay:', error)
});
}

private async setupAdyenCheckout(config: AdyenConfigData) {
const adyenCheckout = await AdyenCheckout(this.getAdyenCheckoutConfig(config));

this.applePay = new ApplePay(adyenCheckout, {
amount: {
currency: config.amount.currency,
value: config.amount.value
},
// Button config
buttonType: "check-out",
buttonColor: "black",
requiredShippingContactFields: [
"postalAddress",
"name",
"email"
],
onSubmit: (state, element: UIElement, actions) => this.handleOnSubmit(state, actions),
onAuthorized: (paymentData, actions) => {
this.authorizedPaymentData = paymentData;
actions.resolve();
},
onError: (error) => this.handleError(error)
})

this.applePay.isAvailable()
.then(() => this.applePay.mount("#apple-pay-button"))
}

private handleOnSubmit(state: SubmitData, actions: any) {
this.adyenOrderService.adyenPlaceAppleExpressOrder(state.data, this.authorizedPaymentData, this.product).subscribe(
result => {
if (result?.success) {
if (result.executeAction && result.paymentsAction !== undefined) {
this.applePay.handleAction(result.paymentsAction);
} else {
this.onSuccess();
}
} else {
console.error(result?.error);
actions.reject();
}
actions.resolve({ resultCode: 'Authorised' });
},
error => {
console.error(error);
actions.reject();
}
);
}

protected handleConfigurationReload(event: CheckoutAdyenConfigurationReloadEvent): void {
this.applePay.unmount();
this.activeCartFacade.getActiveCartId().pipe(
filter(cartId => !!cartId),
switchMap(cartId => this.userIdService.takeUserId().pipe(
switchMap(userId => this.checkoutAdyenConfigurationService.fetchCheckoutConfiguration(userId, cartId))
))
).subscribe(async config => {
if (config) {
await this.setupAdyenCheckout(config)
}
});
}

protected getAdyenCheckoutConfig(adyenConfig: AdyenConfigData): CoreConfiguration {
return {
paymentMethodsResponse: {
paymentMethods: adyenConfig.paymentMethods,
storedPaymentMethods: adyenConfig.storedPaymentMethodList
},
locale: adyenConfig.shopperLocale,
environment: this.castToEnvironment(adyenConfig.environmentMode),
clientKey: adyenConfig.adyenClientKey,
session: {
id: adyenConfig.sessionData.id,
sessionData: adyenConfig.sessionData.sessionData
},
countryCode: adyenConfig.countryCode ? adyenConfig.countryCode : 'US',
analytics: {
enabled: false
},
//@ts-ignore
risk: {
enabled: true
}
};
}

protected castToEnvironment(env: string): CoreConfiguration['environment'] {
const validEnvironments: CoreConfiguration['environment'][] = ['test', 'live', 'live-us', 'live-au', 'live-apse', 'live-in'];
if (validEnvironments.includes(env as CoreConfiguration['environment'])) {
return env as CoreConfiguration['environment'];
}
throw new Error(`Invalid environment: ${env}`);
}

handleError(error: AdyenCheckoutError) {}

onSuccess(): void {
this.routingService.go({ cxRoute: 'orderConfirmation' });
}
}
Loading

0 comments on commit b219a27

Please sign in to comment.