feature: implement payment method selector and checkout confirmation page
This commit is contained in:
parent
4546d309b8
commit
85b0fbf499
1
public/assets/images/mastercard-modern-design-.svg
Normal file
1
public/assets/images/mastercard-modern-design-.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg height="1524" viewBox="55.2 38.3 464.5 287.8" width="2500" xmlns="http://www.w3.org/2000/svg"><path d="m519.7 182.2c0 79.5-64.3 143.9-143.6 143.9s-143.6-64.4-143.6-143.9 64.2-143.9 143.5-143.9 143.7 64.4 143.7 143.9z" fill="#f79f1a"/><path d="m342.4 182.2c0 79.5-64.3 143.9-143.6 143.9s-143.6-64.4-143.6-143.9 64.3-143.9 143.6-143.9 143.6 64.4 143.6 143.9z" fill="#ea001b"/><path d="m287.4 68.9c-33.5 26.3-55 67.3-55 113.3s21.5 87 55 113.3c33.5-26.3 55-67.3 55-113.3s-21.5-86.9-55-113.3z" fill="#ff5f01"/></svg>
|
||||||
|
After Width: | Height: | Size: 516 B |
1
public/assets/images/stripe-4.svg
Normal file
1
public/assets/images/stripe-4.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="2500" height="1045" viewBox="0 0 512 214" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M35.982 83.484c0-5.546 4.551-7.68 12.09-7.68 10.808 0 24.461 3.272 35.27 9.103V51.484c-11.804-4.693-23.466-6.542-35.27-6.542C19.2 44.942 0 60.018 0 85.192c0 39.252 54.044 32.995 54.044 49.92 0 6.541-5.688 8.675-13.653 8.675-11.804 0-26.88-4.836-38.827-11.378v33.849c13.227 5.689 26.596 8.106 38.827 8.106 29.582 0 49.92-14.648 49.92-40.106-.142-42.382-54.329-34.845-54.329-50.774zm96.142-66.986l-34.702 7.395-.142 113.92c0 21.05 15.787 36.551 36.836 36.551 11.662 0 20.195-2.133 24.888-4.693V140.8c-4.55 1.849-27.022 8.391-27.022-12.658V77.653h27.022V47.36h-27.022l.142-30.862zm71.112 41.386L200.96 47.36h-30.72v124.444h35.556V87.467c8.39-10.951 22.613-8.96 27.022-7.396V47.36c-4.551-1.707-21.191-4.836-29.582 10.524zm38.257-10.524h35.698v124.444h-35.698V47.36zm0-10.809l35.698-7.68V0l-35.698 7.538V36.55zm109.938 8.391c-13.938 0-22.898 6.542-27.875 11.094l-1.85-8.818h-31.288v165.83l35.555-7.537.143-40.249c5.12 3.698 12.657 8.96 25.173 8.96 25.458 0 48.64-20.48 48.64-65.564-.142-41.245-23.609-63.716-48.498-63.716zm-8.533 97.991c-8.391 0-13.37-2.986-16.782-6.684l-.143-52.765c3.698-4.124 8.818-6.968 16.925-6.968 12.942 0 21.902 14.506 21.902 33.137 0 19.058-8.818 33.28-21.902 33.28zM512 110.08c0-36.409-17.636-65.138-51.342-65.138-33.85 0-54.33 28.73-54.33 64.854 0 42.808 24.179 64.426 58.88 64.426 16.925 0 29.725-3.84 39.396-9.244v-28.445c-9.67 4.836-20.764 7.823-34.844 7.823-13.796 0-26.027-4.836-27.591-21.618h69.547c0-1.85.284-9.245.284-12.658zm-70.258-13.511c0-16.071 9.814-22.756 18.774-22.756 8.675 0 17.92 6.685 17.92 22.756h-36.694z" fill="#6772E5"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
1
public/assets/images/visa-10.svg
Normal file
1
public/assets/images/visa-10.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg height="812" viewBox="0.5 0.5 999 323.684" width="2500" xmlns="http://www.w3.org/2000/svg"><path d="M651.185.5c-70.933 0-134.322 36.766-134.322 104.694 0 77.9 112.423 83.28 112.423 122.415 0 16.478-18.884 31.229-51.137 31.229-45.773 0-79.984-20.611-79.984-20.611l-14.638 68.547s39.41 17.41 91.734 17.41c77.552 0 138.576-38.572 138.576-107.66 0-82.316-112.89-87.537-112.89-123.86 0-12.91 15.501-27.053 47.662-27.053 36.286 0 65.892 14.99 65.892 14.99l14.326-66.204S696.614.5 651.185.5zM2.218 5.497L.5 15.49s29.842 5.461 56.719 16.356c34.606 12.492 37.072 19.765 42.9 42.353l63.51 244.832h85.138L379.927 5.497h-84.942L210.707 218.67l-34.39-180.696c-3.154-20.68-19.13-32.477-38.685-32.477H2.218zm411.865 0L347.449 319.03h80.999l66.4-313.534h-80.765zm451.759 0c-19.532 0-29.88 10.457-37.474 28.73L709.699 319.03h84.942l16.434-47.468h103.483l9.994 47.468H999.5L934.115 5.497h-68.273zm11.047 84.707l25.178 117.653h-67.454z" fill="#1434cb"/></svg>
|
||||||
|
After Width: | Height: | Size: 945 B |
37
src/app/core/services/session-storage.service.ts
Normal file
37
src/app/core/services/session-storage.service.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class SessionStorageService {
|
||||||
|
setItem<T>(key: string, value: T) {
|
||||||
|
try {
|
||||||
|
const item = JSON.stringify(value);
|
||||||
|
sessionStorage.setItem(key, item);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not set item", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem<T>(key: string): T | null {
|
||||||
|
try {
|
||||||
|
const item = sessionStorage.getItem(key);
|
||||||
|
return item ? (JSON.parse(item) as T) : null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not get item", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Error if key is not found.
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
removeItem(key: string): void {
|
||||||
|
sessionStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
sessionStorage.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/core/services/session-storage.spec.ts
Normal file
16
src/app/core/services/session-storage.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { SessionStorage } from "./session-storage.service";
|
||||||
|
|
||||||
|
describe("SessionStorage", () => {
|
||||||
|
let service: SessionStorage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(SessionStorage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be created", () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,7 +3,7 @@
|
|||||||
<app-stepper [currentStep]="currentStepNumber()" [steps]="steps" />
|
<app-stepper [currentStep]="currentStepNumber()" [steps]="steps" />
|
||||||
</div>
|
</div>
|
||||||
<section class="mt-10">
|
<section class="mt-10">
|
||||||
@if (currentStepNumber() > 0) {
|
@if (currentStepNumber() > 0 && currentStepNumber() < 3) {
|
||||||
<app-go-back
|
<app-go-back
|
||||||
[route]="steps[currentStepNumber() - 1].route"
|
[route]="steps[currentStepNumber() - 1].route"
|
||||||
[text]="steps[currentStepNumber() - 1 ].label"
|
[text]="steps[currentStepNumber() - 1 ].label"
|
||||||
@ -11,10 +11,11 @@
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="grid md:grid-cols-6 gap-10">
|
<div [class.md:grid-cols-6]="currentStepNumber() < 3" class="grid gap-10">
|
||||||
<div class="md:col-span-4">
|
<div class="md:col-span-4">
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
</div>
|
</div>
|
||||||
|
@if (currentStepNumber() < 3) {
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<app-order-summery class="hidden md:block" />
|
<app-order-summery class="hidden md:block" />
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
@ -29,7 +30,11 @@
|
|||||||
<div class="text-red-500 text-sm p-4 mt-4 rounded-xl bg-red-50">
|
<div class="text-red-500 text-sm p-4 mt-4 rounded-xl bg-red-50">
|
||||||
Please select an address
|
Please select an address
|
||||||
</div>
|
</div>
|
||||||
}
|
} @if (paymentMethodControl.invalid && paymentMethodControl.touched) {
|
||||||
|
<div class="text-red-500 text-sm p-4 mt-4 rounded-xl bg-red-50">
|
||||||
|
Please select an payment checkout
|
||||||
|
</div>
|
||||||
|
} @if (currentStepNumber() === 1) {
|
||||||
<button
|
<button
|
||||||
(click)="proceedToPayment()"
|
(click)="proceedToPayment()"
|
||||||
[disabled]="addressIdControl.invalid && addressIdControl.touched || orderCreationLoading()"
|
[disabled]="addressIdControl.invalid && addressIdControl.touched || orderCreationLoading()"
|
||||||
@ -39,8 +44,20 @@
|
|||||||
Proceed to payment
|
Proceed to payment
|
||||||
</app-loading-spinner>
|
</app-loading-spinner>
|
||||||
</button>
|
</button>
|
||||||
|
} @if (currentStepNumber() === 2) {
|
||||||
|
<button
|
||||||
|
(click)="proceedToCheckout()"
|
||||||
|
[disabled]="paymentMethodControl.invalid && paymentMethodControl.touched || proceedToPaymentLoading()"
|
||||||
|
class="btn btn-primary w-full mt-4"
|
||||||
|
>
|
||||||
|
<app-loading-spinner [isLoading]="proceedToPaymentLoading()">
|
||||||
|
Proceed to checkout
|
||||||
|
</app-loading-spinner>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Routes } from "@angular/router";
|
|||||||
import { Address } from "./address/address";
|
import { Address } from "./address/address";
|
||||||
import { Checkout } from "./checkout";
|
import { Checkout } from "./checkout";
|
||||||
import { Payment } from "@app/features/checkout/payment/payment";
|
import { Payment } from "@app/features/checkout/payment/payment";
|
||||||
|
import Confirmation from "@app/features/checkout/confirmation/confirmation";
|
||||||
|
|
||||||
export const checkoutRoutes: Routes = [
|
export const checkoutRoutes: Routes = [
|
||||||
{
|
{
|
||||||
@ -16,6 +17,10 @@ export const checkoutRoutes: Routes = [
|
|||||||
path: "payment",
|
path: "payment",
|
||||||
component: Payment,
|
component: Payment,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "confirmation",
|
||||||
|
component: Confirmation,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { OrderSummery } from "@app/features/checkout/components/order-summery/or
|
|||||||
import { GoBack } from "@shared/components/go-back/go-back";
|
import { GoBack } from "@shared/components/go-back/go-back";
|
||||||
import { AddressService } from "@app/features/checkout/services/address-service";
|
import { AddressService } from "@app/features/checkout/services/address-service";
|
||||||
import { LoadingSpinner } from "@shared/components/loading-spinner/loading-spinner";
|
import { LoadingSpinner } from "@shared/components/loading-spinner/loading-spinner";
|
||||||
import { OrderService } from "@app/features/checkout/services/order-service";
|
import { CheckoutResponse, OrderService } from "@app/features/checkout/services/order-service";
|
||||||
import { CartService } from "@core/services/cart-service";
|
import { CartService } from "@core/services/cart-service";
|
||||||
import { CartModel } from "@core/models/cart.model";
|
import { CartModel } from "@core/models/cart.model";
|
||||||
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
||||||
@ -23,17 +23,21 @@ export class Checkout implements OnInit {
|
|||||||
{ label: "Cart", route: "" },
|
{ label: "Cart", route: "" },
|
||||||
{ label: "Address", route: "/checkout/address" },
|
{ label: "Address", route: "/checkout/address" },
|
||||||
{ label: "Payment", route: "/checkout/payment" },
|
{ label: "Payment", route: "/checkout/payment" },
|
||||||
{ label: "Confirm", route: "/checkout/address" },
|
{ label: "Confirmation", route: "/checkout/confirmation" },
|
||||||
];
|
];
|
||||||
destroyRef = inject(DestroyRef);
|
destroyRef = inject(DestroyRef);
|
||||||
orderCreationLoading = signal(false);
|
orderCreationLoading = signal(false);
|
||||||
|
proceedToPaymentLoading = signal(false);
|
||||||
|
|
||||||
private addressService = inject(AddressService);
|
private addressService = inject(AddressService);
|
||||||
addressIdControl = this.addressService.addressIdControl;
|
addressIdControl = this.addressService.addressIdControl;
|
||||||
|
|
||||||
private orderService = inject(OrderService);
|
private orderService = inject(OrderService);
|
||||||
|
paymentMethodControl = this.orderService.paymentMethodForm;
|
||||||
|
|
||||||
private cartService = inject(CartService);
|
private cartService = inject(CartService);
|
||||||
private cart: CartModel | undefined;
|
private cart: CartModel | undefined;
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
|
||||||
protected currentStepNumber = toSignal(
|
protected currentStepNumber = toSignal(
|
||||||
this.router.events.pipe(
|
this.router.events.pipe(
|
||||||
filter((event): event is NavigationEnd => event instanceof NavigationEnd), // Added TS type guard
|
filter((event): event is NavigationEnd => event instanceof NavigationEnd), // Added TS type guard
|
||||||
@ -74,4 +78,32 @@ export class Checkout implements OnInit {
|
|||||||
await this.router.navigate(["/checkout/payment"]);
|
await this.router.navigate(["/checkout/payment"]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected proceedToCheckout() {
|
||||||
|
if (this.paymentMethodControl.invalid) {
|
||||||
|
this.paymentMethodControl.markAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.proceedToPaymentLoading.set(true);
|
||||||
|
this.orderService
|
||||||
|
.checkout(this.paymentMethodControl.value!)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
finalize(() => this.proceedToPaymentLoading.set(false)),
|
||||||
|
)
|
||||||
|
.subscribe((response) => this.handleCheckout(response));
|
||||||
|
}
|
||||||
|
private handleCheckout(data: CheckoutResponse): void {
|
||||||
|
if (data.success) {
|
||||||
|
if (data.method === "stripeCheckout") {
|
||||||
|
this.handleStripeCheckout(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private handleStripeCheckout(data: CheckoutResponse) {
|
||||||
|
console.log(data);
|
||||||
|
if (data.redirectUrl) {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
<div class="card min-h-20">
|
||||||
|
<ng-content />
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { PaymentMethodCard } from "./payment-method-card";
|
||||||
|
|
||||||
|
describe("PaymentMethodCard", () => {
|
||||||
|
let component: PaymentMethodCard;
|
||||||
|
let fixture: ComponentFixture<PaymentMethodCard>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PaymentMethodCard],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PaymentMethodCard);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-payment-method-card",
|
||||||
|
imports: [],
|
||||||
|
templateUrl: "./payment-method-card.html",
|
||||||
|
styleUrl: "./payment-method-card.css",
|
||||||
|
})
|
||||||
|
export class PaymentMethodCard {}
|
||||||
27
src/app/features/checkout/confirmation/confirmation.html
Normal file
27
src/app/features/checkout/confirmation/confirmation.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<div class="flex flex-col gap-4 items-center justify-center">
|
||||||
|
<div class="card max-w-md text-center flex items-center flex-col gap-4">
|
||||||
|
<div class="rounded-full bg-green-100 p-4">
|
||||||
|
<lucide-angular [img]="BadgeCheck" class="w-6 h-6 text-green-500" />
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-medium text-green-600">Payment Successful</p>
|
||||||
|
<p class="text-sm text-gray-400">
|
||||||
|
Your payment processed successfully. You will receive a confirmation email shortly.
|
||||||
|
</p>
|
||||||
|
<article class="rounded-xl bg-gray-100 px-3 py-4 text-gray-500 text-sm w-full space-y-4">
|
||||||
|
<article class="flex justify-between border-b border-b-gray-200 pb-4">
|
||||||
|
<p class="font-medium">Amount</p>
|
||||||
|
<p>Rs. 20000</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="flex justify-between">
|
||||||
|
<p class="font-medium">Transaction ID</p>
|
||||||
|
<p class="truncate">text_ch_989789y789jhhg8h8</p>
|
||||||
|
</article>
|
||||||
|
<article class="flex justify-between">
|
||||||
|
<p class="font-medium">Payment method</p>
|
||||||
|
<p>Stripe Checkout</p>
|
||||||
|
</article>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<app-go-back class="w-min-content! flex-nowrap" route="/" text="Continue Shopping" />
|
||||||
|
</div>
|
||||||
22
src/app/features/checkout/confirmation/confirmation.spec.ts
Normal file
22
src/app/features/checkout/confirmation/confirmation.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import Confirmation from "./confirmation";
|
||||||
|
|
||||||
|
describe("Confirmation", () => {
|
||||||
|
let component: Confirmation;
|
||||||
|
let fixture: ComponentFixture<Confirmation>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Confirmation],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Confirmation);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
24
src/app/features/checkout/confirmation/confirmation.ts
Normal file
24
src/app/features/checkout/confirmation/confirmation.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Component, inject } from "@angular/core";
|
||||||
|
import { BadgeCheck, LucideAngularModule } from "lucide-angular";
|
||||||
|
import { GoBack } from "@shared/components/go-back/go-back";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-confirmation",
|
||||||
|
imports: [LucideAngularModule, GoBack],
|
||||||
|
templateUrl: "./confirmation.html",
|
||||||
|
styleUrl: "./confirmation.css",
|
||||||
|
})
|
||||||
|
class Confirmation {
|
||||||
|
protected readonly BadgeCheck = BadgeCheck;
|
||||||
|
private route = inject(ActivatedRoute);
|
||||||
|
private orderId: string | null = null;
|
||||||
|
private sessionId: string | null = null;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.orderId = this.route.snapshot.paramMap.get("order_id");
|
||||||
|
this.sessionId = this.route.snapshot.paramMap.get("session_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Confirmation;
|
||||||
@ -1 +1,51 @@
|
|||||||
<p>payment works!</p>
|
<div class="flex flex-col gap-4">
|
||||||
|
<app-payment-method-card>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<input
|
||||||
|
(change)="onPaymentMethodSelected('stripeCheckout')"
|
||||||
|
[formControl]="paymentMethodForm"
|
||||||
|
name="payment-method"
|
||||||
|
type="radio"
|
||||||
|
value="stripeCheckout"
|
||||||
|
/>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex justify-between mb-2">
|
||||||
|
<div class="">
|
||||||
|
<img alt="" class="w-25" src="/assets/images/stripe-4.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="card p-3! flex justify-center items-center">
|
||||||
|
<img alt="" class="h-3" src="/assets/images/visa-10.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="card p-3! flex justify-center items-center">
|
||||||
|
<img alt="" class="h-5" src="/assets/images/mastercard-modern-design-.svg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-600">Pay using card via stripe checkout.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-payment-method-card>
|
||||||
|
<app-payment-method-card>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<input
|
||||||
|
(change)="onPaymentMethodSelected('cashOnDelivery')"
|
||||||
|
[formControl]="paymentMethodForm"
|
||||||
|
name="payment-method"
|
||||||
|
type="radio"
|
||||||
|
value="cashOnDelivery"
|
||||||
|
/>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<div class="text-gray-700">
|
||||||
|
<lucide-angular [img]="coinsIcon" class="h-10" />
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<p class="text-xl font-medium text-gray-700">Cash on Delivery</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-600">Additional charges may apply.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-payment-method-card>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,28 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, inject, OnInit } from "@angular/core";
|
||||||
|
import { PaymentMethodCard } from "@app/features/checkout/components/payment-method-card/payment-method-card";
|
||||||
|
import { Coins, LucideAngularModule } from "lucide-angular";
|
||||||
|
import { ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { OrderService } from "@app/features/checkout/services/order-service";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-payment",
|
selector: "app-payment",
|
||||||
imports: [],
|
imports: [PaymentMethodCard, LucideAngularModule, ReactiveFormsModule],
|
||||||
templateUrl: "./payment.html",
|
templateUrl: "./payment.html",
|
||||||
styleUrl: "./payment.css",
|
styleUrl: "./payment.css",
|
||||||
})
|
})
|
||||||
export class Payment {}
|
export class Payment implements OnInit {
|
||||||
|
coinsIcon = Coins;
|
||||||
|
private orderService = inject(OrderService);
|
||||||
|
protected paymentMethodForm = this.orderService.paymentMethodForm;
|
||||||
|
private route = inject(ActivatedRoute);
|
||||||
|
private orderId: string | null = null;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.orderId = this.route.snapshot.paramMap.get("order_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
onPaymentMethodSelected(paymentMethod: string) {
|
||||||
|
this.paymentMethodForm.setValue(paymentMethod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,22 +2,63 @@ import { inject, Injectable } from "@angular/core";
|
|||||||
import { API_URL } from "@core/tokens/api-url-tokens";
|
import { API_URL } from "@core/tokens/api-url-tokens";
|
||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient } from "@angular/common/http";
|
||||||
import { AuthService } from "@core/services/auth-service";
|
import { AuthService } from "@core/services/auth-service";
|
||||||
|
import { FormControl, Validators } from "@angular/forms";
|
||||||
|
import { tap } from "rxjs";
|
||||||
|
import { SessionStorageService } from "@core/services/session-storage.service";
|
||||||
|
|
||||||
|
export interface CheckoutResponse {
|
||||||
|
success: boolean;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
method: string;
|
||||||
|
redirectUrl: string | null;
|
||||||
|
errorMessage: string | null;
|
||||||
|
}
|
||||||
export interface OrderRequest {
|
export interface OrderRequest {
|
||||||
addressId: number;
|
addressId: number;
|
||||||
cartId: number;
|
cartId: number;
|
||||||
}
|
}
|
||||||
|
export interface OrderResponse {
|
||||||
|
orderId: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
})
|
})
|
||||||
export class OrderService {
|
export class OrderService {
|
||||||
http = inject(HttpClient);
|
public paymentMethodForm = new FormControl<string | null>(null, Validators.required);
|
||||||
apiUrl = inject(API_URL);
|
private currentOrderId: number | null = null;
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private apiUrl = inject(API_URL);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private user = this.authService.user;
|
private user = this.authService.user;
|
||||||
|
private sessionStorage = inject(SessionStorageService);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const cachedOrderId = this.sessionStorage.getItem<number>("orderId");
|
||||||
|
if (cachedOrderId) {
|
||||||
|
this.currentOrderId = cachedOrderId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createOrder(data: OrderRequest) {
|
createOrder(data: OrderRequest) {
|
||||||
return this.http.post(`${this.apiUrl}/users/${this.user()?.id}/orders`, data);
|
return this.http
|
||||||
|
.post<OrderResponse>(`${this.apiUrl}/users/${this.user()?.id}/orders`, data)
|
||||||
|
.pipe(
|
||||||
|
tap((response) => {
|
||||||
|
this.currentOrderId = response.orderId;
|
||||||
|
this.sessionStorage.setItem<number>("orderId", response.orderId);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkout(mode: string) {
|
||||||
|
return this.http.post<CheckoutResponse>(
|
||||||
|
`${this.apiUrl}/orders/${this.currentOrderId}/payments`,
|
||||||
|
{
|
||||||
|
mode: mode,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<a [routerLink]="route" class="flex space-x-2 w-min my-4 text-gray-600 hover:text-blue-500 text-sm">
|
<a [routerLink]="route" class="flex space-x-2 w-max my-4 text-gray-600 hover:text-blue-500 text-sm">
|
||||||
<lucide-angular [img]="MoveLeftIcon" class="w-4" />
|
<lucide-angular [img]="MoveLeftIcon" class="w-4" />
|
||||||
<p class="font-medium">{{ text }}</p>
|
<p class="font-medium">{{ text }}</p>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -3,14 +3,14 @@
|
|||||||
<li class="flex flex-col items-start">
|
<li class="flex flex-col items-start">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<a
|
<a
|
||||||
[routerLink]="$index <= currentStep ? step.route : ''"
|
|
||||||
[class.cursor-not-allowed]="$index > currentStep"
|
|
||||||
[class.bg-blue-600]="$index <= currentStep"
|
[class.bg-blue-600]="$index <= currentStep"
|
||||||
|
[class.cursor-not-allowed]="$index > currentStep"
|
||||||
|
[routerLink]="$index <= currentStep ? step.route : '#'"
|
||||||
class="btn py-0! px-1 rounded-full! w-min"
|
class="btn py-0! px-1 rounded-full! w-min"
|
||||||
>
|
>
|
||||||
<lucide-angular
|
<lucide-angular
|
||||||
[class.text-white]="$index <= currentStep"
|
|
||||||
[class.text-gray-400]="$index > currentStep"
|
[class.text-gray-400]="$index > currentStep"
|
||||||
|
[class.text-white]="$index <= currentStep"
|
||||||
[img]="$index <= currentStep ? CheckIcon : CirecleIcon"
|
[img]="$index <= currentStep ? CheckIcon : CirecleIcon"
|
||||||
class="w-4"
|
class="w-4"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user