Stripe Pricing + Subscriptions
This commit is contained in:
@@ -84,7 +84,7 @@ export class HomeComponent {
|
||||
}
|
||||
login() {
|
||||
this.keycloakService.login({
|
||||
redirectUri: window.location.href,
|
||||
redirectUri: `${window.location.origin}/login${this.router.routerState.snapshot.url}`,
|
||||
});
|
||||
}
|
||||
register() {
|
||||
|
||||
38
bizmatch/src/app/pages/login/login.component.ts
Normal file
38
bizmatch/src/app/pages/login/login.component.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { SubscriptionsService } from '../../services/subscriptions.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { map2User } from '../../utils/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule],
|
||||
template: ``,
|
||||
})
|
||||
export class LoginComponent {
|
||||
page: string | undefined = this.activatedRoute.snapshot.params['page'] as string | undefined;
|
||||
constructor(public userService: UserService, private activatedRoute: ActivatedRoute, private keycloakService: KeycloakService, private router: Router, private subscriptionService: SubscriptionsService) {}
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const keycloakUser = map2User(token);
|
||||
const email = keycloakUser.email;
|
||||
const user = await this.userService.getByMail(email);
|
||||
if (!user.subscriptionPlan) {
|
||||
//this.router.navigate(['/pricing']);
|
||||
const subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(user.email));
|
||||
const activeSubscription = subscriptions.filter(s => s.status === 'active');
|
||||
if (activeSubscription.length > 0) {
|
||||
user.subscriptionPlan = activeSubscription[0].metadata['plan'] === 'Broker Plan' ? 'broker' : 'professional';
|
||||
this.userService.save(user);
|
||||
} else {
|
||||
this.router.navigate([`/pricing`]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.router.navigate([`/${this.page}`]);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { StripeService } from 'ngx-stripe';
|
||||
import { switchMap } from 'rxjs';
|
||||
import { Checkout, KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { SharedModule } from '../../shared/shared/shared.module';
|
||||
import { map2User } from '../../utils/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pricing',
|
||||
@@ -17,26 +20,40 @@ import { SharedModule } from '../../shared/shared/shared.module';
|
||||
export class PricingComponent {
|
||||
private apiBaseUrl = environment.apiBaseUrl;
|
||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||
constructor(public keycloakService: KeycloakService, private http: HttpClient, private stripeService: StripeService, private activatedRoute: ActivatedRoute) {}
|
||||
keycloakUser: KeycloakUser;
|
||||
constructor(public keycloakService: KeycloakService, private http: HttpClient, private stripeService: StripeService, private activatedRoute: ActivatedRoute, private userService: UserService, private router: Router) {}
|
||||
|
||||
ngOnInit() {
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
if (this.id) {
|
||||
this.checkout(atob(this.id));
|
||||
this.checkout({ priceId: atob(this.id), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
|
||||
}
|
||||
}
|
||||
|
||||
register(priceId?: string) {
|
||||
if (priceId) {
|
||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}` });
|
||||
async register(priceId?: string) {
|
||||
if (this.keycloakUser) {
|
||||
if (!priceId) {
|
||||
const user = await this.userService.getByMail(this.keycloakUser.email);
|
||||
user.subscriptionPlan = 'free';
|
||||
await this.userService.save(user);
|
||||
this.router.navigate([`/account`]);
|
||||
} else {
|
||||
this.checkout({ priceId: atob(this.id), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
|
||||
}
|
||||
} else {
|
||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
||||
if (priceId) {
|
||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}` });
|
||||
} else {
|
||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkout(priceId) {
|
||||
checkout(checkout: Checkout) {
|
||||
// Check the server.js tab to see an example implementation
|
||||
this.http
|
||||
.post(`${this.apiBaseUrl}/bizmatch/payment/create-checkout-session`, { priceId })
|
||||
.post(`${this.apiBaseUrl}/bizmatch/payment/create-checkout-session`, checkout)
|
||||
.pipe(
|
||||
switchMap((session: any) => {
|
||||
return this.stripeService.redirectToCheckout({ sessionId: session.id });
|
||||
|
||||
@@ -220,46 +220,42 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Level</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Start Date</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Modified</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">End Date</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Next Settlement</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@for (subscription of subscriptions; track subscriptions; let i=$index){
|
||||
<tr>
|
||||
@for (subscription of userSubscriptions; track userSubscriptions){
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ subscription.id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.level }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.start | date }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.modified | date }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.end | date }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.status }}</td>
|
||||
}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getLevel(i) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getStartDate(i) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getEndDate(i) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getNextSettlement(i) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getStatus(i) }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 sm:hidden">
|
||||
<div class="flex justify-start">
|
||||
<button routerLink="/pricing" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">Upgrade Subscription Plan</button>
|
||||
</div>
|
||||
<!-- <div class="mt-8 sm:hidden">
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-1">Membership Level</h3>
|
||||
<div class="space-y-2">
|
||||
@for (subscription of userSubscriptions; track userSubscriptions){
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<dl class="grid grid-cols-1 gap-x-4 gap-y-2 sm:grid-cols-2">
|
||||
<div class="sm:col-span-1 flex">
|
||||
<dt class="text-sm font-bold text-gray-500 mr-2">ID</dt>
|
||||
<dd class="text-sm text-gray-900">{{ subscription.id }}</dd>
|
||||
</div>
|
||||
<div class="sm:col-span-1 flex">
|
||||
<dt class="text-sm font-bold text-gray-500 mr-2">Level</dt>
|
||||
<dd class="text-sm text-gray-900">{{ subscription.level }}</dd>
|
||||
<dd class="text-sm text-gray-900">{{ level }}</dd>
|
||||
</div>
|
||||
<div class="sm:col-span-1 flex">
|
||||
<dt class="text-sm font-bold text-gray-500 mr-2">Start Date</dt>
|
||||
@@ -282,7 +278,7 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TitleCasePipe } from '@angular/common';
|
||||
import { DatePipe, TitleCasePipe } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
@@ -10,7 +10,7 @@ import { ImageCropperComponent } from 'ngx-image-cropper';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { AutoCompleteCompleteEvent, Invoice, Subscription, UploadParams, ValidationMessage, createDefaultUser, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { AutoCompleteCompleteEvent, Invoice, StripeSubscription, UploadParams, ValidationMessage, createDefaultUser, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
||||
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
||||
@@ -53,15 +53,13 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||
TooltipComponent,
|
||||
ValidatedCountyComponent,
|
||||
],
|
||||
providers: [TitleCasePipe],
|
||||
providers: [TitleCasePipe, DatePipe],
|
||||
templateUrl: './account.component.html',
|
||||
styleUrl: './account.component.scss',
|
||||
})
|
||||
export class AccountComponent {
|
||||
id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||
user: User;
|
||||
subscriptions: Array<Subscription>;
|
||||
userSubscriptions: Array<Subscription> = [];
|
||||
companyLogoUrl: string;
|
||||
profileUrl: string;
|
||||
type: 'company' | 'profile';
|
||||
@@ -79,9 +77,9 @@ export class AccountComponent {
|
||||
customerTypeOptions: Array<{ value: string; label: string }> = [];
|
||||
customerSubTypeOptions: Array<{ value: string; label: string }> = [];
|
||||
tooltipTarget = 'tooltip-areasServed';
|
||||
subscriptions: StripeSubscription[] | any[];
|
||||
constructor(
|
||||
public userService: UserService,
|
||||
private subscriptionService: SubscriptionsService,
|
||||
private geoService: GeoService,
|
||||
public selectOptions: SelectOptionsService,
|
||||
private cdref: ChangeDetectorRef,
|
||||
@@ -95,6 +93,8 @@ export class AccountComponent {
|
||||
private sharedService: SharedService,
|
||||
private titleCasePipe: TitleCasePipe,
|
||||
private validationMessagesService: ValidationMessagesService,
|
||||
private subscriptionService: SubscriptionsService,
|
||||
private datePipe: DatePipe,
|
||||
) {}
|
||||
async ngOnInit() {
|
||||
setTimeout(() => {
|
||||
@@ -109,7 +109,10 @@ export class AccountComponent {
|
||||
this.user = await this.userService.getByMail(email);
|
||||
}
|
||||
|
||||
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.id));
|
||||
this.subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.email));
|
||||
if (this.subscriptions.length === 0) {
|
||||
this.subscriptions = [{ ended_at: null, start_date: Math.floor(new Date(this.user.created).getTime() / 1000), status: null, metadata: { plan: 'Free Plan' } }];
|
||||
}
|
||||
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||
|
||||
@@ -133,7 +136,7 @@ export class AccountComponent {
|
||||
const confirmed = await this.confirmationService.showConfirmation({ message: 'Are you sure you want to switch to Buyer ? All your listings as well as all your professionals informations will be deleted' });
|
||||
if (confirmed) {
|
||||
const id = this.user.id;
|
||||
this.user = createDefaultUser(this.user.email, this.user.firstname, this.user.lastname, 'free');
|
||||
this.user = createDefaultUser(this.user.email, this.user.firstname, this.user.lastname, null);
|
||||
this.user.customerType = 'buyer';
|
||||
this.user.id = id;
|
||||
this.imageService.deleteLogoImagesByMail(this.user.email);
|
||||
@@ -244,4 +247,19 @@ export class AccountComponent {
|
||||
this.user.areasServed[index].county = null;
|
||||
}
|
||||
}
|
||||
getLevel(i: number) {
|
||||
return this.subscriptions[i].metadata.plan;
|
||||
}
|
||||
getStartDate(i: number) {
|
||||
return this.datePipe.transform(new Date(this.subscriptions[i].start_date * 1000));
|
||||
}
|
||||
getEndDate(i: number) {
|
||||
return this.subscriptions[i].status === 'trialing' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
|
||||
}
|
||||
getNextSettlement(i: number) {
|
||||
return this.subscriptions[i].status === 'active' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
|
||||
}
|
||||
getStatus(i: number) {
|
||||
return this.subscriptions[i].status ? this.subscriptions[i].status : '';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user