Skip to content

Commit cf6b864

Browse files
committed
feat(release): add mobile APK download feature
- Add ReleaseService on server to fetch mobile APK URL from GitHub releases - Expose getMobileApkUrl API via new TRPC releasesRouter - Inject ReleaseService in frontend app and login page components - Add mobile app download buttons with error handling in UI - Update package.json and lock with axios and its types dependencies - Adjust UI styles for new download button and existing elements
1 parent 790ff3d commit cf6b864

File tree

10 files changed

+212
-15
lines changed

10 files changed

+212
-15
lines changed

web/package-lock.json

Lines changed: 48 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@trpc/client": "^10.45.2",
5656
"@trpc/server": "^10.45.2",
5757
"@zxing/browser": "^0.1.5",
58+
"axios": "^1.13.2",
5859
"cors": "^2.8.5",
5960
"es-toolkit": "^1.41.0",
6061
"isomorphic-fetch": "^3.0.0",
@@ -81,6 +82,7 @@
8182
"@angular/compiler-cli": "^20.0.0",
8283
"@tailwindcss/postcss": "^4.1.17",
8384
"@tailwindcss/vite": "^4.1.18",
85+
"@types/axios": "^0.9.36",
8486
"@types/node": "^24.9.2",
8587
"@types/qrcode": "^1.5.6",
8688
"@typescript-eslint/eslint-plugin": "^8.46.4",

web/src/app/app.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ShowNavGuard } from './guards/nav.guard';
1818
import { AuthService } from './services/auth.service';
1919
import { ErrorHandlerService } from './services/error-handler.service';
2020
import { ProfileService } from './services/profile.service';
21+
import { ReleaseService } from './services/release.service';
2122

