Stripe Pricing + Subscriptions

This commit is contained in:
2024-08-21 21:13:43 +02:00
parent 48bff89526
commit b4609d07ba
16 changed files with 969 additions and 1035 deletions

View File

@@ -12,6 +12,7 @@ import { HomeComponent } from './pages/home/home.component';
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
import { LoginComponent } from './pages/login/login.component';
import { PricingComponent } from './pages/pricing/pricing.component';
import { AccountComponent } from './pages/subscription/account/account.component';
import { EditBusinessListingComponent } from './pages/subscription/edit-business-listing/edit-business-listing.component';
@@ -56,6 +57,10 @@ export const routes: Routes = [
canActivate: [ListingCategoryGuard],
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
},
{
path: 'login/:page',
component: LoginComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
},
{
path: 'notfound',
component: NotFoundComponent,

View File

@@ -98,7 +98,7 @@ export class HeaderComponent {
}
login() {
this.keycloakService.login({
redirectUri: window.location.href,
redirectUri: `${window.location.origin}/login${this.router.routerState.snapshot.url}`,
});
}
register() {

View File

@@ -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() {

View 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}`]);
}
}

View File

@@ -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 });

View File

@@ -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>

View File

@@ -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 : '';
}
}

View File

@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Subscription } from '../../../../bizmatch-server/src/models/main.model';
import { StripeSubscription } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({
@@ -11,7 +11,7 @@ export class SubscriptionsService {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) {}
getAllSubscriptions(id: string): Observable<Subscription[]> {
return this.http.get<Subscription[]>(`${this.apiBaseUrl}/bizmatch/user/subscriptions/${id}`);
getAllSubscriptions(email: string): Observable<StripeSubscription[]> {
return this.http.get<StripeSubscription[]>(`${this.apiBaseUrl}/bizmatch/payment/subscriptions/${email}`);
}
}