Skip to content

Commit

Permalink
feat: device identification on analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
conradbekondo committed Feb 3, 2025
1 parent d135030 commit 9563498
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 80 deletions.
52 changes: 37 additions & 15 deletions lib/models/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
import { z } from "zod";

export const WalletTransferSchema = z.object({
id: z.string(),
from: z.string().uuid().nullable(),
to: z.string().uuid(),
amount: z.number(),
status: z.enum(['pending', 'cancelled', 'complete']),
type: z.enum(['funding', 'reward', 'withdrawal']),
date: z.date(),
payment: z.object({
id: z.string().uuid(),
currency: z.string(),
amount: z.number(),
status: z.enum(['pending', 'cancelled', 'complete'])
}).nullable()
burst: z.union([z.string(), z.date()]).pipe(z.coerce.date()),
creditAllocationId: z.string().uuid().nullable(),
creditAllocation: z
.object({
allocated: z.union([z.string(), z.number()]).pipe(z.coerce.number()),
status: z.string(),
})
.nullable(),
credits: z.union([z.string(), z.number()]).pipe(z.coerce.number()),
notes: z.string().nullable(),
paymentTransaction: z
.object({
status: z.string(),
amount: z.union([z.string(), z.number()]).pipe(z.coerce.number()),
currency: z.string(),
})
.nullable(),
paymentTransactionId: z.string().nullable(),
recordedAt: z.union([z.string(), z.date()]).pipe(z.coerce.date()),
status: z.string(),
transaction: z.string().uuid(),
type: z.string(),
});

export const WalletTransferGroupSchema = z.object({
burst: z.union([z.string(), z.date()]).pipe(z.coerce.date()),
fundingRewardsRatio: z
.union([z.string(), z.number()])
.pipe(z.coerce.number()),
owner: z.union([z.string(), z.number()]).pipe(z.coerce.number()),
transferredCredits: z.union([z.string(), z.number()]).pipe(z.coerce.number()),
wallet: z.string().uuid(),
transfers: z.array(WalletTransferSchema),
});



export const WalletBalanceSchema = z.object({
balance: z.union([z.string(), z.number()]).pipe(z.coerce.number()),
ownerId: z.number(),
Expand All @@ -29,8 +51,8 @@ export const BalancesSchema = z.object({

export const WalletTransfersResponseSchema = z.object({
total: z.number(),
data: z.array(WalletTransferSchema)
})
groups: z.array(WalletTransferGroupSchema),
});

export type WalletBalanceInfo = z.infer<typeof WalletBalanceSchema>;
export type WalletTransfer = z.infer<typeof WalletTransferSchema>;
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@angular/platform-browser": "^19.1.1",
"@angular/platform-browser-dynamic": "^19.1.1",
"@angular/router": "^19.1.1",
"@aws-crypto/sha256-js": "^5.2.0",
"@ngxs/devtools-plugin": "^19.0.0",
"@ngxs/logger-plugin": "^19.0.0",
"@ngxs/router-plugin": "^19.0.0",
Expand All @@ -37,6 +38,7 @@
"tailwindcss-primeui": "^0.3.4",
"tslib": "^2.8.1",
"zod": "^3.24.1",
"zod-validation-error": "^3.4.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
Expand Down
78 changes: 78 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 0 additions & 26 deletions scripts/seed.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { NotFoundComponent } from './pages/not-found/not-found.component';
const signedInGuard = authGuard('/auth/login');

export const appRoutes: Routes = [
{
path: 'analytics',
title: 'Analytics',
loadComponent: () => import('./pages/analytics/analytics.component').then(m => m.AnalyticsComponent)
},
{
path: 'auth',
loadChildren: () => import('./auth.routes').then(m => m.authRoutes)
Expand Down
2 changes: 2 additions & 0 deletions src/app/directives/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { PhoneDirective } from './phone.directive';
export { Recaptcha } from './recaptcha/re-captcha.directive';
66 changes: 66 additions & 0 deletions src/app/directives/recaptcha/re-captcha.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Directive, input, OnDestroy, OnInit, output } from '@angular/core';
import { AsyncSubject, lastValueFrom, switchMap } from 'rxjs';

declare global {
interface Window {
grecaptcha: {
ready: (fn: () => void) => void;
execute: (key: string, opts: { action: string }) => Promise<string>;
},
recaptchaLoaded?: () => void;
}
}

@Directive({
selector: '[tmReCaptcha]'
})
export class Recaptcha implements OnInit, OnDestroy {
private static scriptElement?: HTMLScriptElement;
private static instanceCount = 0;
private static loaded = new AsyncSubject<void>();
readonly recaptchaKey = input.required<string>();
readonly ready = output();

constructor() {
Recaptcha.instanceCount++;
}

async getToken(action: string) {
if (Recaptcha.loaded.closed)
return await window.grecaptcha.execute(this.recaptchaKey(), { action });
return await lastValueFrom(Recaptcha.loaded.pipe(switchMap(() => window.grecaptcha.execute(this.recaptchaKey(), { action }))));
}

ngOnInit(): void {
let script = Recaptcha.scriptElement;
if (!script) {
window.recaptchaLoaded = () => {
Recaptcha.loaded.next();
Recaptcha.loaded.complete();
this.ready.emit();
}
script = document.createElement('script');
Recaptcha.scriptElement = script;
const url = new URL('/recaptcha/api.js', 'https://google.com');
url.searchParams.set('render', this.recaptchaKey());
url.searchParams.set('onload', 'recaptchaLoaded');
url.searchParams.set('trustedtypes', 'true');

script.src = url.toString();
script.async = true;
script.defer = true;
script.id = 'recaptcha-script';

document.head.appendChild(script);
} else {
this.ready.emit();
}
}

ngOnDestroy() {
Recaptcha.instanceCount = Math.max(Recaptcha.instanceCount - 1, 0);
const instanceCount = Recaptcha.instanceCount;
if (instanceCount > 0) return;
document.getElementById('recaptcha-script')?.remove();
}
}
2 changes: 1 addition & 1 deletion src/app/interceptors/access-token.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const accessTokenInterceptor: HttpInterceptorFn = (req, next) => {
const store = inject(Store);
const token = store.selectSnapshot(accessToken);

if (req.url.startsWith('/api') && token) {
if (req.url.startsWith(environment.apiOrigin) && token) {
return next(req.clone({ setHeaders: { 'Authorization': `Bearer ${token}` } })).pipe(
catchError((e: HttpErrorResponse) => {
if (e.status == 401 && req.url != environment.apiOrigin + '/auth/revoke-token')
Expand Down
9 changes: 9 additions & 0 deletions src/app/pages/analytics/analytics.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div tmReCaptcha [recaptchaKey]="siteKey" class="flex items-center justify-center h-full" (ready)="onRecaptchaReady()">
<p>
@if(failed()) {
Broken URL. Redirecting to home page...
}@else {
Redirecting...
}
</p>
</div>
3 changes: 3 additions & 0 deletions src/app/pages/analytics/analytics.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
@apply w-full h-full block
}
Loading

0 comments on commit 9563498

Please sign in to comment.