2223
export const routeMeta: RouteMeta = {
2324
canActivate: [ShowNavGuard],
@@ -95,6 +96,15 @@ export const routeMeta: RouteMeta = {
9596
<i-lucide name="settings" class="w-6 h-6 mr-0 lg:mr-3"></i-lucide>
9697
<span class="mobile-hidden lg:inline">Settings</span>
9798
</a>
99+
100+
101+
<button
102+
(click)="downloadMobileApk()"
103+
class="flex items-center p-3 rounded-xl text-gray-600 font-medium transition-all duration-300 hover:bg-gray-100 hover:text-gray-800 w-full text-left"
104+
>
105+
<i-lucide name="smartphone" class="w-6 h-6 mr-0 lg:mr-3"></i-lucide>
106+
<span class="mobile-hidden lg:inline">Mobile App</span>
107+
</button>
98108
</nav>
99109
100110
<div
@@ -103,11 +113,12 @@ export const routeMeta: RouteMeta = {
103113
<div class="flex justify-between items-center w-full px-2">
104114
<button
105115
(click)="signOut()"
106-
class="mt-2 text-red-500 font-medium hover:text-red-700 transition-colors flex items-center"
116+
class="p-2 rounded-lg bg-gray-200 hover:bg-gray-300 text-red-500 font-medium transition-colors flex items-center"
107117
>
108118
<i-lucide name="log-out" class="w-5 h-5 mr-0 lg:mr-2"></i-lucide>
109119
<span class="mobile-hidden lg:inline">Sign Out</span>
110120
</button>
121+
111122
<color-scheme-switcher class="flex items-center" />
112123
</div>
113124
</div>
@@ -128,6 +139,7 @@ export class AppComponent implements OnInit {
128139
private readonly router = inject(Router);
129140
private readonly appInitializerService = inject(AppInitializerService);
130141
private readonly errorHandler = inject(ErrorHandlerService);
142+
private readonly releaseService = inject(ReleaseService);
131143

132144
ngOnInit(): void {
133145
this.router.events
@@ -175,4 +187,30 @@ export class AppComponent implements OnInit {
175187
new Error('This feature is not available yet')
176188
);
177189
}
190+
191+
downloadMobileApk() {
192+
this.releaseService.getMobileApkDownloadUrl().subscribe({
193+
next: (downloadUrl) => {
194+
if (downloadUrl) {
195+
// Open the download URL in a new tab
196+
window.open(downloadUrl, '_blank');
197+
} else {
198+
this.errorHandler
199+
.handleError(
200+
new Error('Mobile APK download URL not found'),
201+
'Download Unavailable'
202+
)
203+
.then();
204+
}
205+
},
206+
error: (error) => {
207+
this.errorHandler
208+
.handleError(
209+
error,
210+
'Download Error'
211+
)
212+
.then();
213+
}
214+
});
215+
}
178216
}

web/src/app/components/theme/color-scheme-switcher.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { ThemeService } from '../../services/theme.service';
2828
fill="none"
2929
strokeLinecap="round"
3030
strokeLinejoin="round"
31-
class="text-gray-700"
31+
class="text-gray-700 w-5 h-5 mr-0"
3232
>
3333
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
3434
<path
@@ -49,7 +49,7 @@ import { ThemeService } from '../../services/theme.service';
4949
fill="none"
5050
strokeLinecap="round"
5151
strokeLinejoin="round"
52-
class="hover:bg-pastel-blue hover:text-white"
52+
class="hover:bg-pastel-blue hover:text-white w-5 h-5 mr-0"
5353
>
5454
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
5555
<path

web/src/app/pages/login.page.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { NgxTelegramWidgetComponent } from '../components/telegram/ngx-telegram-
88
import { HideNavGuard } from '../guards/nav.guard';
99
import { AuthService } from '../services/auth.service';
1010
import { ErrorHandlerService } from '../services/error-handler.service';
11+
import { ReleaseService } from '../services/release.service';
1112
import { TelegramSettingsService } from '../services/telegram-settings.service';
1213
import { TelegramService } from '../services/telegram.service';
1314

@@ -300,6 +301,13 @@ export const routeMeta: RouteMeta = {
300301
>
301302
Continue as Guest
302303
</button>
304+
<span class="text-gray-400 mx-2">|</span>
305+
<button
306+
(click)="downloadMobileApk()"
307+
class="text-gray-500 font-medium hover:text-gray-800 transition-colors"
308+
>
309+
Download Mobile App
310+
</button>
303311
</div>
304312
</div>
305313
</div>
@@ -311,6 +319,7 @@ export default class LoginPageComponent {
311319
private readonly telegramService = inject(TelegramService);
312320
private readonly telegramSettingsService = inject(TelegramSettingsService);
313321
private readonly errorHandlerService = inject(ErrorHandlerService);
322+
private readonly releaseService = inject(ReleaseService);
314323

315324
activeTab: 'signin' | 'signup' = 'signin'; // Default to sign in tab
316325

@@ -434,4 +443,25 @@ export default class LoginPageComponent {
434443
)
435444
.subscribe();
436445
}
446+
447+
downloadMobileApk() {
448+
this.releaseService.getMobileApkDownloadUrl().subscribe({
449+
next: downloadUrl => {
450+
if (downloadUrl) {
451+
// Open the download URL in a new tab
452+
window.open(downloadUrl, '_blank');
453+
} else {
454+
this.errorHandlerService
455+
.handleError(
456+
new Error('Mobile APK download URL not found'),
457+
'Download Unavailable'
458+
)
459+
.then();
460+
}
461+
},
462+
error: error => {
463+
this.errorHandlerService.handleError(error, 'Download Error').then();
464+
},
465+
});
466+
}
437467
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { inject, Injectable } from '@angular/core';
2+
import { map, Observable } from 'rxjs';
3+
import { injectTrpcClient } from '../trpc-client';
4+
5+
@Injectable({
6+
providedIn: 'root',
7+
})
8+
export class ReleaseService {
9+
private trpc = injectTrpcClient();
10+
11+
getMobileApkDownloadUrl(): Observable<string | null> {
12+
return this.trpc.releases.getMobileApkUrl
13+
.query()
14+
.pipe(map(result => result));
15+
}
16+
}

0 commit comments

Comments
 (0)