Skip to content

Commit 53b5668

Browse files
committed
feat(qr-code): add manual code entry option for device linking
- Added "Enter Code Manually" button to QR code page for manual input - Implemented startManualCodeEntry() to prompt user input via alert dialog - Created processManualCode() to handle manual code linking with loading state - Refactored linking logic into a reusable private link() method - Added keypadOutline icon and registered it for use in the component - Updated QR code parsing to use unified link() method for device linking - Improved error handling and loading state management for manual code entry - Enhanced web dashboard link-device page to show QR code with code text - Changed QR code data observable to include both code and QR URI string - Simplified QR code generation subscription by removing unnecessary mapping - Cleaned up imports by removing unused JsonPipe from dashboard page module
1 parent 3b5411b commit 53b5668

File tree

3 files changed

+157
-53
lines changed

3 files changed

+157
-53
lines changed

mobile/src/app/dashboard/dashboard.page.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AsyncPipe, JsonPipe } from '@angular/common';
1+
import { AsyncPipe } from '@angular/common';
22
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
33
import { RouterLink } from '@angular/router';
44
import {
@@ -124,7 +124,6 @@ import { TrpcPureHeaders } from '../trpc-pure-client';
124124
ExploreContainerComponent,
125125
AsyncPipe,
126126
NoSanitizePipe,
127-
JsonPipe,
128127
],
129128
})
130129
export class DashboardPage {

mobile/src/app/qr-code/qr-code.page.ts

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '@ionic/angular/standalone';
1818
import { BrowserQRCodeReader } from '@zxing/browser';
1919
import { addIcons } from 'ionicons';
20-
import { scanOutline } from 'ionicons/icons';
20+
import { keypadOutline, scanOutline } from 'ionicons/icons';
2121
import jsQR from 'jsqr';
2222
import {
2323
BehaviorSubject,
@@ -213,6 +213,15 @@ interface QrCodeData {
213213
<ion-icon name="scan-outline" slot="start"></ion-icon>
214214
Scan QR Code
215215
</ion-button>
216+
<br />
217+
<ion-button
218+
(click)="startManualCodeEntry()"
219+
fill="outline"
220+
style="margin-top: 10px;"
221+
>
222+
<ion-icon name="keypad-outline" slot="start"></ion-icon>
223+
Enter Code Manually
224+
</ion-button>
216225
}
217226
</div>
218227
</app-explore-container>
@@ -243,7 +252,7 @@ export class QrCodePage {
243252
isLoading$ = new BehaviorSubject<boolean>(false);
244253

245254
constructor() {
246-
addIcons({ scanOutline });
255+
addIcons({ scanOutline, keypadOutline });
247256
// Initialize the error handler with the toast controller
248257
this.errorHandler.initialize(this.toastController);
249258
}
@@ -270,26 +279,10 @@ export class QrCodePage {
270279
// Parse the QR code data
271280
try {
272281
const qrData: QrCodeData = JSON.parse(result);
273-
282+
const code = qrData.code;
274283
// Generate a unique device ID (in a real app, you might want to use a more robust method)
275-
const deviceId = this.generateDeviceId();
276-
277-
// Call the device/link API
278-
return from(
279-
this.deviceService.link({
280-
code: qrData.code,
281-
deviceId: deviceId,
282-
})
283-
).pipe(
284-
map(() => qrData), // Return the QR data on success
285-
catchError((err) => {
286-
console.error('Error linking device:', err);
287-
// Handle the error using our global error handler
288-
this.errorHandler
289-
.handleError(err, 'Failed to link device')
290-
.catch(console.error);
291-
throw new Error('link failed');
292-
})
284+
return this.link(code).pipe(
285+
map(() => qrData) // Return the QR data on success
293286
);
294287
} catch (parseError) {
295288
console.error('Error parsing QR code data:', parseError);
@@ -332,10 +325,99 @@ export class QrCodePage {
332325
.subscribe();
333326
}
334327

328+
private link(code: string) {
329+
const deviceId = this.generateDeviceId();
330+
331+
// Call the device/link API
332+
return from(
333+
this.deviceService.link({
334+
code,
335+
deviceId,
336+
})
337+
).pipe(
338+
catchError((err) => {
339+
console.error('Error linking device:', err);
340+
// Handle the error using our global error handler
341+
this.errorHandler
342+
.handleError(err, 'Failed to link device')
343+
.catch(console.error);
344+
throw new Error('link failed');
345+
})
346+
);
347+
}
348+
335349
resetScanner() {
336350
this.scanResultQrData$.next(null);
337351
}
338352

353+
async startManualCodeEntry() {
354+
const alert = await this.alertController.create({
355+
header: 'Enter Code',
356+
inputs: [
357+
{
358+
name: 'code',
359+
type: 'text',
360+
placeholder: 'Enter the code',
361+
value: '',
362+
},
363+
],
364+
buttons: [
365+
{
366+
text: 'Cancel',
367+
role: 'cancel',
368+
},
369+
{
370+
text: 'Send',
371+
handler: (data) => {
372+
if (data && data.code) {
373+
this.processManualCode(data.code);
374+
}
375+
},
376+
},
377+
],
378+
});
379+
380+
await alert.present();
381+
}
382+
383+
processManualCode(code: string) {
384+
// Set loading state to true when processing the manual code
385+
this.isLoading$.next(true);
386+
387+
// Generate a unique device ID
388+
const deviceId = this.generateDeviceId();
389+
390+
TrpcHeaders.set({});
391+
TrpcPureHeaders.set({});
392+
393+
// Call the device/link API
394+
this.link(code)
395+
.pipe(
396+
first(),
397+
tap((data: any) => {
398+
if (data && typeof data === 'object' && 'dashboardId' in data) {
399+
console.log('Device linked successfully:', data);
400+
this.scanResultQrData$.next(data);
401+
// Navigate to the dashboard tab after successful linking
402+
this.router.navigate(['/tabs/dashboard']);
403+
}
404+
}),
405+
catchError((error) => {
406+
console.error('Error scanning barcode:', error);
407+
// Handle the error using our global error handler
408+
this.errorHandler
409+
.handleError(error, 'Error scanning QR code')
410+
.catch(console.error);
411+
return of(null);
412+
}),
413+
// Always set loading state to false when the operation completes
414+
finalize(() => {
415+
this.isLoading$.next(false);
416+
})
417+
)
418+
.subscribe();
419+
}
420+
339421
private generateDeviceId(): string {
340422
// Check if we already have a device ID in local storage
341423
const storedDeviceId = localStorage.getItem('deviceId');

web/src/app/pages/dashboards/[dashboardId]/link-device.page.ts

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ export const routeMeta: RouteMeta = {
1818
selector: 'dashboards-link-device-page',
1919
standalone: true,
2020
changeDetection: ChangeDetectionStrategy.OnPush,
21-
imports: [FormlyBootstrapModule, AsyncPipe, ReactiveFormsModule, LucideAngularModule],
21+
imports: [
22+
FormlyBootstrapModule,
23+
AsyncPipe,
24+
ReactiveFormsModule,
25+
LucideAngularModule,
26+
],
2227
template: `
2328
@if (dashboard$ | async; as dashboard) {
2429
<h1 class="text-4xl font-extrabold text-gray-800 mb-2">
@@ -31,7 +36,9 @@ export const routeMeta: RouteMeta = {
3136
class="text-gray-500 hover:text-pastel-blue transition-colors mb-10 mt-2 flex items-center"
3237
>
3338
<i-lucide name="arrow-left" class="w-6 h-6 mr-0 lg:mr-2"></i-lucide>
34-
<span class="hidden lg:inline text-lg font-medium">Back to Dashboard</span>
39+
<span class="hidden lg:inline text-lg font-medium"
40+
>Back to Dashboard</span
41+
>
3542
</a>
3643
Scan the QR code with your mobile device to link it to this dashboard.
3744
</p>
@@ -40,14 +47,25 @@ export const routeMeta: RouteMeta = {
4047
<div class="bg-white p-6 rounded-2xl long-shadow mb-8 space-y-4">
4148
<div class="flex flex-col items-center justify-center py-8">
4249
@if (qrForLinkDevice$ | async; as qrForLinkDevice) {
43-
<div class="mb-6 p-4 bg-white rounded-xl border border-gray-200 inline-block">
44-
<img src="{{ qrForLinkDevice }}" alt="QR Code" class="w-64 h-64" />
50+
<div
51+
class="mb-6 p-4 bg-white rounded-xl border border-gray-200 inline-block"
52+
>
53+
<img
54+
src="{{ qrForLinkDevice.qr }}"
55+
alt="QR Code"
56+
class="w-64 h-64"
57+
/>
58+
59+
<div class="text-center mt-4">
60+
Code: {{ qrForLinkDevice.code }}
61+
</div>
4562
</div>
4663
<p class="text-gray-600 text-center max-w-md mb-8">
47-
Open the mobile app and scan this QR code to link your device to the
64+
Open the mobile app and scan this QR code to link your device to
65+
the
4866
<span class="font-bold">{{ dashboard.name }}</span> dashboard.
4967
</p>
50-
68+
5169
<!-- Regenerate QR Code Button -->
5270
<button
5371
(click)="regenerateQrCode()"
@@ -67,7 +85,10 @@ export default class DashboardsLinkDevicePageComponent {
6785
private readonly route = inject(ActivatedRoute);
6886

6987
// Use BehaviorSubject to allow manual QR code regeneration
70-
private qrCodeSubject = new BehaviorSubject<string | null>(null);
88+
private qrCodeSubject = new BehaviorSubject<{
89+
code: string;
90+
qr: string;
91+
} | null>(null);
7192
readonly qrForLinkDevice$ = this.qrCodeSubject.asObservable();
7293

7394
readonly dashboard$ = this.route.paramMap.pipe(
@@ -84,30 +105,32 @@ export default class DashboardsLinkDevicePageComponent {
84105
}
85106

86107
private loadInitialQrCode() {
87-
this.route.paramMap.pipe(
88-
switchMap(params =>
89-
params.get('dashboardId') !== null
90-
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
91-
this.dashboardsService.generateQrCode(params.get('dashboardId')!)
92-
: of(null)
93-
),
94-
map(result => result?.qr)
95-
).subscribe(qrCode => {
96-
this.qrCodeSubject.next(qrCode || null);
97-
});
108+
this.route.paramMap
109+
.pipe(
110+
switchMap(params =>
111+
params.get('dashboardId') !== null
112+
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
113+
this.dashboardsService.generateQrCode(params.get('dashboardId')!)
114+
: of(null)
115+
)
116+
)
117+
.subscribe(result => {
118+
this.qrCodeSubject.next(result || null);
119+
});
98120
}
99121

100122
regenerateQrCode() {
101-
this.route.paramMap.pipe(
102-
switchMap(params =>
103-
params.get('dashboardId') !== null
104-
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
105-
this.dashboardsService.generateQrCode(params.get('dashboardId')!)
106-
: of(null)
107-
),
108-
map(result => result?.qr)
109-
).subscribe(qrCode => {
110-
this.qrCodeSubject.next(qrCode || null);
111-
});
123+
this.route.paramMap
124+
.pipe(
125+
switchMap(params =>
126+
params.get('dashboardId') !== null
127+
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
128+
this.dashboardsService.generateQrCode(params.get('dashboardId')!)
129+
: of(null)
130+
)
131+
)
132+
.subscribe(result => {
133+
this.qrCodeSubject.next(result || null);
134+
});
112135
}
113-
}
136+
}

0 commit comments

Comments
 (0)