diff --git a/backend/nest-api/prisma/schema.prisma b/backend/nest-api/prisma/schema.prisma index a522b77..5955339 100644 --- a/backend/nest-api/prisma/schema.prisma +++ b/backend/nest-api/prisma/schema.prisma @@ -18,6 +18,7 @@ enum Fine_Status { PENDING PAID OVERDUE + COURT_CASE } enum Payment_Status { @@ -35,11 +36,13 @@ model QR_Scan_History { scan_Id String @id @default(uuid()) qr_Token String scan_Time DateTime @default(now()) + traffic_Officer_Id String traffic_Officer_Name String driver_Name String location String? license_Id String license Driving_License @relation(fields: [license_Id], references: [license_Id]) + officer Traffic_Officer @relation(fields: [traffic_Officer_Id], references: [traffic_Officer_Id]) } model DMT_Admin { @@ -77,11 +80,12 @@ model Driving_License { nic_No String @unique status License_Status @default(ACTIVE) image String? - points Int @default(24) + points Int @default(0) + suspended_Until DateTime? address String date_of_birth DateTime blood_Group String - issue_Date DateTime + issue_Date DateTime user_Id String @unique user User @relation(fields: [user_Id], references: [user_Id]) dmt_Admin_Id String @@ -94,10 +98,10 @@ model Driving_License { model License_Vehicle_Category { category_Id String @id @default(uuid()) - vehicle_Class String + vehicle_Class String issue_Date DateTime expiry_Date DateTime - restriction String? + restriction String? license_Id String license Driving_License @relation(fields: [license_Id], references: [license_Id], onDelete: Cascade) } @@ -108,6 +112,8 @@ model Temporary_License { expiry_Date DateTime license_Id String license Driving_License @relation(fields: [license_Id], references: [license_Id]) + issued_By String + officer Traffic_Officer @relation(fields: [issued_By], references: [traffic_Officer_Id]) } model Division { @@ -134,18 +140,20 @@ model Divisional_Head { } model Traffic_Officer { - traffic_Officer_Id String @id @default(uuid()) - badge_No String @unique - email String @unique + traffic_Officer_Id String @id @default(uuid()) + badge_No String @unique + email String @unique name String - role Officer_Role @default(TRAFFIC_OFFICER) + role Officer_Role @default(TRAFFIC_OFFICER) password String reset_Otp String? reset_Otp_Expires_At DateTime? divisional_Head_Id String - divisionalHead Divisional_Head @relation(fields: [divisional_Head_Id], references: [divisional_Head_Id]) + divisionalHead Divisional_Head @relation(fields: [divisional_Head_Id], references: [divisional_Head_Id]) shifts Shift[] fines Fine[] + qrScans QR_Scan_History[] + temporaryLicenses Temporary_License[] } model Shift { @@ -191,6 +199,7 @@ model Fine_Offence { offense_Id String fine Fine @relation(fields: [fine_Id], references: [fine_Id]) offenceCategory Offence_Category @relation(fields: [offense_Id], references: [offense_Id]) + @@id([fine_Id, offense_Id]) } diff --git a/backend/nest-api/src/auth/auth.controller.ts b/backend/nest-api/src/auth/auth.controller.ts index 52843c6..e1794ab 100644 --- a/backend/nest-api/src/auth/auth.controller.ts +++ b/backend/nest-api/src/auth/auth.controller.ts @@ -257,7 +257,6 @@ export class OfficerResetPasswordDto { export interface AuthRequest { user: { id: string; - sub: string; role: string; }; } @@ -393,7 +392,7 @@ export class AuthController { @Body() changePasswordDto: ChangePasswordDto, ) { return this.authService.changePassword( - req.user.sub, + req.user.id, req.user.role, changePasswordDto, ); @@ -407,6 +406,6 @@ export class AuthController { @Request() req: AuthRequest, @Body() changePasswordDto: ChangePasswordDto, ) { - return this.authService.changeUserPassword(req.user.sub, changePasswordDto); + return this.authService.changeUserPassword(req.user.id, changePasswordDto); } } diff --git a/backend/nest-api/src/auth/auth.service.ts b/backend/nest-api/src/auth/auth.service.ts index 81fa069..6693a35 100644 --- a/backend/nest-api/src/auth/auth.service.ts +++ b/backend/nest-api/src/auth/auth.service.ts @@ -399,9 +399,12 @@ export class AuthService { throw new UnauthorizedException('Invalid NIC or password.'); if (user.device_Id !== deviceId) { - throw new UnauthorizedException( - 'Access Denied: You can only log in from your registered device.', - ); + // Return specific error structure for new device matching + throw new ForbiddenException({ + code: 'DEVICE_MISMATCH', + message: 'New device detected. OTP verification required.', + phone: user.mobile_Phone_No, + }); } return this.generateUserToken(user); diff --git a/backend/nest-api/src/fines/fines.controller.ts b/backend/nest-api/src/fines/fines.controller.ts index bc030bf..c2ee539 100644 --- a/backend/nest-api/src/fines/fines.controller.ts +++ b/backend/nest-api/src/fines/fines.controller.ts @@ -33,16 +33,30 @@ export class FinesController { @Post() issueFine( @Request() req: AuthRequest, - @Body() body: { licenseId: string; offenseIds: string[]; comment?: string }, + @Body() body: { scanToken: string; offenseIds: string[]; comment?: string }, ) { return this.finesService.issueFine({ - licenseId: body.licenseId, + scanToken: body.scanToken, officerId: req.user.id, offenseIds: body.offenseIds, comment: body.comment, }); } + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('TRAFFIC_OFFICER') + @Get('officer-stats') + getTrafficOfficerStats(@Request() req: AuthRequest) { + return this.finesService.getTrafficOfficerStats(req.user.id); + } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('TRAFFIC_OFFICER') + @Get('officer-fines') + async getOfficerFines(@Request() req: AuthRequest) { + return this.finesService.getOfficerFines(req.user.id); + } + @UseGuards(JwtAuthGuard) @Get('my-fines') getMyFines(@Request() req: AuthRequest) { diff --git a/backend/nest-api/src/fines/fines.module.ts b/backend/nest-api/src/fines/fines.module.ts index d276322..acb3712 100644 --- a/backend/nest-api/src/fines/fines.module.ts +++ b/backend/nest-api/src/fines/fines.module.ts @@ -2,9 +2,15 @@ import { Module } from '@nestjs/common'; import { FinesService } from './fines.service'; import { FinesController } from './fines.controller'; import { PrismaModule } from '../prisma/prisma.module'; +import { JwtModule } from '@nestjs/jwt'; @Module({ - imports: [PrismaModule], + imports: [ + PrismaModule, + JwtModule.register({ + secret: process.env.JWT_SECRET || 'defaultSecretKey', + }), + ], controllers: [FinesController], providers: [FinesService], exports: [FinesService], diff --git a/backend/nest-api/src/fines/fines.service.ts b/backend/nest-api/src/fines/fines.service.ts index c9f9aae..189bae7 100644 --- a/backend/nest-api/src/fines/fines.service.ts +++ b/backend/nest-api/src/fines/fines.service.ts @@ -2,8 +2,11 @@ import { Injectable, NotFoundException, BadRequestException, + UnauthorizedException, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; +import { JwtService } from '@nestjs/jwt'; +import { Fine, Payment } from '@prisma/client'; export interface CreateOffenseData { code: string; @@ -20,43 +23,146 @@ export interface UpdateOffenseData { isCourtCase?: boolean; } +type FineWithPayment = Fine & { payment: Payment | null }; + +interface ScanTokenPayload { + licenseId: string; + iat?: number; + exp?: number; +} + @Injectable() export class FinesService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + private jwtService: JwtService, + ) {} + + private async autoActivateLicenses() { + await this.prisma.driving_License.updateMany({ + where: { status: 'SUSPENDED', suspended_Until: { lte: new Date() } }, + data: { status: 'ACTIVE', suspended_Until: null }, + }); + + const licensesToActivate = await this.prisma.driving_License.findMany({ + where: { + status: 'SUSPENDED', + suspended_Until: null, + fines: { + none: { status: { in: ['PENDING', 'OVERDUE', 'COURT_CASE'] } }, + }, + }, + }); + + if (licensesToActivate.length > 0) { + await this.prisma.driving_License.updateMany({ + where: { + license_Id: { in: licensesToActivate.map((l) => l.license_Id) }, + }, + data: { status: 'ACTIVE' }, + }); + } + } + + private async processOverdueFines() { + const overdueFines = await this.prisma.fine.findMany({ + where: { status: 'PENDING', due_Date: { lt: new Date() } }, + }); + + for (const fine of overdueFines) { + await this.prisma.fine.update({ + where: { fine_Id: fine.fine_Id }, + data: { status: 'OVERDUE' }, + }); + + await this.prisma.temporary_License.deleteMany({ + where: { license_Id: fine.license_Id }, + }); + } + } async issueFine(data: { - licenseId: string; + scanToken: string; officerId: string; offenseIds: string[]; comment?: string; }) { + let licenseId = ''; + try { + const payload = this.jwtService.verify(data.scanToken); + licenseId = payload.licenseId; + } catch { + throw new UnauthorizedException( + 'Scan session expired. Please scan QR again.', + ); + } + + await this.autoActivateLicenses(); + await this.processOverdueFines(); + const officer = await this.prisma.traffic_Officer.findUnique({ where: { traffic_Officer_Id: data.officerId }, }); - if (!officer) throw new NotFoundException('Officer not found'); + if (!officer) throw new NotFoundException(); const license = await this.prisma.driving_License.findUnique({ - where: { license_Id: data.licenseId }, + where: { license_Id: licenseId }, }); - if (!license) throw new NotFoundException('License not found'); + if (!license) throw new NotFoundException(); const offenses = await this.prisma.offence_Category.findMany({ where: { offense_Id: { in: data.offenseIds } }, }); - if (offenses.length === 0) - throw new BadRequestException('Invalid offenses'); + if (offenses.length === 0) throw new BadRequestException(); const isCourtCase = offenses.some((o) => o.is_Court_Case); const fineDueDate = new Date(); fineDueDate.setDate(fineDueDate.getDate() + 14); + const oldPoints = license.points; + const totalPointsAdded = offenses.reduce( + (sum, off) => sum + off.points_Value, + 0, + ); + const newPoints = oldPoints + totalPointsAdded; + + let newStatus = license.status as 'ACTIVE' | 'SUSPENDED' | 'REVOKED'; + if (newStatus !== 'REVOKED' && newStatus !== 'SUSPENDED') { + newStatus = 'ACTIVE'; + } + + let suspendedUntil = license.suspended_Until; + let fineStatus: 'PENDING' | 'COURT_CASE' = 'PENDING'; + let isPointSuspension = false; + + const now = new Date(); + + if (newPoints >= 100 && oldPoints < 100) { + newStatus = 'REVOKED'; + suspendedUntil = null; + isPointSuspension = true; + } else if (newPoints >= 50 && oldPoints < 50) { + newStatus = 'SUSPENDED'; + suspendedUntil = new Date(now.setMonth(now.getMonth() + 6)); + isPointSuspension = true; + } else if (newPoints >= 24 && oldPoints < 24) { + newStatus = 'SUSPENDED'; + suspendedUntil = new Date(now.setMonth(now.getMonth() + 1)); + isPointSuspension = true; + } else if (isCourtCase) { + newStatus = 'SUSPENDED'; + fineStatus = 'COURT_CASE'; + } else if (oldPoints >= 100 || license.status === 'REVOKED') { + newStatus = 'REVOKED'; + } + return this.prisma.$transaction(async (tx) => { const fine = await tx.fine.create({ data: { - license_Id: data.licenseId, + license_Id: licenseId, traffic_Officer_Id: data.officerId, due_Date: fineDueDate, - status: 'PENDING', + status: fineStatus, comment: data.comment || null, }, }); @@ -68,19 +174,28 @@ export class FinesService { } await tx.driving_License.update({ - where: { license_Id: data.licenseId }, + where: { license_Id: licenseId }, data: { - points: { - decrement: offenses.reduce((sum, off) => sum + off.points_Value, 0), - }, - status: 'SUSPENDED', + points: newPoints, + status: newStatus, + suspended_Until: suspendedUntil, }, }); - if (!isCourtCase) { - await tx.temporary_License.create({ - data: { license_Id: data.licenseId, expiry_Date: fineDueDate }, + if (!isPointSuspension && !isCourtCase && newStatus !== 'REVOKED') { + const existingTemp = await tx.temporary_License.findFirst({ + where: { license_Id: licenseId }, }); + + if (!existingTemp) { + await tx.temporary_License.create({ + data: { + license_Id: licenseId, + expiry_Date: fineDueDate, + issued_By: data.officerId, + }, + }); + } } return fine; @@ -88,10 +203,13 @@ export class FinesService { } async getMyFines(userId: string) { + await this.autoActivateLicenses(); + await this.processOverdueFines(); + const license = await this.prisma.driving_License.findUnique({ where: { user_Id: userId }, }); - if (!license) throw new NotFoundException('License not found'); + if (!license) throw new NotFoundException(); return this.prisma.fine.findMany({ where: { license_Id: license.license_Id }, @@ -107,36 +225,62 @@ export class FinesService { async payFine(fineId: string, amount: number) { const fine = await this.prisma.fine.findUnique({ where: { fine_Id: fineId }, + include: { license: true }, }); - if (!fine) throw new NotFoundException('Fine not found'); - if (fine.status === 'PAID') - throw new BadRequestException('Fine is already paid'); - if (fine.status === 'OVERDUE') - throw new BadRequestException( - 'Fine is overdue (Court Case). You cannot pay via the app.', - ); + if (!fine) throw new NotFoundException(); + if (fine.status === 'PAID') throw new BadRequestException(); + + const now = new Date(); + const isOverdue = fine.due_Date ? now > fine.due_Date : false; return this.prisma.$transaction(async (tx) => { const payment = await tx.payment.create({ data: { fine_Id: fineId, amount: amount, status: 'COMPLETED' }, }); + const newFineStatus = + isOverdue || fine.status === 'COURT_CASE' ? fine.status : 'PAID'; + const updatedFine = await tx.fine.update({ where: { fine_Id: fineId }, - data: { status: 'PAID' }, + data: { status: newFineStatus }, }); - await tx.driving_License.update({ - where: { license_Id: fine.license_Id }, - data: { status: 'ACTIVE' }, - }); + if (!isOverdue && fine.status !== 'COURT_CASE') { + const pendingCount = await tx.fine.count({ + where: { + license_Id: fine.license_Id, + status: { in: ['PENDING', 'OVERDUE', 'COURT_CASE'] }, + fine_Id: { not: fineId }, + }, + }); - await tx.temporary_License.deleteMany({ - where: { license_Id: fine.license_Id }, - }); + const currentLicense = await tx.driving_License.findUnique({ + where: { license_Id: fine.license_Id }, + }); + + const isPointsSuspended = currentLicense?.suspended_Until != null; + + if ( + pendingCount === 0 && + currentLicense?.status !== 'REVOKED' && + !isPointsSuspended + ) { + await tx.temporary_License.deleteMany({ + where: { license_Id: fine.license_Id }, + }); + await tx.driving_License.update({ + where: { license_Id: fine.license_Id }, + data: { status: 'ACTIVE' }, + }); + } + } return { - message: `Payment successful.`, + message: + isOverdue || fine.status === 'COURT_CASE' + ? 'Payment recorded. Waiting for Divisional Head approval.' + : 'Payment successful. License activated.', paymentId: payment.payment_Id, fineStatus: updatedFine.status, }; @@ -146,49 +290,80 @@ export class FinesService { async payBulkFines(fineIds: string[], totalAmount: number) { const fines = await this.prisma.fine.findMany({ where: { fine_Id: { in: fineIds } }, + include: { license: true }, }); - if (fines.length !== fineIds.length) - throw new BadRequestException('Some fines not found'); + if (fines.length !== fineIds.length) throw new BadRequestException(); for (const fine of fines) { - if (fine.status === 'PAID') - throw new BadRequestException(`Fine ${fine.fine_Id} already paid`); - if (fine.status === 'OVERDUE') - throw new BadRequestException(`Fine ${fine.fine_Id} is overdue`); + if (fine.status === 'PAID') throw new BadRequestException(); } const licenseId = fines[0].license_Id; return this.prisma.$transaction(async (tx) => { const payments: { payment_Id: string }[] = []; + let hasOverdueOrCourt = false; + + for (const fine of fines) { + const now = new Date(); + const isOverdue = fine.due_Date ? now > fine.due_Date : false; + + if (isOverdue || fine.status === 'COURT_CASE') { + hasOverdueOrCourt = true; + } - for (const fineId of fineIds) { const p = await tx.payment.create({ data: { - fine_Id: fineId, + fine_Id: fine.fine_Id, amount: totalAmount / fineIds.length, status: 'COMPLETED', }, }); payments.push(p); + const newFineStatus = + isOverdue || fine.status === 'COURT_CASE' ? fine.status : 'PAID'; + await tx.fine.update({ - where: { fine_Id: fineId }, - data: { status: 'PAID' }, + where: { fine_Id: fine.fine_Id }, + data: { status: newFineStatus }, }); } - await tx.driving_License.update({ - where: { license_Id: licenseId }, - data: { status: 'ACTIVE' }, - }); + if (!hasOverdueOrCourt) { + const pendingCount = await tx.fine.count({ + where: { + license_Id: licenseId, + status: { in: ['PENDING', 'OVERDUE', 'COURT_CASE'] }, + fine_Id: { notIn: fineIds }, + }, + }); - await tx.temporary_License.deleteMany({ - where: { license_Id: licenseId }, - }); + const currentLicense = await tx.driving_License.findUnique({ + where: { license_Id: licenseId }, + }); + + const isPointsSuspended = currentLicense?.suspended_Until != null; + + if ( + pendingCount === 0 && + currentLicense?.status !== 'REVOKED' && + !isPointsSuspended + ) { + await tx.temporary_License.deleteMany({ + where: { license_Id: licenseId }, + }); + await tx.driving_License.update({ + where: { license_Id: licenseId }, + data: { status: 'ACTIVE' }, + }); + } + } return { - message: 'Bulk payment successful', + message: hasOverdueOrCourt + ? 'Payment recorded. Waiting for Divisional Head approval.' + : 'Bulk payment successful.', payments: payments.map((p) => p.payment_Id), }; }); @@ -197,8 +372,9 @@ export class FinesService { async updateCourtCase(fineId: string, verdict: 'ACTIVE' | 'REVOKED') { const fine = await this.prisma.fine.findUnique({ where: { fine_Id: fineId }, + include: { license: true }, }); - if (!fine) throw new NotFoundException('Fine not found'); + if (!fine) throw new NotFoundException(); return this.prisma.$transaction(async (tx) => { await tx.fine.update({ @@ -208,7 +384,11 @@ export class FinesService { return tx.driving_License.update({ where: { license_Id: fine.license_Id }, - data: { status: verdict, points: verdict === 'ACTIVE' ? 24 : 0 }, + data: { + status: verdict, + points: verdict === 'ACTIVE' ? 0 : fine.license.points, + suspended_Until: null, + }, }); }); } @@ -246,7 +426,7 @@ export class FinesService { const offense = await this.prisma.offence_Category.findUnique({ where: { offense_Id: id }, }); - if (!offense) throw new NotFoundException('Offense not found'); + if (!offense) throw new NotFoundException(); return this.prisma.offence_Category.update({ where: { offense_Id: id }, @@ -255,6 +435,7 @@ export class FinesService { } async getAllFinesForDMT() { + await this.autoActivateLicenses(); return this.prisma.fine.findMany({ include: { license: { @@ -269,15 +450,19 @@ export class FinesService { } async getProblematicLicensesForDMT() { + await this.autoActivateLicenses(); return this.prisma.driving_License.findMany({ where: { status: { in: ['SUSPENDED', 'REVOKED'] }, }, - orderBy: { points: 'asc' }, + orderBy: { points: 'desc' }, }); } async getCourtCasesByDH(headId: string) { + await this.autoActivateLicenses(); + await this.processOverdueFines(); + const officers = await this.prisma.traffic_Officer.findMany({ where: { divisional_Head_Id: headId }, select: { traffic_Officer_Id: true }, @@ -285,19 +470,26 @@ export class FinesService { const officerIds = officers.map((o) => o.traffic_Officer_Id); return this.prisma.fine.findMany({ - where: { status: 'OVERDUE', traffic_Officer_Id: { in: officerIds } }, + where: { + status: { in: ['OVERDUE', 'COURT_CASE'] }, + traffic_Officer_Id: { in: officerIds }, + }, include: { license: { select: { license_No: true, full_Name: true, nic_No: true }, }, trafficOfficer: { select: { name: true, badge_No: true } }, offenses: { include: { offenceCategory: true } }, + payment: true, }, orderBy: { issue_At: 'desc' }, }); } async getDashboardStats(headId: string) { + await this.autoActivateLicenses(); + await this.processOverdueFines(); + const now = new Date(); const officers = await this.prisma.traffic_Officer.findMany({ where: { divisional_Head_Id: headId }, @@ -313,20 +505,111 @@ export class FinesService { }); const officerIds = officers.map((o) => o.traffic_Officer_Id); + const fines = await this.prisma.fine.findMany({ where: { traffic_Officer_Id: { in: officerIds } }, include: { payment: true }, }); + const startOfDay = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + const dailyFines = fines.filter((f) => new Date(f.issue_At) >= startOfDay); + const monthlyFines = fines.filter( + (f) => new Date(f.issue_At) >= startOfMonth, + ); + + const calcRevenue = (fineArray: FineWithPayment[]) => + fineArray + .filter((f) => f.status === 'PAID' && f.payment) + .reduce((sum, f) => sum + (f.payment?.amount || 0), 0); + return { totalOfficers: officers.length, activeOfficersOnDuty: officers.filter((o) => o.shifts.length > 0).length, totalFinesIssued: fines.length, pendingFinesCount: fines.filter((f) => f.status === 'PENDING').length, - overdueCourtCases: fines.filter((f) => f.status === 'OVERDUE').length, - totalRevenue: fines - .filter((f) => f.status === 'PAID' && f.payment) - .reduce((sum, f) => sum + (f.payment?.amount || 0), 0), + overdueCourtCases: fines.filter( + (f) => f.status === 'OVERDUE' || f.status === 'COURT_CASE', + ).length, + totalRevenue: calcRevenue(fines), + daily: { count: dailyFines.length, revenue: calcRevenue(dailyFines) }, + monthly: { + count: monthlyFines.length, + revenue: calcRevenue(monthlyFines), + }, }; } + + async getTrafficOfficerStats(officerId: string) { + const now = new Date(); + const startOfDay = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + const fines = await this.prisma.fine.findMany({ + where: { traffic_Officer_Id: officerId }, + include: { payment: true }, + }); + + const qrScans = await this.prisma.qR_Scan_History.count({ + where: { traffic_Officer_Id: officerId }, + }); + + const tempLicenses = await this.prisma.temporary_License.count({ + where: { issued_By: officerId }, + }); + + const stats = { + daily: { count: 0, revenue: 0 }, + monthly: { count: 0, revenue: 0 }, + allTime: { + count: 0, + revenue: 0, + qrScans: qrScans, + tempLicenses: tempLicenses, + }, + }; + + fines.forEach((f) => { + const issueDate = new Date(f.issue_At); + const isPaid = f.status === 'PAID' && f.payment; + const amount = isPaid ? f.payment.amount : 0; + + stats.allTime.count++; + stats.allTime.revenue += amount; + + if (issueDate >= startOfMonth) { + stats.monthly.count++; + stats.monthly.revenue += amount; + } + if (issueDate >= startOfDay) { + stats.daily.count++; + stats.daily.revenue += amount; + } + }); + + return stats; + } + + async getOfficerFines(officerId: string) { + return this.prisma.fine.findMany({ + where: { traffic_Officer_Id: officerId }, + include: { + license: { + select: { license_No: true, full_Name: true, nic_No: true }, + }, + offenses: { include: { offenceCategory: true } }, + payment: true, + }, + orderBy: { issue_At: 'desc' }, + }); + } } diff --git a/backend/nest-api/src/license/license.controller.ts b/backend/nest-api/src/license/license.controller.ts index 79b8fd0..c1ababb 100644 --- a/backend/nest-api/src/license/license.controller.ts +++ b/backend/nest-api/src/license/license.controller.ts @@ -103,7 +103,7 @@ export class CreateLicenseDto { } export class ScanQRDto { - @ApiProperty({ example: 'License_ID:RandomHash:Timestamp' }) + @ApiProperty({ example: 'License_ID:RandomHash' }) @IsString() @IsNotEmpty() qrToken: string; @@ -166,12 +166,19 @@ export class LicenseController { return this.licenseService.getMyLicense(req.user.id); } - @ApiOperation({ summary: 'Generate 3-Minute QR Code for License' }) + @ApiOperation({ summary: 'Generate QR Code for License' }) @Get('generate-qr') async generateQR(@Request() req: AuthRequest) { return this.licenseService.generateLicenseQR(req.user.id); } + @ApiOperation({ summary: 'Check if QR code has been scanned' }) + @Get('check-scan-status') + async checkScanStatus(@Query('qrToken') qrToken: string) { + return this.licenseService.checkScanStatus(qrToken); + } + + @Roles('TRAFFIC_OFFICER') @ApiOperation({ summary: 'Scan License QR Code' }) @Post('scan-qr') async scanQR(@Request() req: AuthRequest, @Body() data: ScanQRDto) { @@ -182,12 +189,14 @@ export class LicenseController { ); } + @Roles('DMT_ADMIN') @ApiOperation({ summary: 'Update License Status' }) @Patch(':id/status') async updateStatus(@Param('id') id: string, @Body() data: UpdateStatusDto) { return this.licenseService.updateStatus(id, data.status); } + @Roles('DMT_ADMIN', 'POLICE_ADMIN') @ApiOperation({ summary: 'Get License by NIC' }) @Get('search/:nic') async getLicenseByNIC(@Param('nic') nic: string) { diff --git a/backend/nest-api/src/license/license.service.ts b/backend/nest-api/src/license/license.service.ts index c533860..f128546 100644 --- a/backend/nest-api/src/license/license.service.ts +++ b/backend/nest-api/src/license/license.service.ts @@ -9,6 +9,7 @@ import { ConfigService } from '@nestjs/config'; import * as crypto from 'crypto'; import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { JwtService } from '@nestjs/jwt'; export interface VehicleCategoryData { vehicleClass: string; @@ -47,6 +48,7 @@ export class LicenseService { constructor( private prisma: PrismaService, private configService: ConfigService, + private jwtService: JwtService, ) { const region = this.configService.get('AWS_REGION') || 'ap-southeast-1'; @@ -64,6 +66,32 @@ export class LicenseService { }); } + private async autoActivateLicenses() { + await this.prisma.driving_License.updateMany({ + where: { status: 'SUSPENDED', suspended_Until: { lte: new Date() } }, + data: { suspended_Until: null }, + }); + + const licensesToActivate = await this.prisma.driving_License.findMany({ + where: { + status: 'SUSPENDED', + suspended_Until: null, + fines: { + none: { status: { in: ['PENDING', 'OVERDUE', 'COURT_CASE'] } }, + }, + }, + }); + + if (licensesToActivate.length > 0) { + await this.prisma.driving_License.updateMany({ + where: { + license_Id: { in: licensesToActivate.map((l) => l.license_Id) }, + }, + data: { status: 'ACTIVE', points: 0 }, + }); + } + } + async getS3UploadUrl(fileName: string, fileType: string) { const bucketName = this.configService.get('AWS_S3_BUCKET_NAME') || @@ -134,31 +162,37 @@ export class LicenseService { } async getMyLicense(userId: string) { + await this.autoActivateLicenses(); const license = await this.prisma.driving_License.findUnique({ where: { user_Id: userId }, include: { - fines: { where: { status: 'PENDING' } }, + fines: { + where: { status: { in: ['PENDING', 'OVERDUE', 'COURT_CASE'] } }, + }, temporaryLicenses: true, vehicleCategories: true, }, }); - if (!license) throw new NotFoundException('License not found'); + if (!license) throw new NotFoundException(); return license; } async generateLicenseQR(userId: string) { + await this.autoActivateLicenses(); const license = await this.prisma.driving_License.findUnique({ where: { user_Id: userId }, include: { temporaryLicenses: true, - fines: { where: { status: 'PENDING' } }, + fines: { + where: { status: { in: ['PENDING', 'OVERDUE', 'COURT_CASE'] } }, + }, }, }); - if (!license) throw new NotFoundException('Active license not found'); + if (!license) throw new NotFoundException(); if (license.status === 'REVOKED' || license.status === 'EXPIRED') { - throw new BadRequestException(`License is ${license.status}`); + throw new BadRequestException(); } if (license.status === 'SUSPENDED') { @@ -173,22 +207,30 @@ export class LicenseService { data: { status: 'OVERDUE' }, }); } - throw new BadRequestException( - 'Temporary license has expired. Auto-escalated to Court Case. QR Code generation blocked.', - ); + await this.prisma.temporary_License.deleteMany({ + where: { license_Id: license.license_Id }, + }); + throw new BadRequestException(); } } else { - throw new BadRequestException( - 'License is suspended. QR Code generation blocked.', - ); + throw new BadRequestException(); } } - const expiryTime = Date.now() + 3 * 60 * 1000; const randomToken = crypto.randomBytes(16).toString('hex'); - const qrToken = `${license.license_Id}:${randomToken}:${expiryTime}`; + const qrToken = `${license.license_Id}:${randomToken}`; + + return { qrToken }; + } - return { qrToken, expiresAt: new Date(expiryTime) }; + async checkScanStatus(qrToken: string) { + if (!qrToken) throw new BadRequestException(); + + const scan = await this.prisma.qR_Scan_History.findFirst({ + where: { qr_Token: qrToken }, + }); + + return { scanned: !!scan }; } async scanLicenseQR( @@ -197,33 +239,28 @@ export class LicenseService { location?: string, ) { const parts = qrToken.split(':'); - if (parts.length !== 3) throw new BadRequestException('Invalid QR format'); + if (parts.length !== 2) throw new BadRequestException(); const licenseId = parts[0]; - const expiryTime = parseInt(parts[2], 10); - - if (Date.now() > expiryTime) { - throw new BadRequestException( - 'QR Code has expired. Please ask driver to refresh.', - ); - } + await this.autoActivateLicenses(); const license = await this.prisma.driving_License.findUnique({ where: { license_Id: licenseId }, include: { user: true }, }); - if (!license) throw new NotFoundException('License not found'); + if (!license) throw new NotFoundException(); const officer = await this.prisma.traffic_Officer.findUnique({ where: { traffic_Officer_Id: trafficOfficerId }, }); - if (!officer) throw new UnauthorizedException('Invalid Officer'); + if (!officer) throw new UnauthorizedException(); await this.prisma.qR_Scan_History.create({ data: { qr_Token: qrToken, + traffic_Officer_Id: trafficOfficerId, traffic_Officer_Name: officer.name, driver_Name: license.user.name, location: location || null, @@ -231,12 +268,17 @@ export class LicenseService { }, }); + const scanToken = this.jwtService.sign( + { licenseId: license.license_Id }, + { expiresIn: '3m' }, + ); + return { - message: 'Scan successful', driverName: license.user.name, licenseNo: license.license_No, status: license.status, points: license.points, + scanToken: scanToken, }; } @@ -251,6 +293,7 @@ export class LicenseService { } async getLicenseByNIC(nicNo: string) { + await this.autoActivateLicenses(); const license = await this.prisma.driving_License.findUnique({ where: { nic_No: nicNo }, include: { @@ -265,11 +308,12 @@ export class LicenseService { }, }); - if (!license) throw new NotFoundException('License not found for this NIC'); + if (!license) throw new NotFoundException(); return license; } async getAllLicenses(nic?: string) { + await this.autoActivateLicenses(); return this.prisma.driving_License.findMany({ where: nic ? { nic_No: { contains: nic, mode: 'insensitive' } } : {}, include: { @@ -284,7 +328,7 @@ export class LicenseService { where: { license_Id: licenseId }, }); - if (!license) throw new NotFoundException('License not found'); + if (!license) throw new NotFoundException(); const updatePayload: { full_Name?: string; @@ -326,6 +370,7 @@ export class LicenseService { } async getLicensesWithFines(nic?: string) { + await this.autoActivateLicenses(); return this.prisma.driving_License.findMany({ where: { fines: { some: {} }, diff --git a/frontend_apps/auto_ledger/.fvmrc b/frontend_apps/auto_ledger/.fvmrc new file mode 100644 index 0000000..c300356 --- /dev/null +++ b/frontend_apps/auto_ledger/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "stable" +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/.gitignore b/frontend_apps/auto_ledger/.gitignore new file mode 100644 index 0000000..f03d821 --- /dev/null +++ b/frontend_apps/auto_ledger/.gitignore @@ -0,0 +1,58 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# FVM Version Cache +.fvm/ + +# Firebase +**/google-services.json +**/GoogleService-Info.plist +firebase.json +google-services.json + +#Privaate keys +.env + diff --git a/frontend_apps/auto_ledger/.metadata b/frontend_apps/auto_ledger/.metadata new file mode 100644 index 0000000..6a623a4 --- /dev/null +++ b/frontend_apps/auto_ledger/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: android + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: ios + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: linux + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: macos + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: web + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: windows + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend_apps/auto_ledger/README.md b/frontend_apps/auto_ledger/README.md new file mode 100644 index 0000000..cac587c --- /dev/null +++ b/frontend_apps/auto_ledger/README.md @@ -0,0 +1,16 @@ +# auto_ledger + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/frontend_apps/auto_ledger/all_flutter_code.txt b/frontend_apps/auto_ledger/all_flutter_code.txt new file mode 100644 index 0000000..6e09396 --- /dev/null +++ b/frontend_apps/auto_ledger/all_flutter_code.txt @@ -0,0 +1,3173 @@ + +/// --- firebase_options.dart --- /// +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyB22a7ZLYFaUusG_D7wzyzg6Df_Io-zLn4', + appId: '1:663237094439:android:acdc387c864d6c0703b202', + messagingSenderId: '663237094439', + projectId: 'auto-ledger-41d16', + storageBucket: 'auto-ledger-41d16.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDzJ7AhObCD_NPpZLufbQy7zY1t2IFDukE', + appId: '1:663237094439:ios:675897db564987f003b202', + messagingSenderId: '663237094439', + projectId: 'auto-ledger-41d16', + storageBucket: 'auto-ledger-41d16.firebasestorage.app', + iosBundleId: 'com.example.autoLedger', + ); +} + +/// --- main.dart --- /// +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'firebase_options.dart'; +import 'services/api_service.dart'; +import 'screens/splash_screen.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + ApiService.init(); + + runApp(const AutoLedgerApp()); +} + +class AutoLedgerApp extends StatelessWidget { + const AutoLedgerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Auto Ledger', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1A2980)), + useMaterial3: true, + textTheme: GoogleFonts.poppinsTextTheme(), + ), + home: const SplashScreen(), + ); + } +} +/// --- home_screen.dart --- /// +import 'dart:async'; +import 'dart:ui'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import '../services/api_service.dart'; +import '../utils/secure_storage.dart'; +import 'login_screen.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + int _currentIndex = 0; + bool _isFront = true; + + Map? _licenseData; + bool _isLoading = true; + String _errorMessage = ''; + + bool _showQR = false; + bool _isGeneratingQR = false; + String _qrToken = ''; + int _remainingSeconds = 180; + Timer? _timer; + + @override + void initState() { + super.initState(); + _fetchLicenseData(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + Future _fetchLicenseData() async { + try { + final response = await ApiService.dio.get('/license/my-license'); + setState(() { + _licenseData = response.data; + _isLoading = false; + }); + } on DioException catch (e) { + setState(() { + _errorMessage = e.response?.data['message'] ?? 'Failed to load license details.'; + _isLoading = false; + }); + } catch (e) { + setState(() { + _errorMessage = 'An unexpected error occurred.'; + _isLoading = false; + }); + } + } + + Future _generateQR() async { + setState(() { + _isGeneratingQR = true; + _showQR = true; + }); + + try { + final response = await ApiService.dio.get('/license/generate-qr'); + final String token = response.data['qrToken']; + + setState(() { + _qrToken = token; + _isGeneratingQR = false; + _remainingSeconds = 180; + }); + + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_remainingSeconds > 0) { + setState(() => _remainingSeconds--); + } else { + timer.cancel(); + setState(() => _showQR = false); + } + }); + } catch (e) { + setState(() { + _isGeneratingQR = false; + _showQR = false; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to generate QR Code. Check license status.'), backgroundColor: Colors.red), + ); + } + } + } + + String get _formattedTime { + int minutes = _remainingSeconds ~/ 60; + int seconds = _remainingSeconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + + Future _logout() async { + await SecureStorage.deleteToken(); + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); + } + } + + String _formatDate(String? isoString) { + if (isoString == null || isoString.isEmpty) return '---'; + try { + final date = DateTime.parse(isoString); + return '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}.${date.year}'; + } catch (e) { + return '---'; + } + } + + Widget _buildDetailText(String number, String value, {bool isBold = false}) { + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: number, + style: TextStyle(fontSize: 8.5, color: Colors.blueGrey[800], fontWeight: FontWeight.bold), + ), + TextSpan( + text: value, + style: TextStyle(fontSize: 9.5, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: Colors.black87), + ), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } + + Widget _buildFrontCard() { + final String name = _licenseData?['full_Name'] ?? 'N/A'; + final String address = _licenseData?['address'] ?? 'N/A'; + final String nicNo = _licenseData?['nic_No'] ?? 'N/A'; + final String licenseNo = _licenseData?['license_No'] ?? 'N/A'; + final String dob = _formatDate(_licenseData?['date_of_birth']); + final String bloodGroup = _licenseData?['blood_Group'] ?? '-'; + final String issueDate = _formatDate(_licenseData?['issue_Date']); + final String status = _licenseData?['status'] ?? 'UNKNOWN'; + final String? imageUrl = _licenseData?['image']; + + return Container( + key: const ValueKey(true), + width: double.infinity, + height: 230, + decoration: BoxDecoration( + color: const Color(0xFFEAF5E1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.withValues(alpha: 0.3)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 5)), + ], + ), + child: Stack( + children: [ + Positioned.fill( + child: Center( + child: Opacity( + opacity: 0.08, + child: Image.asset( + 'assets/emblem.png', + width: 130, + color: Colors.black, + colorBlendMode: BlendMode.srcIn, + errorBuilder: (c, e, s) => const SizedBox(), + ), + ), + ), + ), + Positioned( + bottom: 18, + right: 48, + child: Opacity( + opacity: 0.15, + child: Image.asset( + 'assets/punkalasa.png', + width: 60, + color: Colors.black, + colorBlendMode: BlendMode.srcIn, + errorBuilder: (c, e, s) => const Icon(Icons.security, size: 50, color: Colors.black), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration(border: Border.all(color: Colors.grey, width: 0.5)), + child: Image.asset( + 'assets/flag.png', + width: 35, + height: 22, + fit: BoxFit.cover, + errorBuilder: (c, e, s) => Container(width: 35, height: 22, color: Colors.grey[300]), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('DRIVING LICENCE', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: Color(0xFF0D47A1), letterSpacing: 1.0)), + Container( + margin: const EdgeInsets.symmetric(vertical: 2), + height: 1.0, + width: double.infinity, + color: Colors.grey.withValues(alpha: 0.4), + ), + const Text('DEMOCRATIC SOCIALIST REPUBLIC OF SRI LANKA', textAlign: TextAlign.center, style: TextStyle(fontSize: 8.5, fontWeight: FontWeight.w900, color: Color(0xFF0D47A1))), + ], + ), + ), + const SizedBox(width: 8), + Image.asset( + 'assets/emblem.png', + width: 30, + height: 40, + errorBuilder: (c, e, s) => const SizedBox(width: 30, height: 40), + ), + ], + ), + const SizedBox(height: 10), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8.0, left: 10.0), + child: Column( + children: [ + Container( + width: 70, + height: 85, + decoration: const BoxDecoration( + color: Colors.transparent, + ), + child: imageUrl != null && imageUrl.isNotEmpty + ? Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => const Icon(Icons.person, size: 55, color: Colors.black54), + ) + : const Icon(Icons.person, size: 55, color: Colors.black54), + ), + const SizedBox(height: 4), + Text('4a. $issueDate', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Colors.black87)), + const SizedBox(height: 12), + Builder( + builder: (context) { + List statusGradient; + Color glowColor; + IconData statusIcon; + + if (status == 'ACTIVE') { + statusGradient = [const Color(0xFF00b09b), const Color(0xFF96c93d)]; + glowColor = const Color(0xFF00b09b); + statusIcon = Icons.check_circle_rounded; + } else if (status == 'SUSPENDED') { + statusGradient = [const Color(0xFFf12711), const Color(0xFFf5af19)]; + glowColor = const Color(0xFFf12711); + statusIcon = Icons.warning_rounded; + } else { + statusGradient = [const Color(0xFFcb2d3e), const Color(0xFFef473a)]; + glowColor = const Color(0xFFcb2d3e); + statusIcon = Icons.cancel_rounded; + } + + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: statusGradient.map((c) => c.withValues(alpha: 0.8)).toList(), + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: glowColor.withValues(alpha: 0.4), + blurRadius: 4, + spreadRadius: 1, + offset: const Offset(0, 1), + ), + ], + border: Border.all( + color: Colors.white.withValues(alpha: 0.6), + width: 1.0, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(statusIcon, color: Colors.white, size: 11), + const SizedBox(width: 4), + Text( + status, + style: const TextStyle( + color: Colors.white, + fontSize: 9.5, + fontWeight: FontWeight.w900, + letterSpacing: 0.8, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ], + ), + ), + const SizedBox(width: 15), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 6.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDetailText('5. ', licenseNo, isBold: true)), + const SizedBox(width: 8), + Expanded(child: _buildDetailText('4c. ', nicNo)), + ], + ), + const SizedBox(height: 8), + _buildDetailText('1, 2. ', name), + const SizedBox(height: 8), + _buildDetailText('8. ', address), + const SizedBox(height: 8), + _buildDetailText('3. ', dob), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('Blood Group ', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600)), + Text(bloodGroup, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87)), + const Spacer(), + const Text('SL', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: Color(0xFF8E24AA))), + ], + ), + const SizedBox(height: 6), + ], + ), + ), + ), + ], + ), + ) + ], + ), + ), + ], + ), + ); + } + + TableRow _buildCategoryRow(String code, String icon) { + final categories = _licenseData?['vehicleCategories'] as List? ?? []; + final cat = categories.cast>().firstWhere( + (c) => c['vehicle_Class'] == code, + orElse: () => {}, + ); + + if (cat.isNotEmpty) { + return _buildTableRow( + '$code $icon', + _formatDate(cat['issue_Date']), + _formatDate(cat['expiry_Date']), + cat['restriction'] ?? '---' + ); + } else { + return _buildTableRow('$code $icon', '---', '---', '---'); + } + } + + Widget _buildBackCard() { + return Container( + key: const ValueKey(false), + width: double.infinity, + height: 230, + decoration: BoxDecoration( + color: const Color(0xFFEAF5E1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.withValues(alpha: 0.3)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 5)), + ], + ), + child: Stack( + children: [ + Positioned.fill( + child: Center( + child: Opacity( + opacity: 0.04, + child: Image.asset( + 'assets/emblem.png', + width: 130, + color: Colors.black, + colorBlendMode: BlendMode.srcIn, + errorBuilder: (c, e, s) => const SizedBox(), + ), + ), + ), + ), + Positioned( + left: 6, + top: 15, + bottom: 15, + child: Center( + child: RotatedBox( + quarterTurns: 3, + child: Text( + 'Department of Motor Traffic - Sri Lanka', + style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.blueGrey[800]), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 28, right: 16, top: 12, bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLegendText('1. Surname'), + _buildLegendText('2. Other names'), + _buildLegendText('3. Date of birth'), + _buildLegendText('4a. Date of Issue of the License'), + _buildLegendText('4b. Issuing Authority'), + _buildLegendText('4c. Administrative Number'), + _buildLegendText('5. Number of the LICENCE'), + _buildLegendText('7. Signature of the holder'), + _buildLegendText('8. Permanent place of residence'), + _buildLegendText('9. Categories of vehicles'), + _buildLegendText('10. Date of Issue per category'), + _buildLegendText('11. Date of Expiry per category'), + _buildLegendText('12. Restrictions in code form'), + ], + ), + ), + const SizedBox(width: 6), + Expanded( + flex: 7, + child: Align( + alignment: Alignment.centerLeft, + child: Table( + border: TableBorder.all(color: Colors.black.withValues(alpha: 0.3), width: 0.5), + columnWidths: const { + 0: FlexColumnWidth(1.2), + 1: FlexColumnWidth(2.2), + 2: FlexColumnWidth(2.2), + 3: FlexColumnWidth(1.2), + }, + children: [ + _buildTableRow('9.', '10.', '11.', '12.', isHeader: true), + _buildCategoryRow('A1', '🛺'), + _buildCategoryRow('A', '🏍️'), + _buildCategoryRow('B1', '🛺'), + _buildCategoryRow('B', '🚗'), + _buildCategoryRow('C1', '🚚'), + _buildCategoryRow('C', '🚛'), + _buildCategoryRow('CE', '🚛'), + _buildCategoryRow('D1', '🚐'), + _buildCategoryRow('D', '🚌'), + _buildCategoryRow('DE', '🚌'), + _buildCategoryRow('G1', '🚜'), + _buildCategoryRow('G', '🚜'), + _buildCategoryRow('J', '🏗️'), + _buildCategoryRow('H', '♿'), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildLegendText(String text) { + return Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + text, + style: TextStyle(fontSize: 6.0, color: Colors.blueGrey[800], fontWeight: FontWeight.w600, height: 1.0), + ), + ); + } + + TableRow _buildTableRow(String col1, String col2, String col3, String restriction, {bool isHeader = false}) { + return TableRow( + decoration: BoxDecoration(color: isHeader ? Colors.grey.withValues(alpha: 0.2) : Colors.transparent), + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 1.0, horizontal: 1.0), + child: Text(col1, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal)), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 1.0, horizontal: 1.0), + child: Text(col2, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal)), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 1.0, horizontal: 1.0), + child: Text(col3, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal)), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 1.0, horizontal: 1.0), + child: Text(restriction, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal)), + ), + ], + ); + } + + Widget _buildDashboard() { + if (_isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (_errorMessage.isNotEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 60), + const SizedBox(height: 16), + Text(_errorMessage, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + _isLoading = true; + _errorMessage = ''; + }); + _fetchLicenseData(); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 10), + Text( + _isFront ? 'Tap the card to see the back side' : 'Tap the card to see the front side', + style: const TextStyle(color: Colors.grey, fontSize: 12, fontStyle: FontStyle.italic), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: () { + setState(() { + _isFront = !_isFront; + }); + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition(opacity: animation, child: child); + }, + child: _isFront ? _buildFrontCard() : _buildBackCard(), + ), + ), + const SizedBox(height: 32), + if (!_showQR) + SizedBox( + width: double.infinity, + height: 55, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1A2980), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 5, + ), + icon: const Icon(Icons.qr_code_scanner, size: 28), + label: const Text('SHOW QR TO OFFICER', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + onPressed: _generateQR, + ), + ) + else + Column( + children: [ + const Text('Scan within the time limit', style: TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, 5))], + ), + child: _isGeneratingQR + ? const SizedBox( + height: 200, + width: 200, + child: Center(child: CircularProgressIndicator()) + ) + : QrImageView( + data: _qrToken, + version: QrVersions.auto, + size: 200.0, + eyeStyle: const QrEyeStyle(eyeShape: QrEyeShape.square, color: Color(0xFF1A2980)), + ), + ), + const SizedBox(height: 16), + Text( + _formattedTime, + style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.redAccent), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () { + _timer?.cancel(); + setState(() => _showQR = false); + }, + child: const Text('Close QR', style: TextStyle(fontSize: 16)), + ) + ], + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: const Color(0xFF1A2980), + foregroundColor: Colors.white, + title: const Text('Auto-Ledger Dashboard', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + elevation: 0, + actions: [ + IconButton(icon: const Icon(Icons.logout), onPressed: _logout), + ], + ), + body: _currentIndex == 0 + ? _buildDashboard() + : _currentIndex == 1 + ? const Center(child: Text('Fines Screen Coming Soon!')) + : const Center(child: Text('Profile Screen Coming Soon!')), + bottomNavigationBar: NavigationBar( + height: 65, + selectedIndex: _currentIndex, + onDestinationSelected: (index) => setState(() => _currentIndex = index), + destinations: const [ + NavigationDestination(icon: Icon(Icons.credit_card), label: 'License'), + NavigationDestination(icon: Icon(Icons.receipt_long), label: 'Fines'), + NavigationDestination(icon: Icon(Icons.person), label: 'Profile'), + ], + ), + ); + } +} +/// --- login_screen.dart --- /// +import 'package:flutter/material.dart'; +import '../services/auth_service.dart'; +import '../utils/device_info.dart'; +import '../utils/settings_util.dart'; +import '../widgets/glass_container.dart'; +import 'home_screen.dart'; +import 'register_screen.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _nicController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _isLoading = false; + bool _isBiometricEnabled = false; + bool _obscurePassword = true; + + @override + void initState() { + super.initState(); + _checkBiometricStatus(); + } + + Future _checkBiometricStatus() async { + final isEnabled = await SettingsUtil.isBiometricEnabled(); + setState(() { + _isBiometricEnabled = isEnabled; + }); + } + + void _showToast(String message, {bool isError = false}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(isError ? Icons.error_outline : Icons.check_circle_outline, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ], + ), + backgroundColor: isError ? Colors.redAccent : Colors.green.shade600, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + margin: const EdgeInsets.all(16), + elevation: 6, + duration: const Duration(seconds: 3), + ), + ); + } + + Future _handleLogin() async { + if (_nicController.text.trim().isEmpty || _passwordController.text.trim().isEmpty) { + _showToast('Please enter both NIC and Password', isError: true); + return; + } + + setState(() => _isLoading = true); + try { + final deviceId = await DeviceInfoUtil.getDeviceId(); + final success = await AuthService.loginUser( + _nicController.text.trim(), + _passwordController.text.trim(), + deviceId, + ); + + if (success && mounted) { + _showToast('Login Successful!'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } + } catch (e) { + if (mounted) { + _showToast('Login Failed. Check credentials or device.', isError: true); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF1A2980), Color(0xFF26D0CE)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: GlassContainer( + width: double.infinity, + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.directions_car, size: 60, color: Colors.white), + const SizedBox(height: 16), + const Text( + 'Auto-Ledger', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 32), + TextField( + controller: _nicController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'NIC Number', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white54), + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Password', + labelStyle: const TextStyle(color: Colors.white70), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white54), + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + const SizedBox(height: 24), + if (_isLoading) + const CircularProgressIndicator(color: Colors.white) + else + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF1A2980), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: _handleLogin, + child: const Text( + 'LOGIN', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(height: 16), + if (_isBiometricEnabled) + IconButton( + icon: const Icon(Icons.fingerprint, color: Colors.white, size: 40), + onPressed: () { + _showToast('Biometric Login Coming Soon!', isError: false); + }, + ), + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const RegisterScreen()), + ); + }, + child: const Text( + 'New Driver? Register Here', + style: TextStyle(color: Colors.white70), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} +/// --- register_screen.dart --- /// +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../services/auth_service.dart'; +import '../utils/device_info.dart'; +import '../widgets/glass_container.dart'; +import 'login_screen.dart'; +import 'home_screen.dart'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final _nicController = TextEditingController(); + final _nameController = TextEditingController(); + final _mobileController = TextEditingController(); + final _passwordController = TextEditingController(); + final _otpController = TextEditingController(); + + bool _isLoading = false; + bool _obscurePassword = true; + String _verificationId = ''; + Map? _registeredData; + + void _showToast(String message, {bool isError = false}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(isError ? Icons.error_outline : Icons.check_circle_outline, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ], + ), + backgroundColor: isError ? Colors.redAccent : Colors.green.shade600, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + margin: const EdgeInsets.all(16), + elevation: 6, + duration: const Duration(seconds: 3), + ), + ); + } + + bool _validateInputs() { + if (_nicController.text.trim().isEmpty || + _nameController.text.trim().isEmpty || + _mobileController.text.trim().isEmpty || + _passwordController.text.trim().isEmpty) { + _showToast('All fields are required!', isError: true); + return false; + } + if (_passwordController.text.trim().length < 8) { + _showToast('Password must be at least 8 characters long', isError: true); + return false; + } + return true; + } + + Future _handleRegister() async { + if (!_validateInputs()) return; + + setState(() => _isLoading = true); + try { + final deviceId = await DeviceInfoUtil.getDeviceId(); + final nic = _nicController.text.trim(); + final phone = _mobileController.text.trim(); + + _registeredData = { + "nicNo": nic, + "name": _nameController.text.trim(), + "mobilePhoneNo": phone, + "password": _passwordController.text.trim(), + "deviceId": deviceId + }; + + final success = await AuthService.registerUser(_registeredData!); + + if (success && mounted) { + await _sendOTP(phone); + } + } catch (e) { + if (mounted) { + _showToast('Registration Failed. Check NIC.', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _sendOTP(String phone) async { + await FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: phone, + verificationCompleted: (PhoneAuthCredential credential) async { + await FirebaseAuth.instance.signInWithCredential(credential); + await _verifyBackendAndLogin(); + }, + verificationFailed: (FirebaseAuthException e) { + if (mounted) { + _showToast(e.message ?? 'Verification Failed', isError: true); + setState(() => _isLoading = false); + } + }, + codeSent: (String verificationId, int? resendToken) { + setState(() { + _verificationId = verificationId; + _isLoading = false; + }); + _showOTPDialog(); + }, + codeAutoRetrievalTimeout: (String verificationId) { + _verificationId = verificationId; + }, + ); + } + + Future _verifyOTP() async { + setState(() => _isLoading = true); + try { + final credential = PhoneAuthProvider.credential( + verificationId: _verificationId, + smsCode: _otpController.text.trim(), + ); + await FirebaseAuth.instance.signInWithCredential(credential); + if (mounted) { + Navigator.pop(context); + } + await _verifyBackendAndLogin(); + } catch (e) { + if (mounted) { + _showToast('Invalid OTP', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _verifyBackendAndLogin() async { + try { + final isVerified = await AuthService.verifyRegistration(_registeredData!['nicNo']); + if (isVerified && mounted) { + _showToast('Registration & Verification Successful!'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } + } catch (e) { + if (mounted) { + _showToast('Backend Verification Failed', isError: true); + setState(() => _isLoading = false); + } + } + } + + void _showOTPDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('Enter OTP'), + content: TextField( + controller: _otpController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'OTP Code', + prefixIcon: Icon(Icons.security), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + setState(() => _isLoading = false); + }, + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: _verifyOTP, + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF1A2980), foregroundColor: Colors.white), + child: const Text('Verify'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF1A2980), Color(0xFF26D0CE)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 40.0), + child: GlassContainer( + width: double.infinity, + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Register Driver', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white), + ), + const SizedBox(height: 24), + TextField( + controller: _nicController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'NIC Number', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white54), borderRadius: BorderRadius.circular(12)), + focusedBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white), borderRadius: BorderRadius.circular(12)), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _nameController, + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.name, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Full Name', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white54), borderRadius: BorderRadius.circular(12)), + focusedBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white), borderRadius: BorderRadius.circular(12)), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _mobileController, + keyboardType: TextInputType.phone, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Mobile Number (+94...)', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white54), borderRadius: BorderRadius.circular(12)), + focusedBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white), borderRadius: BorderRadius.circular(12)), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Password', + labelStyle: const TextStyle(color: Colors.white70), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + enabledBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white54), borderRadius: BorderRadius.circular(12)), + focusedBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white), borderRadius: BorderRadius.circular(12)), + ), + ), + const SizedBox(height: 24), + if (_isLoading) + const CircularProgressIndicator(color: Colors.white) + else + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF1A2980), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _handleRegister, + child: const Text('REGISTER', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 16), + TextButton( + onPressed: () => Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const LoginScreen()), + ), + child: const Text('Already have an account? Login', style: TextStyle(color: Colors.white70)), + ), + ], + ), + ), + ), + ), + ), + ); + } +} +/// --- splash_screen.dart --- /// +import 'package:flutter/material.dart'; +import '../utils/secure_storage.dart'; +import 'login_screen.dart'; +import 'home_screen.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + @override + void initState() { + super.initState(); + _checkAuthStatus(); + } + + Future _checkAuthStatus() async { + await Future.delayed(const Duration(seconds: 2)); + final token = await SecureStorage.getToken(); + + if (!mounted) return; + + if (token != null) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); + } + } + + @override + Widget build(BuildContext context) { + return const Scaffold( + backgroundColor: Color(0xFF1A2980), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.directions_car, size: 80, color: Colors.white), + SizedBox(height: 20), + CircularProgressIndicator(color: Colors.white), + ], + ), + ), + ); + } +} +/// --- api_service.dart --- /// +import 'package:dio/dio.dart'; +import '../utils/secure_storage.dart'; + +class ApiService { + static final Dio _dio = Dio(BaseOptions( + baseUrl: 'http://47.129.144.60:3000', + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), + )); + + static void init() { + _dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) async { + final token = await SecureStorage.getToken(); + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } + return handler.next(options); + }, + )); + } + + static Dio get dio => _dio; +} +/// --- auth_service.dart --- /// +import 'package:dio/dio.dart'; +import 'api_service.dart'; +import '../utils/secure_storage.dart'; + +class AuthService { + static Future registerUser(Map data) async { + try { + final response = await ApiService.dio.post('/auth/user/register', data: data); + return response.statusCode == 201 || response.statusCode == 200; + } on DioException { + rethrow; + } + } + + static Future verifyRegistration(String nicNo) async { + try { + final response = await ApiService.dio.post( + '/auth/user/verify-registration', + data: {'nicNo': nicNo}, + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final token = response.data['accessToken']; + await SecureStorage.saveToken(token); + return true; + } + return false; + } on DioException { + rethrow; + } + } + + static Future loginUser(String nicNo, String password, String deviceId) async { + try { + final response = await ApiService.dio.post( + '/auth/user/login', + data: { + 'nicNo': nicNo, + 'password': password, + 'deviceId': deviceId, + }, + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final token = response.data['accessToken']; + await SecureStorage.saveToken(token); + return true; + } + return false; + } on DioException { + rethrow; + } + } +} +/// --- user_service.dart --- /// +import 'package:dio/dio.dart'; +import 'api_service.dart'; + +class UserService { + static Future?> getUserProfile() async { + try { + final response = await ApiService.dio.get('/users/profile'); + if (response.statusCode == 200) { + return response.data; + } + return null; + } on DioException { + rethrow; + } + } + + static Future changePassword(String oldPassword, String newPassword) async { + try { + final response = await ApiService.dio.patch( + '/auth/user/change-password', + data: { + 'oldPassword': oldPassword, + 'newPassword': newPassword, + }, + ); + return response.statusCode == 200; + } on DioException { + rethrow; + } + } +} +/// --- device_info.dart --- /// +import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; + +class DeviceInfoUtil { + static Future getDeviceId() async { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + return androidInfo.id; + } else if (Platform.isIOS) { + final IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + return iosInfo.identifierForVendor ?? 'UNKNOWN_IOS_DEVICE'; + } + return 'UNKNOWN_DEVICE'; + } +} +/// --- secure_storage.dart --- /// +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class SecureStorage { + static const _storage = FlutterSecureStorage(); + + static Future saveToken(String token) async => + await _storage.write(key: 'jwt_token', value: token); + + static Future getToken() async => + await _storage.read(key: 'jwt_token'); + + static Future deleteToken() async => + await _storage.delete(key: 'jwt_token'); +} +/// --- settings_util.dart --- /// +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingsUtil { + static const String _biometricKey = 'biometric_enabled'; + + static Future isBiometricEnabled() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_biometricKey) ?? false; + } + + static Future setBiometricEnabled(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_biometricKey, value); + } +} +/// --- glass_container.dart --- /// +import 'dart:ui'; +import 'package:flutter/material.dart'; + +class GlassContainer extends StatelessWidget { + final Widget child; + final double? width; + final double? height; + final double borderRadius; + final EdgeInsetsGeometry padding; + + const GlassContainer({ + super.key, + required this.child, + this.width = double.infinity, + this.height, + this.borderRadius = 16.0, + this.padding = const EdgeInsets.all(16.0), + }); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: Container( + width: width, + height: height, + padding: padding, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(borderRadius), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1.5, + ), + ), + child: child, + ), + ), + ); + } +} +/// --- firebase_options.dart --- /// +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyB22a7ZLYFaUusG_D7wzyzg6Df_Io-zLn4', + appId: '1:663237094439:android:acdc387c864d6c0703b202', + messagingSenderId: '663237094439', + projectId: 'auto-ledger-41d16', + storageBucket: 'auto-ledger-41d16.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDzJ7AhObCD_NPpZLufbQy7zY1t2IFDukE', + appId: '1:663237094439:ios:675897db564987f003b202', + messagingSenderId: '663237094439', + projectId: 'auto-ledger-41d16', + storageBucket: 'auto-ledger-41d16.firebasestorage.app', + iosBundleId: 'com.example.autoLedger', + ); +} + +/// --- main.dart --- /// +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'firebase_options.dart'; +import 'services/api_service.dart'; +import 'screens/splash_screen.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + ApiService.init(); + + runApp(const AutoLedgerApp()); +} + +class AutoLedgerApp extends StatelessWidget { + const AutoLedgerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Auto Ledger', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1A2980)), + useMaterial3: true, + textTheme: GoogleFonts.poppinsTextTheme(), + ), + home: const SplashScreen(), + ); + } +} +/// --- home_screen.dart --- /// +import 'dart:async'; +import 'dart:ui'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import '../services/api_service.dart'; +import '../utils/secure_storage.dart'; +import 'login_screen.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + int _currentIndex = 0; + bool _isFront = true; + + Map? _licenseData; + bool _isLoading = true; + String _errorMessage = ''; + + bool _showQR = false; + bool _isGeneratingQR = false; + String _qrToken = ''; + int _remainingSeconds = 180; + Timer? _timer; + + @override + void initState() { + super.initState(); + _fetchLicenseData(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + Future _fetchLicenseData() async { + try { + final response = await ApiService.dio.get('/license/my-license'); + setState(() { + _licenseData = response.data; + _isLoading = false; + }); + } on DioException catch (e) { + setState(() { + _errorMessage = e.response?.data['message'] ?? 'Failed to load license details.'; + _isLoading = false; + }); + } catch (e) { + setState(() { + _errorMessage = 'An unexpected error occurred.'; + _isLoading = false; + }); + } + } + + Future _generateQR() async { + setState(() { + _isGeneratingQR = true; + _showQR = true; + }); + + try { + final response = await ApiService.dio.get('/license/generate-qr'); + final String token = response.data['qrToken']; + + setState(() { + _qrToken = token; + _isGeneratingQR = false; + _remainingSeconds = 180; + }); + + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_remainingSeconds > 0) { + setState(() => _remainingSeconds--); + } else { + timer.cancel(); + setState(() => _showQR = false); + } + }); + } catch (e) { + setState(() { + _isGeneratingQR = false; + _showQR = false; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to generate QR Code. Check license status.'), backgroundColor: Colors.red), + ); + } + } + } + + String get _formattedTime { + int minutes = _remainingSeconds ~/ 60; + int seconds = _remainingSeconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + + Future _logout() async { + await SecureStorage.deleteToken(); + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); + } + } + + String _formatDate(String? isoString) { + if (isoString == null || isoString.isEmpty) return '---'; + try { + final date = DateTime.parse(isoString); + return '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}.${date.year}'; + } catch (e) { + return '---'; + } + } + + Widget _buildDetailText(String number, String value, {bool isBold = false}) { + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: number, + style: TextStyle(fontSize: 8.5, color: Colors.blueGrey[800], fontWeight: FontWeight.bold), + ), + TextSpan( + text: value, + style: TextStyle(fontSize: 9.5, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: Colors.black87), + ), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } + + Widget _buildFrontCard() { + final String name = _licenseData?['full_Name'] ?? 'N/A'; + final String address = _licenseData?['address'] ?? 'N/A'; + final String nicNo = _licenseData?['nic_No'] ?? 'N/A'; + final String licenseNo = _licenseData?['license_No'] ?? 'N/A'; + final String dob = _formatDate(_licenseData?['date_of_birth']); + final String bloodGroup = _licenseData?['blood_Group'] ?? '-'; + final String issueDate = _formatDate(_licenseData?['issue_Date']); + final String status = _licenseData?['status'] ?? 'UNKNOWN'; + final String? imageUrl = _licenseData?['image']; + + return Container( + key: const ValueKey(true), + width: double.infinity, + height: 230, + decoration: BoxDecoration( + color: const Color(0xFFEAF5E1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.withValues(alpha: 0.3)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 5)), + ], + ), + child: Stack( + children: [ + Positioned.fill( + child: Center( + child: Opacity( + opacity: 0.08, + child: Image.asset( + 'assets/emblem.png', + width: 130, + color: Colors.black, + colorBlendMode: BlendMode.srcIn, + errorBuilder: (c, e, s) => const SizedBox(), + ), + ), + ), + ), + Positioned( + bottom: 18, + right: 48, + child: Opacity( + opacity: 0.15, + child: Image.asset( + 'assets/punkalasa.png', + width: 60, + color: Colors.black, + colorBlendMode: BlendMode.srcIn, + errorBuilder: (c, e, s) => const Icon(Icons.security, size: 50, color: Colors.black), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration(border: Border.all(color: Colors.grey, width: 0.5)), + child: Image.asset( + 'assets/flag.png', + width: 35, + height: 22, + fit: BoxFit.cover, + errorBuilder: (c, e, s) => Container(width: 35, height: 22, color: Colors.grey[300]), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('DRIVING LICENCE', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: Color(0xFF0D47A1), letterSpacing: 1.0)), + Container( + margin: const EdgeInsets.symmetric(vertical: 2), + height: 1.0, + width: double.infinity, + color: Colors.grey.withValues(alpha: 0.4), + ), + const Text('DEMOCRATIC SOCIALIST REPUBLIC OF SRI LANKA', textAlign: TextAlign.center, style: TextStyle(fontSize: 8.5, fontWeight: FontWeight.w900, color: Color(0xFF0D47A1))), + ], + ), + ), + const SizedBox(width: 8), + Image.asset( + 'assets/emblem.png', + width: 30, + height: 40, + errorBuilder: (c, e, s) => const SizedBox(width: 30, height: 40), + ), + ], + ), + const SizedBox(height: 10), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8.0, left: 10.0), + child: Column( + children: [ + Container( + width: 70, + height: 85, + decoration: const BoxDecoration( + color: Colors.transparent, + ), + child: imageUrl != null && imageUrl.isNotEmpty + ? Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => const Icon(Icons.person, size: 55, color: Colors.black54), + ) + : const Icon(Icons.person, size: 55, color: Colors.black54), + ), + const SizedBox(height: 4), + Text('4a. $issueDate', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Colors.black87)), + const SizedBox(height: 12), + Builder( + builder: (context) { + List statusGradient; + Color glowColor; + IconData statusIcon; + + if (status == 'ACTIVE') { + statusGradient = [const Color(0xFF00b09b), const Color(0xFF96c93d)]; + glowColor = const Color(0xFF00b09b); + statusIcon = Icons.check_circle_rounded; + } else if (status == 'SUSPENDED') { + statusGradient = [const Color(0xFFf12711), const Color(0xFFf5af19)]; + glowColor = const Color(0xFFf12711); + statusIcon = Icons.warning_rounded; + } else { + statusGradient = [const Color(0xFFcb2d3e), const Color(0xFFef473a)]; + glowColor = const Color(0xFFcb2d3e); + statusIcon = Icons.cancel_rounded; + } + + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: statusGradient.map((c) => c.withValues(alpha: 0.8)).toList(), + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: glowColor.withValues(alpha: 0.4), + blurRadius: 4, + spreadRadius: 1, + offset: const Offset(0, 1), + ), + ], + border: Border.all( + color: Colors.white.withValues(alpha: 0.6), + width: 1.0, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(statusIcon, color: Colors.white, size: 11), + const SizedBox(width: 4), + Text( + status, + style: const TextStyle( + color: Colors.white, + fontSize: 9.5, + fontWeight: FontWeight.w900, + letterSpacing: 0.8, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ], + ), + ), + const SizedBox(width: 15), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 6.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDetailText('5. ', licenseNo, isBold: true)), + const SizedBox(width: 8), + Expanded(child: _buildDetailText('4c. ', nicNo)), + ], + ), + const SizedBox(height: 8), + _buildDetailText('1, 2. ', name), + const SizedBox(height: 8), + _buildDetailText('8. ', address), + const SizedBox(height: 8), + _buildDetailText('3. ', dob), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('Blood Group ', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600)), + Text(bloodGroup, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87)), + const Spacer(), + const Text('SL', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: Color(0xFF8E24AA))), + ], + ), + const SizedBox(height: 6), + ], + ), + ), + ), + ], + ), + ) + ], + ), + ), + ], + ), + ); + } + + TableRow _buildCategoryRow(String code, String icon) { + final categories = _licenseData?['vehicleCategories'] as List? ?? []; + final cat = categories.cast>().firstWhere( + (c) => c['vehicle_Class'] == code, + orElse: () => {}, + ); + + if (cat.isNotEmpty) { + return _buildTableRow( + '$code $icon', + _formatDate(cat['issue_Date']), + _formatDate(cat['expiry_Date']), + cat['restriction'] ?? '---' + ); + } else { + return _buildTableRow('$code $icon', '---', '---', '---'); + } + } + + Widget _buildBackCard() { + return Container( + key: const ValueKey(false), + width: double.infinity, + height: 230, + decoration: BoxDecoration( + color: const Color(0xFFEAF5E1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.withValues(alpha: 0.3)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 5)), + ], + ), + child: Stack( + children: [ + Positioned.fill( + child: Center( + child: Opacity( + opacity: 0.04, + child: Image.asset( + 'assets/emblem.png', + width: 130, + color: Colors.black, + colorBlendMode: BlendMode.srcIn, + errorBuilder: (c, e, s) => const SizedBox(), + ), + ), + ), + ), + Positioned( + left: 6, + top: 15, + bottom: 15, + child: Center( + child: RotatedBox( + quarterTurns: 3, + child: Text( + 'Department of Motor Traffic - Sri Lanka', + style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.blueGrey[800]), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 28, right: 16, top: 12, bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLegendText('1. Surname'), + _buildLegendText('2. Other names'), + _buildLegendText('3. Date of birth'), + _buildLegendText('4a. Date of Issue of the License'), + _buildLegendText('4b. Issuing Authority'), + _buildLegendText('4c. Administrative Number'), + _buildLegendText('5. Number of the LICENCE'), + _buildLegendText('7. Signature of the holder'), + _buildLegendText('8. Permanent place of residence'), + _buildLegendText('9. Categories of vehicles'), + _buildLegendText('10. Date of Issue per category'), + _buildLegendText('11. Date of Expiry per category'), + _buildLegendText('12. Restrictions in code form'), + ], + ), + ), + const SizedBox(width: 6), + Expanded( + flex: 7, + child: Align( + alignment: Alignment.centerLeft, + child: Table( + border: TableBorder.all(color: Colors.black.withValues(alpha: 0.3), width: 0.5), + columnWidths: const { + 0: FlexColumnWidth(1.2), + 1: FlexColumnWidth(2.2), + 2: FlexColumnWidth(2.2), + 3: FlexColumnWidth(1.2), + }, + children: [ + _buildTableRow('9.', '10.', '11.', '12.', isHeader: true), + _buildCategoryRow('A1', '🛺'), + _buildCategoryRow('A', '🏍️'), + _buildCategoryRow('B1', '🛺'), + _buildCategoryRow('B', '🚗'), + _buildCategoryRow('C1', '🚚'), + _buildCategoryRow('C', '🚛'), + _buildCategoryRow('CE', '🚛'), + _buildCategoryRow('D1', '🚐'), + _buildCategoryRow('D', '🚌'), + _buildCategoryRow('DE', '🚌'), + _buildCategoryRow('G1', '🚜'), + _buildCategoryRow('G', '🚜'), + _buildCategoryRow('J', '🏗️'), + _buildCategoryRow('H', '♿'), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildLegendText(String text) { + return Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + text, + style: TextStyle(fontSize: 6.0, color: Colors.blueGrey[800], fontWeight: FontWeight.w600, height: 1.0), + ), + ); + } + + TableRow _buildTableRow(String col1, String col2, String col3, String restriction, {bool isHeader = false}) { + return TableRow( + decoration: BoxDecoration(color: isHeader ? Colors.grey.withValues(alpha: 0.2) : Colors.transparent), + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 1.0, horizontal: 1.0), + child: Text(col1, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal)), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 1.0, horizontal: 1.0), + child: Text(col2, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal)), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 1.0, horizontal: 1.0), + child: Text(col3, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal)), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 1.0, horizontal: 1.0), + child: Text(restriction, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal)), + ), + ], + ); + } + + Widget _buildDashboard() { + if (_isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (_errorMessage.isNotEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 60), + const SizedBox(height: 16), + Text(_errorMessage, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + _isLoading = true; + _errorMessage = ''; + }); + _fetchLicenseData(); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 10), + Text( + _isFront ? 'Tap the card to see the back side' : 'Tap the card to see the front side', + style: const TextStyle(color: Colors.grey, fontSize: 12, fontStyle: FontStyle.italic), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: () { + setState(() { + _isFront = !_isFront; + }); + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition(opacity: animation, child: child); + }, + child: _isFront ? _buildFrontCard() : _buildBackCard(), + ), + ), + const SizedBox(height: 32), + if (!_showQR) + SizedBox( + width: double.infinity, + height: 55, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1A2980), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 5, + ), + icon: const Icon(Icons.qr_code_scanner, size: 28), + label: const Text('SHOW QR TO OFFICER', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + onPressed: _generateQR, + ), + ) + else + Column( + children: [ + const Text('Scan within the time limit', style: TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, 5))], + ), + child: _isGeneratingQR + ? const SizedBox( + height: 200, + width: 200, + child: Center(child: CircularProgressIndicator()) + ) + : QrImageView( + data: _qrToken, + version: QrVersions.auto, + size: 200.0, + eyeStyle: const QrEyeStyle(eyeShape: QrEyeShape.square, color: Color(0xFF1A2980)), + ), + ), + const SizedBox(height: 16), + Text( + _formattedTime, + style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.redAccent), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () { + _timer?.cancel(); + setState(() => _showQR = false); + }, + child: const Text('Close QR', style: TextStyle(fontSize: 16)), + ) + ], + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: const Color(0xFF1A2980), + foregroundColor: Colors.white, + title: const Text('Auto-Ledger Dashboard', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + elevation: 0, + actions: [ + IconButton(icon: const Icon(Icons.logout), onPressed: _logout), + ], + ), + body: _currentIndex == 0 + ? _buildDashboard() + : _currentIndex == 1 + ? const Center(child: Text('Fines Screen Coming Soon!')) + : const Center(child: Text('Profile Screen Coming Soon!')), + bottomNavigationBar: NavigationBar( + height: 65, + selectedIndex: _currentIndex, + onDestinationSelected: (index) => setState(() => _currentIndex = index), + destinations: const [ + NavigationDestination(icon: Icon(Icons.credit_card), label: 'License'), + NavigationDestination(icon: Icon(Icons.receipt_long), label: 'Fines'), + NavigationDestination(icon: Icon(Icons.person), label: 'Profile'), + ], + ), + ); + } +} +/// --- login_screen.dart --- /// +import 'package:flutter/material.dart'; +import '../services/auth_service.dart'; +import '../utils/device_info.dart'; +import '../utils/settings_util.dart'; +import '../widgets/glass_container.dart'; +import 'home_screen.dart'; +import 'register_screen.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _nicController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _isLoading = false; + bool _isBiometricEnabled = false; + bool _obscurePassword = true; + + @override + void initState() { + super.initState(); + _checkBiometricStatus(); + } + + Future _checkBiometricStatus() async { + final isEnabled = await SettingsUtil.isBiometricEnabled(); + setState(() { + _isBiometricEnabled = isEnabled; + }); + } + + void _showToast(String message, {bool isError = false}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(isError ? Icons.error_outline : Icons.check_circle_outline, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ], + ), + backgroundColor: isError ? Colors.redAccent : Colors.green.shade600, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + margin: const EdgeInsets.all(16), + elevation: 6, + duration: const Duration(seconds: 3), + ), + ); + } + + Future _handleLogin() async { + if (_nicController.text.trim().isEmpty || _passwordController.text.trim().isEmpty) { + _showToast('Please enter both NIC and Password', isError: true); + return; + } + + setState(() => _isLoading = true); + try { + final deviceId = await DeviceInfoUtil.getDeviceId(); + final success = await AuthService.loginUser( + _nicController.text.trim(), + _passwordController.text.trim(), + deviceId, + ); + + if (success && mounted) { + _showToast('Login Successful!'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } + } catch (e) { + if (mounted) { + _showToast('Login Failed. Check credentials or device.', isError: true); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF1A2980), Color(0xFF26D0CE)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: GlassContainer( + width: double.infinity, + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.directions_car, size: 60, color: Colors.white), + const SizedBox(height: 16), + const Text( + 'Auto-Ledger', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 32), + TextField( + controller: _nicController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'NIC Number', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white54), + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Password', + labelStyle: const TextStyle(color: Colors.white70), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white54), + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + const SizedBox(height: 24), + if (_isLoading) + const CircularProgressIndicator(color: Colors.white) + else + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF1A2980), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: _handleLogin, + child: const Text( + 'LOGIN', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(height: 16), + if (_isBiometricEnabled) + IconButton( + icon: const Icon(Icons.fingerprint, color: Colors.white, size: 40), + onPressed: () { + _showToast('Biometric Login Coming Soon!', isError: false); + }, + ), + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const RegisterScreen()), + ); + }, + child: const Text( + 'New Driver? Register Here', + style: TextStyle(color: Colors.white70), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} +/// --- register_screen.dart --- /// +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../services/auth_service.dart'; +import '../utils/device_info.dart'; +import '../widgets/glass_container.dart'; +import 'login_screen.dart'; +import 'home_screen.dart'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final _nicController = TextEditingController(); + final _nameController = TextEditingController(); + final _mobileController = TextEditingController(); + final _passwordController = TextEditingController(); + final _otpController = TextEditingController(); + + bool _isLoading = false; + bool _obscurePassword = true; + String _verificationId = ''; + Map? _registeredData; + + void _showToast(String message, {bool isError = false}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(isError ? Icons.error_outline : Icons.check_circle_outline, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ], + ), + backgroundColor: isError ? Colors.redAccent : Colors.green.shade600, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + margin: const EdgeInsets.all(16), + elevation: 6, + duration: const Duration(seconds: 3), + ), + ); + } + + bool _validateInputs() { + if (_nicController.text.trim().isEmpty || + _nameController.text.trim().isEmpty || + _mobileController.text.trim().isEmpty || + _passwordController.text.trim().isEmpty) { + _showToast('All fields are required!', isError: true); + return false; + } + if (_passwordController.text.trim().length < 8) { + _showToast('Password must be at least 8 characters long', isError: true); + return false; + } + return true; + } + + Future _handleRegister() async { + if (!_validateInputs()) return; + + setState(() => _isLoading = true); + try { + final deviceId = await DeviceInfoUtil.getDeviceId(); + final nic = _nicController.text.trim(); + final phone = _mobileController.text.trim(); + + _registeredData = { + "nicNo": nic, + "name": _nameController.text.trim(), + "mobilePhoneNo": phone, + "password": _passwordController.text.trim(), + "deviceId": deviceId + }; + + final success = await AuthService.registerUser(_registeredData!); + + if (success && mounted) { + await _sendOTP(phone); + } + } catch (e) { + if (mounted) { + _showToast('Registration Failed. Check NIC.', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _sendOTP(String phone) async { + await FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: phone, + verificationCompleted: (PhoneAuthCredential credential) async { + await FirebaseAuth.instance.signInWithCredential(credential); + await _verifyBackendAndLogin(); + }, + verificationFailed: (FirebaseAuthException e) { + if (mounted) { + _showToast(e.message ?? 'Verification Failed', isError: true); + setState(() => _isLoading = false); + } + }, + codeSent: (String verificationId, int? resendToken) { + setState(() { + _verificationId = verificationId; + _isLoading = false; + }); + _showOTPDialog(); + }, + codeAutoRetrievalTimeout: (String verificationId) { + _verificationId = verificationId; + }, + ); + } + + Future _verifyOTP() async { + setState(() => _isLoading = true); + try { + final credential = PhoneAuthProvider.credential( + verificationId: _verificationId, + smsCode: _otpController.text.trim(), + ); + await FirebaseAuth.instance.signInWithCredential(credential); + if (mounted) { + Navigator.pop(context); + } + await _verifyBackendAndLogin(); + } catch (e) { + if (mounted) { + _showToast('Invalid OTP', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _verifyBackendAndLogin() async { + try { + final isVerified = await AuthService.verifyRegistration(_registeredData!['nicNo']); + if (isVerified && mounted) { + _showToast('Registration & Verification Successful!'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } + } catch (e) { + if (mounted) { + _showToast('Backend Verification Failed', isError: true); + setState(() => _isLoading = false); + } + } + } + + void _showOTPDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('Enter OTP'), + content: TextField( + controller: _otpController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'OTP Code', + prefixIcon: Icon(Icons.security), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + setState(() => _isLoading = false); + }, + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: _verifyOTP, + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF1A2980), foregroundColor: Colors.white), + child: const Text('Verify'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF1A2980), Color(0xFF26D0CE)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 40.0), + child: GlassContainer( + width: double.infinity, + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Register Driver', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white), + ), + const SizedBox(height: 24), + TextField( + controller: _nicController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'NIC Number', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white54), borderRadius: BorderRadius.circular(12)), + focusedBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white), borderRadius: BorderRadius.circular(12)), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _nameController, + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.name, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Full Name', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white54), borderRadius: BorderRadius.circular(12)), + focusedBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white), borderRadius: BorderRadius.circular(12)), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _mobileController, + keyboardType: TextInputType.phone, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Mobile Number (+94...)', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white54), borderRadius: BorderRadius.circular(12)), + focusedBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white), borderRadius: BorderRadius.circular(12)), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Password', + labelStyle: const TextStyle(color: Colors.white70), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + enabledBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white54), borderRadius: BorderRadius.circular(12)), + focusedBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.white), borderRadius: BorderRadius.circular(12)), + ), + ), + const SizedBox(height: 24), + if (_isLoading) + const CircularProgressIndicator(color: Colors.white) + else + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF1A2980), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _handleRegister, + child: const Text('REGISTER', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 16), + TextButton( + onPressed: () => Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const LoginScreen()), + ), + child: const Text('Already have an account? Login', style: TextStyle(color: Colors.white70)), + ), + ], + ), + ), + ), + ), + ), + ); + } +} +/// --- splash_screen.dart --- /// +import 'package:flutter/material.dart'; +import '../utils/secure_storage.dart'; +import 'login_screen.dart'; +import 'home_screen.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + @override + void initState() { + super.initState(); + _checkAuthStatus(); + } + + Future _checkAuthStatus() async { + await Future.delayed(const Duration(seconds: 2)); + final token = await SecureStorage.getToken(); + + if (!mounted) return; + + if (token != null) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); + } + } + + @override + Widget build(BuildContext context) { + return const Scaffold( + backgroundColor: Color(0xFF1A2980), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.directions_car, size: 80, color: Colors.white), + SizedBox(height: 20), + CircularProgressIndicator(color: Colors.white), + ], + ), + ), + ); + } +} +/// --- api_service.dart --- /// +import 'package:dio/dio.dart'; +import '../utils/secure_storage.dart'; + +class ApiService { + static final Dio _dio = Dio(BaseOptions( + baseUrl: 'http://47.129.144.60:3000', + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), + )); + + static void init() { + _dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) async { + final token = await SecureStorage.getToken(); + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } + return handler.next(options); + }, + )); + } + + static Dio get dio => _dio; +} +/// --- auth_service.dart --- /// +import 'package:dio/dio.dart'; +import 'api_service.dart'; +import '../utils/secure_storage.dart'; + +class AuthService { + static Future registerUser(Map data) async { + try { + final response = await ApiService.dio.post('/auth/user/register', data: data); + return response.statusCode == 201 || response.statusCode == 200; + } on DioException { + rethrow; + } + } + + static Future verifyRegistration(String nicNo) async { + try { + final response = await ApiService.dio.post( + '/auth/user/verify-registration', + data: {'nicNo': nicNo}, + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final token = response.data['accessToken']; + await SecureStorage.saveToken(token); + return true; + } + return false; + } on DioException { + rethrow; + } + } + + static Future loginUser(String nicNo, String password, String deviceId) async { + try { + final response = await ApiService.dio.post( + '/auth/user/login', + data: { + 'nicNo': nicNo, + 'password': password, + 'deviceId': deviceId, + }, + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final token = response.data['accessToken']; + await SecureStorage.saveToken(token); + return true; + } + return false; + } on DioException { + rethrow; + } + } +} +/// --- user_service.dart --- /// +import 'package:dio/dio.dart'; +import 'api_service.dart'; + +class UserService { + static Future?> getUserProfile() async { + try { + final response = await ApiService.dio.get('/users/profile'); + if (response.statusCode == 200) { + return response.data; + } + return null; + } on DioException { + rethrow; + } + } + + static Future changePassword(String oldPassword, String newPassword) async { + try { + final response = await ApiService.dio.patch( + '/auth/user/change-password', + data: { + 'oldPassword': oldPassword, + 'newPassword': newPassword, + }, + ); + return response.statusCode == 200; + } on DioException { + rethrow; + } + } +} +/// --- device_info.dart --- /// +import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; + +class DeviceInfoUtil { + static Future getDeviceId() async { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + return androidInfo.id; + } else if (Platform.isIOS) { + final IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + return iosInfo.identifierForVendor ?? 'UNKNOWN_IOS_DEVICE'; + } + return 'UNKNOWN_DEVICE'; + } +} +/// --- secure_storage.dart --- /// +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class SecureStorage { + static const _storage = FlutterSecureStorage(); + + static Future saveToken(String token) async => + await _storage.write(key: 'jwt_token', value: token); + + static Future getToken() async => + await _storage.read(key: 'jwt_token'); + + static Future deleteToken() async => + await _storage.delete(key: 'jwt_token'); +} +/// --- settings_util.dart --- /// +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingsUtil { + static const String _biometricKey = 'biometric_enabled'; + + static Future isBiometricEnabled() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_biometricKey) ?? false; + } + + static Future setBiometricEnabled(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_biometricKey, value); + } +} +/// --- glass_container.dart --- /// +import 'dart:ui'; +import 'package:flutter/material.dart'; + +class GlassContainer extends StatelessWidget { + final Widget child; + final double? width; + final double? height; + final double borderRadius; + final EdgeInsetsGeometry padding; + + const GlassContainer({ + super.key, + required this.child, + this.width = double.infinity, + this.height, + this.borderRadius = 16.0, + this.padding = const EdgeInsets.all(16.0), + }); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: Container( + width: width, + height: height, + padding: padding, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(borderRadius), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1.5, + ), + ), + child: child, + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/analysis_options.yaml b/frontend_apps/auto_ledger/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/frontend_apps/auto_ledger/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/frontend_apps/auto_ledger/android/.gitignore b/frontend_apps/auto_ledger/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/frontend_apps/auto_ledger/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/frontend_apps/auto_ledger/android/app/build.gradle.kts b/frontend_apps/auto_ledger/android/app/build.gradle.kts new file mode 100644 index 0000000..a21262e --- /dev/null +++ b/frontend_apps/auto_ledger/android/app/build.gradle.kts @@ -0,0 +1,63 @@ +import java.io.File +import java.util.Properties + +plugins { + id("com.android.application") + id("com.google.gms.google-services") + id("kotlin-android") + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.auto_ledger" + compileSdk = 36 + ndkVersion = "27.0.12077973" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + val keystoreProperties = Properties() + val keystorePropertiesFile = rootProject.file("key.properties") + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(keystorePropertiesFile.inputStream()) + } + + defaultConfig { + applicationId = "com.auto_ledger" + minSdk = 23 + targetSdk = 36 + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + signingConfigs { + create("release") { + keyAlias = keystoreProperties.getProperty("keyAlias", "auto-ledger") + keyPassword = keystoreProperties.getProperty("keyPassword", "Venu1234@@") + storePassword = keystoreProperties.getProperty("storePassword", "Venu1234@@") + storeFile = file(keystoreProperties.getProperty("storeFile", "release-key.keystore")) + } + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("release") + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } +} + +flutter { + source = "../.." +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/android/app/src/debug/AndroidManifest.xml b/frontend_apps/auto_ledger/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/frontend_apps/auto_ledger/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/frontend_apps/auto_ledger/android/app/src/main/AndroidManifest.xml b/frontend_apps/auto_ledger/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..aa714d6 --- /dev/null +++ b/frontend_apps/auto_ledger/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend_apps/auto_ledger/android/app/src/main/kotlin/com/auto_ledger/MainActivity.kt b/frontend_apps/auto_ledger/android/app/src/main/kotlin/com/auto_ledger/MainActivity.kt new file mode 100644 index 0000000..2048ae6 --- /dev/null +++ b/frontend_apps/auto_ledger/android/app/src/main/kotlin/com/auto_ledger/MainActivity.kt @@ -0,0 +1,11 @@ +package com.auto_ledger + +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity : FlutterFragmentActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine) + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/drawable/launch_background.xml b/frontend_apps/auto_ledger/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..323045e --- /dev/null +++ b/frontend_apps/auto_ledger/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..7f68aa1 Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..3d33a45 Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..fcfe712 Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..123906d Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..e4da652 Binary files /dev/null and b/frontend_apps/auto_ledger/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/values-night/styles.xml b/frontend_apps/auto_ledger/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/frontend_apps/auto_ledger/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend_apps/auto_ledger/android/app/src/main/res/values/styles.xml b/frontend_apps/auto_ledger/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/frontend_apps/auto_ledger/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend_apps/auto_ledger/android/app/src/profile/AndroidManifest.xml b/frontend_apps/auto_ledger/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/frontend_apps/auto_ledger/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/frontend_apps/auto_ledger/android/build.gradle.kts b/frontend_apps/auto_ledger/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/frontend_apps/auto_ledger/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/frontend_apps/auto_ledger/android/gradle.properties b/frontend_apps/auto_ledger/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/frontend_apps/auto_ledger/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/frontend_apps/auto_ledger/android/gradle/wrapper/gradle-wrapper.properties b/frontend_apps/auto_ledger/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/frontend_apps/auto_ledger/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/frontend_apps/auto_ledger/android/settings.gradle.kts b/frontend_apps/auto_ledger/android/settings.gradle.kts new file mode 100644 index 0000000..bd7522f --- /dev/null +++ b/frontend_apps/auto_ledger/android/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/frontend_apps/auto_ledger/assets/emblem.png b/frontend_apps/auto_ledger/assets/emblem.png new file mode 100644 index 0000000..3775b0f Binary files /dev/null and b/frontend_apps/auto_ledger/assets/emblem.png differ diff --git a/frontend_apps/auto_ledger/assets/flag.png b/frontend_apps/auto_ledger/assets/flag.png new file mode 100644 index 0000000..ee6ee86 Binary files /dev/null and b/frontend_apps/auto_ledger/assets/flag.png differ diff --git a/frontend_apps/auto_ledger/assets/icon/app_icon.png b/frontend_apps/auto_ledger/assets/icon/app_icon.png new file mode 100644 index 0000000..fe6800c Binary files /dev/null and b/frontend_apps/auto_ledger/assets/icon/app_icon.png differ diff --git a/frontend_apps/auto_ledger/assets/punkalasa.png b/frontend_apps/auto_ledger/assets/punkalasa.png new file mode 100644 index 0000000..5712e59 Binary files /dev/null and b/frontend_apps/auto_ledger/assets/punkalasa.png differ diff --git a/frontend_apps/auto_ledger/ios/.gitignore b/frontend_apps/auto_ledger/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/frontend_apps/auto_ledger/ios/Flutter/AppFrameworkInfo.plist b/frontend_apps/auto_ledger/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/frontend_apps/auto_ledger/ios/Flutter/Debug.xcconfig b/frontend_apps/auto_ledger/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/frontend_apps/auto_ledger/ios/Flutter/Release.xcconfig b/frontend_apps/auto_ledger/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.pbxproj b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f3134a1 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend_apps/auto_ledger/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend_apps/auto_ledger/ios/Runner.xcworkspace/contents.xcworkspacedata b/frontend_apps/auto_ledger/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend_apps/auto_ledger/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend_apps/auto_ledger/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend_apps/auto_ledger/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend_apps/auto_ledger/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend_apps/auto_ledger/ios/Runner/AppDelegate.swift b/frontend_apps/auto_ledger/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d0d98aa --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..b6abdec Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..dddc22b Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..b997fd8 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..a935058 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..060b02c Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..3b08841 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..e452d8f Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..b997fd8 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..b39cb38 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..e90628c Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..50bc331 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..dd13f35 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..3e202cb Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..c493fcb Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..e90628c Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..18d1b99 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..7f68aa1 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..123906d Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..07ebf90 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..0935f07 Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..deaa8fc Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/frontend_apps/auto_ledger/ios/Runner/Base.lproj/LaunchScreen.storyboard b/frontend_apps/auto_ledger/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend_apps/auto_ledger/ios/Runner/Base.lproj/Main.storyboard b/frontend_apps/auto_ledger/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend_apps/auto_ledger/ios/Runner/Info.plist b/frontend_apps/auto_ledger/ios/Runner/Info.plist new file mode 100644 index 0000000..456a4a2 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Auto Ledger + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + auto_ledger + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/frontend_apps/auto_ledger/ios/Runner/Runner-Bridging-Header.h b/frontend_apps/auto_ledger/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/frontend_apps/auto_ledger/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/frontend_apps/auto_ledger/ios/RunnerTests/RunnerTests.swift b/frontend_apps/auto_ledger/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/frontend_apps/auto_ledger/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/frontend_apps/auto_ledger/lib/firebase_options.dart b/frontend_apps/auto_ledger/lib/firebase_options.dart new file mode 100644 index 0000000..decc1ff --- /dev/null +++ b/frontend_apps/auto_ledger/lib/firebase_options.dart @@ -0,0 +1,68 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyB22a7ZLYFaUusG_D7wzyzg6Df_Io-zLn4', + appId: '1:663237094439:android:5e6899719d6ca9b203b202', + messagingSenderId: '663237094439', + projectId: 'auto-ledger-41d16', + storageBucket: 'auto-ledger-41d16.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDzJ7AhObCD_NPpZLufbQy7zY1t2IFDukE', + appId: '1:663237094439:ios:add1d5276050008103b202', + messagingSenderId: '663237094439', + projectId: 'auto-ledger-41d16', + storageBucket: 'auto-ledger-41d16.firebasestorage.app', + iosBundleId: 'com.example.autoLedger', + ); +} diff --git a/frontend_apps/auto_ledger/lib/main.dart b/frontend_apps/auto_ledger/lib/main.dart new file mode 100644 index 0000000..f3f1089 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/main.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'firebase_options.dart'; +import 'services/api_service.dart'; +import 'screens/splash_screen.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + await dotenv.load(fileName: ".env"); + + ApiService.init(); + + runApp(const AutoLedgerApp()); +} + +class AutoLedgerApp extends StatelessWidget { + const AutoLedgerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Auto Ledger', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1A2980)), + useMaterial3: true, + textTheme: GoogleFonts.poppinsTextTheme(), + scaffoldBackgroundColor: const Color(0xFF0B0F19), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFF0B0F19), + foregroundColor: Colors.white, + elevation: 0, + ), + ), + home: const SplashScreen(), + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/screens/fines_screen.dart b/frontend_apps/auto_ledger/lib/screens/fines_screen.dart new file mode 100644 index 0000000..ed86590 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/screens/fines_screen.dart @@ -0,0 +1,1251 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:dio/dio.dart'; +import '../services/api_service.dart'; +import '../services/pdf_service.dart'; + +class FinesScreen extends StatefulWidget { + final void Function(String, IconData) onLogActivity; + final ValueChanged onSelectionModeChanged; + + const FinesScreen({ + super.key, + required this.onLogActivity, + required this.onSelectionModeChanged, + }); + + @override + State createState() => _FinesScreenState(); +} + +class _FinesScreenState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + final Set _selectedFines = {}; + + List> _pendingFines = []; + List> _paidFines = []; + + bool _isLoading = true; + String _errorMessage = ''; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + _tabController.addListener(_handleTabChange); + _fetchFines(); + } + + void _handleTabChange() { + if (!_tabController.indexIsChanging) { + final tabName = _tabController.index == 0 ? 'Pending Fines' : _tabController.index == 1 ? 'Paid Fines' : 'Points History'; + widget.onLogActivity('Viewed $tabName', Icons.tab); + if (_selectedFines.isNotEmpty) { + setState(() => _selectedFines.clear()); + widget.onSelectionModeChanged(false); + } + } + } + + Future _fetchFines() async { + setState(() { + _isLoading = true; + _errorMessage = ''; + }); + + try { + final response = await ApiService.dio.get('/fines/my-fines'); + final finesData = response.data as List; + + final List> parsedPending = []; + final List> parsedPaid = []; + + for (var f in finesData) { + double totalAmount = 0.0; + int totalPoints = 0; + List> offenseDetails = []; + + if (f['offenses'] != null) { + for (var o in f['offenses']) { + final category = o['offenceCategory']; + if (category != null) { + final amount = (category['amount'] as num?)?.toDouble() ?? 0.0; + final points = (category['points_Value'] as num?)?.toInt() ?? 0; + totalAmount += amount; + totalPoints += points; + offenseDetails.add({ + 'name': category['name'].toString(), + 'code': category['code'] ?? '', + 'points': points, + 'amount': amount, + }); + } + } + } + + final officer = f['trafficOfficer']; + final officerName = officer != null + ? '${officer['name']} (${officer['badge_No']})' + : 'Unknown Officer'; + + final mappedFine = { + 'id': f['fine_Id'], + 'date': f['issue_At'], + 'amount': totalAmount, + 'points': totalPoints, + 'offenses': offenseDetails.isNotEmpty ? offenseDetails : [{'name': 'Unknown Offense', 'code': '', 'points': 0, 'amount': 0}], + 'status': f['status'], + 'officer': officerName, + 'comment': f['comment'] ?? '', + 'dueDate': f['due_Date'], + 'payment': f['payment'], + }; + + if (f['status'] == 'PAID') { + parsedPaid.add(mappedFine); + } else { + parsedPending.add(mappedFine); + } + } + + if (mounted) { + setState(() { + _pendingFines = parsedPending; + _paidFines = parsedPaid; + _isLoading = false; + }); + } + } on DioException catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + _errorMessage = e.response?.data['message'] ?? 'Failed to load fines.'; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + _errorMessage = 'An unexpected error occurred.'; + }); + } + } + } + + Future _processPayment(List> finesToPay, double totalAmount) async { + FocusManager.instance.primaryFocus?.unfocus(); + + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => const Center(child: CircularProgressIndicator(color: Colors.white)), + ); + + try { + if (finesToPay.length == 1) { + await ApiService.dio.post('/fines/${finesToPay.first['id']}/pay', data: {'amount': totalAmount}); + } else { + final ids = finesToPay.map((f) => f['id'].toString()).toList(); + await ApiService.dio.post('/fines/pay-bulk', data: {'fineIds': ids, 'totalAmount': totalAmount}); + } + + if (!mounted) return; + Navigator.pop(context); + Navigator.pop(context); + + HapticFeedback.heavyImpact(); + widget.onLogActivity('Successfully Paid Rs. ${totalAmount.toStringAsFixed(2)}', Icons.check_circle); + + setState(() => _selectedFines.clear()); + widget.onSelectionModeChanged(false); + + showDialog( + context: context, + barrierColor: Colors.black.withAlpha(160), + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Center( + child: Material( + color: Colors.transparent, + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Container( + width: 220, + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white.withAlpha(40), Colors.white.withAlpha(15)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withAlpha(60), width: 1.0), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(20), blurRadius: 40, offset: const Offset(0, 10)) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.greenAccent.withAlpha(30), + shape: BoxShape.circle, + border: Border.all(color: Colors.greenAccent.withAlpha(60), width: 1.5), + ), + child: const Icon(Icons.check_rounded, color: Colors.greenAccent, size: 40), + ), + const SizedBox(height: 20), + const Text( + 'Payment Successful!', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 0.5), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + + Future.delayed(const Duration(milliseconds: 2000), () { + if (mounted) { + Navigator.pop(context); + _fetchFines(); + } + }); + + } catch (e) { + if (!mounted) return; + Navigator.pop(context); + _showGlassToast('Payment Failed! Please try again.', isError: true); + } + } + + void _showReceiptDialog(Map fine) { + showDialog( + context: context, + barrierColor: Colors.black.withAlpha(160), + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white.withAlpha(40), Colors.white.withAlpha(15)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withAlpha(60), width: 1.0), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.receipt_long_rounded, color: Colors.white, size: 50), + const SizedBox(height: 16), + const Text( + 'Download Receipt', + style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + Text( + 'Fine #${_formatId(fine['id'])}', + style: const TextStyle(color: Colors.white60, fontSize: 14), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel', style: TextStyle(color: Colors.white70)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(40), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.white.withAlpha(60)), + ), + ), + onPressed: () { + Navigator.pop(context); + PdfService.generateAndPrintReceipt(fine); + }, + child: const Text('Download', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } + + void _showGlassToast(String message, {bool isError = false}) { + final topPadding = MediaQuery.of(context).padding.top; + final entry = OverlayEntry( + builder: (context) => Positioned( + top: topPadding + 10, + left: 20, + right: 20, + child: Material( + color: Colors.transparent, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: isError ? Colors.redAccent.withAlpha(50) : Colors.green.shade600.withAlpha(50), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withAlpha(100), width: 1.0), + ), + child: Row( + children: [ + Icon(isError ? Icons.error_outline_rounded : Icons.check_circle_outline_rounded, color: Colors.white, size: 28), + const SizedBox(width: 12), + Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ], + ), + ), + ), + ), + ), + ), + ); + Navigator.of(context, rootNavigator: true).overlay?.insert(entry); + Future.delayed(const Duration(seconds: 3), () => entry.remove()); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + String _formatDate(String isoString) { + try { + final date = DateTime.parse(isoString); + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } catch (_) { + return '---'; + } + } + + String _formatDateTime(String isoString) { + try { + final date = DateTime.parse(isoString); + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; + } catch (_) { + return '---'; + } + } + + String _formatId(String uuid) { + return uuid.length >= 8 ? '#${uuid.substring(0, 8).toUpperCase()}' : uuid; + } + + void _toggleSelection(String id) { + HapticFeedback.selectionClick(); + final wasEmpty = _selectedFines.isEmpty; + setState(() { + if (!_selectedFines.remove(id)) { + _selectedFines.add(id); + widget.onLogActivity('Selected Fine ${_formatId(id)}', Icons.touch_app_rounded); + } else { + widget.onLogActivity('Deselected Fine ${_formatId(id)}', Icons.touch_app_rounded); + } + }); + if (wasEmpty != _selectedFines.isEmpty) { + widget.onSelectionModeChanged(_selectedFines.isNotEmpty); + } + } + + double _calculateTotalSelectedAmount() { + return _pendingFines + .where((fine) => _selectedFines.contains(fine['id'])) + .fold(0, (sum, fine) => sum + fine['amount']); + } + + void _showPaymentDialog(List> finesToPay) { + final double totalAmount = finesToPay.fold(0, (sum, item) => sum + item['amount']); + final bool isBulk = finesToPay.length > 1; + + widget.onLogActivity('Initiated ${isBulk ? 'Bulk ' : ''}Payment', Icons.payment); + + showDialog( + context: context, + barrierColor: Colors.black.withAlpha(160), + builder: (BuildContext context) { + bool isCvvObscured = true; + + return StatefulBuilder( + builder: (context, setModalState) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + insetPadding: const EdgeInsets.symmetric(horizontal: 24), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white.withAlpha(40), Colors.white.withAlpha(15)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withAlpha(60), width: 1.0), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(30), blurRadius: 40, offset: const Offset(0, 10)), + ], + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.payment_rounded, size: 40, color: Colors.white), + const SizedBox(height: 12), + Text( + isBulk ? 'Bulk Payment' : 'Pay Fine', + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white), + ), + const SizedBox(height: 20), + + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withAlpha(15), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withAlpha(40), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isBulk ? 'Bulk Payment' : 'Pay Fine', + style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.w900, fontSize: 14), + ), + const SizedBox(height: 2), + Text( + isBulk ? '${finesToPay.length} Fines Selected' : 'ID: ${_formatId(finesToPay.first['id'])}', + style: const TextStyle(color: Colors.white60, fontWeight: FontWeight.w600, fontSize: 11), + ), + ], + ), + Text( + 'Rs. ${totalAmount.toStringAsFixed(2)}', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: Colors.redAccent), + ), + ], + ), + if (finesToPay.length == 1 && finesToPay.first['offenses'] != null) ...[ + const SizedBox(height: 12), + ...List.generate(finesToPay.first['offenses'].length, (index) { + final offense = finesToPay.first['offenses'][index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Text(offense['code'] ?? '', style: TextStyle(color: Colors.white60, fontSize: 11, fontWeight: FontWeight.w700)), + const SizedBox(width: 8), + Expanded(child: Text(offense['name'], style: const TextStyle(color: Colors.white70, fontSize: 12))), + Text('+${offense['points']} pts', style: TextStyle(color: Colors.orangeAccent, fontSize: 11, fontWeight: FontWeight.bold)), + ], + ), + ); + }), + ], + + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: List.generate( + 30, + (index) => Expanded(child: Container(height: 1.2, color: index % 2 == 0 ? Colors.white.withAlpha(40) : Colors.transparent)) + ), + ), + ), + + _buildPaymentTextField( + 'Card Number', + Icons.credit_card_rounded, + true, + fontSize: 15, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(16), + _CardNumberFormatter(), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildPaymentTextField( + 'MM/YY', + Icons.calendar_today_rounded, + true, + inputFormatters: [ + _ExpiryDateFormatter(), + LengthLimitingTextInputFormatter(5), + ], + ) + ), + const SizedBox(width: 12), + Expanded( + child: _buildPaymentTextField( + 'CVV', + Icons.lock_outline_rounded, + true, + isObscure: isCvvObscured, + suffixIcon: IconButton( + icon: Icon( + isCvvObscured ? Icons.visibility_off_rounded : Icons.visibility_rounded, + color: Colors.white70, + size: 18, + ), + onPressed: () { + setModalState(() { + isCvvObscured = !isCvvObscured; + }); + }, + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(4), + ], + ) + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 20), + + Row( + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + onPressed: () => Navigator.pop(context), + child: const Text('Cancel', style: TextStyle(color: Colors.white70, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + backgroundColor: Colors.white.withAlpha(40), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: Colors.white.withAlpha(60))), + ), + onPressed: () => _processPayment(finesToPay, totalAmount), + child: const Text('CONFIRM', style: TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.5)), + ), + ), + ], + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ); + } + + Widget _buildPaymentTextField(String label, IconData icon, bool isNumber, {bool isObscure = false, List? inputFormatters, Widget? suffixIcon, double? fontSize}) { + return Container( + decoration: BoxDecoration( + color: Colors.white.withAlpha(15), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.white.withAlpha(40), width: 1.0), + ), + child: TextField( + keyboardType: isNumber ? TextInputType.number : TextInputType.text, + obscureText: isObscure, + inputFormatters: inputFormatters, + style: TextStyle(fontWeight: FontWeight.w800, color: Colors.white, fontSize: fontSize ?? 15), + decoration: InputDecoration( + isDense: true, + labelText: label, + labelStyle: const TextStyle(color: Colors.white60, fontWeight: FontWeight.w600, fontSize: 12), + prefixIcon: Icon(icon, color: Colors.white70, size: 18), + suffixIcon: suffixIcon, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + ), + ), + ); + } + + Widget _buildGlassBackground() { + return RepaintBoundary( + child: Stack( + children: [ + Container(color: const Color(0xFF0B0F19)), + Positioned(top: -50, left: -50, child: Container(width: 300, height: 300, decoration: BoxDecoration(color: const Color(0xFF1E3A8A).withAlpha(140), shape: BoxShape.circle))), + Positioned(bottom: 50, right: -100, child: Container(width: 350, height: 350, decoration: BoxDecoration(color: Colors.teal.shade900.withAlpha(120), shape: BoxShape.circle))), + Positioned(top: 250, right: 20, child: Container(width: 200, height: 200, decoration: BoxDecoration(color: Colors.purpleAccent.withAlpha(50), shape: BoxShape.circle))), + Positioned.fill(child: BackdropFilter(filter: ImageFilter.blur(sigmaX: 75, sigmaY: 75), child: Container(color: Colors.transparent))), + ], + ), + ); + } + + Widget _buildFineCard(Map fine, bool isPending) { + final isSelected = _selectedFines.contains(fine['id']); + final isOverdue = fine['dueDate'] != null && DateTime.parse(fine['dueDate']).isBefore(DateTime.now()); + + final gradientColors = isSelected + ? [const Color(0xFF1A2980).withAlpha(45), const Color(0xFF1A2980).withAlpha(15)] + : [Colors.white.withAlpha(20), Colors.white.withAlpha(6)]; + + final borderColor = isSelected + ? const Color(0xFF1A2980).withAlpha(150) + : Colors.white.withAlpha(40); + + return GestureDetector( + onTap: isPending ? () => _toggleSelection(fine['id']) : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(bottom: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 55, sigmaY: 55), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient(colors: gradientColors, begin: Alignment.topLeft, end: Alignment.bottomRight), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: borderColor, width: isSelected ? 2.0 : 1.0), + boxShadow: [BoxShadow(color: Colors.black.withAlpha(25), blurRadius: 25, offset: const Offset(0, 8))], + ), + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (isPending) ...[ + Icon(isSelected ? Icons.check_circle : Icons.radio_button_unchecked, color: isSelected ? Colors.white : Colors.white60, size: 22), + const SizedBox(width: 10), + ], + Text(_formatId(fine['id']), style: const TextStyle(fontWeight: FontWeight.w900, color: Colors.white, fontSize: 16, letterSpacing: 1.0)), + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isPending ? Colors.red.withAlpha(30) : Colors.green.withAlpha(30), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isPending + ? (isOverdue ? Colors.red.shade400.withAlpha(80) : Colors.red.withAlpha(80)) + : Colors.green.withAlpha(80) + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + fine['status'], + style: TextStyle( + color: isPending + ? (isOverdue ? Colors.red.shade400 : Colors.red.shade300) + : Colors.green.shade300, + fontWeight: FontWeight.bold, + fontSize: 10, + letterSpacing: 0.5, + ), + ), + if (isOverdue && isPending) ...[ + const SizedBox(width: 4), + const Icon( + Icons.warning_rounded, + color: Colors.redAccent, + size: 12, + ), + ], + ], + ), + ), + ], + ), + const SizedBox(height: 16), + + // Issue Date + Row( + children: [ + Icon(Icons.calendar_month_rounded, size: 14, color: Colors.white60), + const SizedBox(width: 8), + Text(_formatDate(fine['date']), style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.w600, fontSize: 13)), + ], + ), + const SizedBox(height: 8), + + // Due Date + if (fine['dueDate'] != null) ...[ + Row( + children: [ + Icon(Icons.event_note, size: 14, color: isOverdue ? Colors.red.shade300 : Colors.white60), + const SizedBox(width: 8), + Text( + 'Due: ${_formatDate(fine['dueDate'])}', + style: TextStyle( + color: isOverdue ? Colors.red.shade300 : Colors.white60, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 8), + ], + + // Officer + Row( + children: [ + Icon(Icons.person_outline_rounded, size: 14, color: Colors.white60), + const SizedBox(width: 8), + Expanded( + child: Text( + fine['officer'], + style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.w600, fontSize: 13), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + + // Comment + if (fine['comment'] != null && fine['comment'].isNotEmpty) ...[ + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.comment_outlined, size: 14, color: Colors.white60), + const SizedBox(width: 8), + Expanded( + child: Text( + fine['comment'], + style: const TextStyle(color: Colors.white54, fontWeight: FontWeight.w500, fontSize: 12, fontStyle: FontStyle.italic), + ), + ), + ], + ), + ], + + // Payment Info (for PAID fines) + if (!isPending && fine['payment'] != null) ...[ + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.payment, size: 14, color: Colors.green.shade300), + const SizedBox(width: 8), + Text( + 'Paid: ${_formatDateTime(fine['payment']['paidTime'])}', + style: TextStyle(color: Colors.green.shade300, fontSize: 12, fontWeight: FontWeight.w600), + ), + ], + ), + ], + + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Container(height: 1, color: Colors.white.withAlpha(40)), + ), + + // Offenses Header + const Text('OFFENSES', style: TextStyle(fontSize: 10, color: Colors.white60, fontWeight: FontWeight.w900, letterSpacing: 1.0)), + const SizedBox(height: 8), + + // Offenses List + ...List.generate(fine['offenses'].length, (index) { + final offense = fine['offenses'][index]; + return Padding( + padding: const EdgeInsets.only(bottom: 6.0), + child: Row( + children: [ + const Icon(Icons.warning_amber_rounded, size: 14, color: Colors.orangeAccent), + const SizedBox(width: 8), + Expanded( + child: Row( + children: [ + if (offense['code'] != null && offense['code'].isNotEmpty) ...[ + Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: Colors.blueGrey.shade800.withAlpha(50), + borderRadius: BorderRadius.circular(3), + ), + child: Text( + offense['code'], + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: Colors.blueGrey.shade300, + ), + ), + ), + const SizedBox(width: 6), + ], + Expanded( + child: Text( + offense['name'], + style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white70, fontSize: 13), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + if (offense['points'] > 0) + Text( + '+${offense['points']} pts', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: Colors.orangeAccent.withAlpha(180), + ), + ), + ], + ), + ); + }), + + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('TOTAL AMOUNT', style: TextStyle(fontSize: 10, color: Colors.white60, fontWeight: FontWeight.w900, letterSpacing: 1.0)), + const SizedBox(height: 2), + Text( + 'Rs. ${fine['amount'].toStringAsFixed(2)}', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: isPending + ? (isOverdue ? Colors.red.shade400 : Colors.redAccent) + : Colors.teal.shade300, + ), + ), + if (fine['points'] > 0) + Text( + '+${fine['points']} points', + style: TextStyle( + fontSize: 11, + color: Colors.orangeAccent.withAlpha(150), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + if (isPending && !isSelected) + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(25), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + elevation: 0, + side: BorderSide(color: Colors.white.withAlpha(60), width: 1.0), + ), + onPressed: () { + HapticFeedback.lightImpact(); + _showPaymentDialog([fine]); + }, + child: const Text('PAY NOW', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 12)), + ), + if (!isPending) + TextButton.icon( + onPressed: () => _showReceiptDialog(fine), + icon: const Icon(Icons.picture_as_pdf, color: Colors.white60, size: 16), + label: const Text( + 'Receipt', + style: TextStyle(color: Colors.white60, fontSize: 11), + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildBulkPaymentBar() { + final total = _selectedFines.isEmpty ? 0.0 : _calculateTotalSelectedAmount(); + final finesToPay = _pendingFines.where((f) => _selectedFines.contains(f['id'])).toList(); + + return IgnorePointer( + ignoring: _selectedFines.isEmpty, + child: AnimatedSlide( + offset: _selectedFines.isEmpty ? const Offset(0, 2.0) : Offset.zero, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: Container( + margin: const EdgeInsets.only(bottom: 25, left: 24, right: 24), + height: 65, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(35), + boxShadow: [BoxShadow(color: Colors.black.withAlpha(30), blurRadius: 20, offset: const Offset(0, 10))], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(35), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF1A2980).withAlpha(150), + borderRadius: BorderRadius.circular(35), + border: Border.all(color: Colors.white.withAlpha(60), width: 1.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('${_selectedFines.length} Selected', style: const TextStyle(color: Colors.white60, fontSize: 11, fontWeight: FontWeight.w600)), + Text('Rs. ${total.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold)), + ], + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(40), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withAlpha(60))), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 0), + elevation: 0, + minimumSize: const Size(0, 36), + ), + onPressed: () => _showPaymentDialog(finesToPay), + child: const Text('PAY SELECTED', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 0.5)), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildPointsHistoryTab() { + final allFines = [..._pendingFines, ..._paidFines] + ..sort((a, b) => DateTime.parse(b['date']).compareTo(DateTime.parse(a['date']))); + + if (allFines.isEmpty) { + return const Center( + child: Text( + 'No points history available.', + style: TextStyle(color: Colors.white70, fontWeight: FontWeight.w500), + ), + ); + } + + int totalPoints = 0; + final List> history = []; + + for (var fine in allFines) { + final points = (fine['points'] as int?) ?? 0; + totalPoints += points; + history.add({ + 'date': fine['date'], + 'points': points, + 'cumulative': totalPoints, + 'status': fine['status'], + 'id': fine['id'], + 'officer': fine['officer'], + }); + } + + return ListView.builder( + padding: const EdgeInsets.only(top: kToolbarHeight + kTextTabBarHeight + 40, left: 16, right: 16, bottom: 100), + itemCount: history.length, + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final item = history[index]; + final isPending = item['status'] == 'PENDING'; + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withAlpha(isPending ? 15 : 8), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isPending ? Colors.white.withAlpha(40) : Colors.white.withAlpha(20), + width: 1.0, + ), + ), + child: Row( + children: [ + Container( + width: 4, + height: 40, + decoration: BoxDecoration( + color: isPending ? Colors.orangeAccent : Colors.greenAccent, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _formatDate(item['date']), + style: const TextStyle(color: Colors.white70, fontSize: 12), + ), + Text( + '#${_formatId(item['id'])}', + style: const TextStyle(color: Colors.white60, fontSize: 10), + ), + Text( + item['officer'] ?? '', + style: const TextStyle(color: Colors.white54, fontSize: 10), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + item['status'], + style: TextStyle( + color: isPending ? Colors.orangeAccent : Colors.greenAccent, + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '+${item['points']}', + style: TextStyle( + color: isPending ? Colors.orangeAccent : Colors.greenAccent, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'Total: ${item['cumulative']}', + style: const TextStyle(color: Colors.white60, fontSize: 11), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _buildGlassBackground(), + Scaffold( + backgroundColor: Colors.transparent, + extendBodyBehindAppBar: true, + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: const Color(0xFF0B0F19).withAlpha(120), + flexibleSpace: ClipRect(child: BackdropFilter(filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), child: Container(color: Colors.transparent))), + title: const Text('Traffic Fines', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + bottom: TabBar( + controller: _tabController, + labelColor: Colors.cyanAccent, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.cyanAccent, + indicatorWeight: 3, + tabs: const [ + Tab(text: 'PENDING'), + Tab(text: 'PAID'), + Tab(text: 'POINTS'), + ], + ), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator(color: Colors.white)) + : _errorMessage.isNotEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.white, size: 50), + const SizedBox(height: 16), + Text(_errorMessage, style: const TextStyle(color: Colors.white, fontSize: 16)), + const SizedBox(height: 16), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.white.withAlpha(30)), + onPressed: _fetchFines, + child: const Text('Retry', style: TextStyle(color: Colors.white)) + ) + ], + ), + ) + : Stack( + children: [ + TabBarView( + controller: _tabController, + children: [ + // Pending Fines + _pendingFines.isEmpty + ? const Center(child: Text("No pending fines available.", style: TextStyle(color: Colors.white70, fontWeight: FontWeight.w500))) + : ListView.builder( + padding: const EdgeInsets.only(top: kToolbarHeight + kTextTabBarHeight + 40, left: 16, right: 16, bottom: 100), + itemCount: _pendingFines.length, + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) => _buildFineCard(_pendingFines[index], true), + ), + // Paid Fines + _paidFines.isEmpty + ? const Center(child: Text("No paid fines history.", style: TextStyle(color: Colors.white70, fontWeight: FontWeight.w500))) + : ListView.builder( + padding: const EdgeInsets.only(top: kToolbarHeight + kTextTabBarHeight + 40, left: 16, right: 16, bottom: 100), + itemCount: _paidFines.length, + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) => _buildFineCard(_paidFines[index], false), + ), + // Points History + _buildPointsHistoryTab(), + ], + ), + Align( + alignment: Alignment.bottomCenter, + child: _buildBulkPaymentBar(), + ), + ], + ), + ), + ], + ); + } +} + +class _CardNumberFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + final text = newValue.text.replaceAll(RegExp(r'\s+'), ''); + final buffer = StringBuffer(); + for (int i = 0; i < text.length; i++) { + buffer.write(text[i]); + if ((i + 1) % 4 == 0 && i != text.length - 1) { + buffer.write(' '); + } + } + final string = buffer.toString(); + return TextEditingValue( + text: string, + selection: TextSelection.collapsed(offset: string.length), + ); + } +} + +class _ExpiryDateFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + if (newValue.text.length < oldValue.text.length) { + return newValue; + } + String text = newValue.text.replaceAll(RegExp(r'[^0-9]'), ''); + if (text.isEmpty) return newValue; + + if (text.length == 1 && int.parse(text) > 1) { + text = '0$text'; + } + + if (text.length >= 2) { + int month = int.parse(text.substring(0, 2)); + if (month < 1 || month > 12) { + return oldValue; + } + } + + final buffer = StringBuffer(); + for (int i = 0; i < text.length; i++) { + buffer.write(text[i]); + if (i == 1 && text.length > 2) { + buffer.write('/'); + } else if (i == 1 && text.length == 2) { + buffer.write('/'); + } + } + + final string = buffer.toString(); + return TextEditingValue( + text: string, + selection: TextSelection.collapsed(offset: string.length), + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/screens/home_screen.dart b/frontend_apps/auto_ledger/lib/screens/home_screen.dart new file mode 100644 index 0000000..bef99b0 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/screens/home_screen.dart @@ -0,0 +1,800 @@ +import 'dart:async'; +import 'dart:ui'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import '../services/api_service.dart'; +import '../utils/secure_storage.dart'; +import 'login_screen.dart'; +import 'fines_screen.dart'; +import 'profile_screen.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + int _currentIndex = 0; + bool _isFront = true; + bool _isSelectionMode = false; + + Map? _licenseData; + bool _isLoading = true; + String _errorMessage = ''; + bool _hasShownPointsWarning = false; + final List> _recentActivities = []; + + @override + void initState() { + super.initState(); + _fetchLicenseData(); + } + + void _addRecentActivity(String title, IconData icon) { + if (!mounted) return; + setState(() { + _recentActivities.insert(0, { + 'title': title, + 'date': _formatDate(DateTime.now().toIso8601String()), + 'icon': icon, + }); + }); + } + + Future _fetchLicenseData() async { + try { + final response = await ApiService.dio.get('/license/my-license'); + if (!mounted) return; + + setState(() { + _licenseData = Map.from(response.data); + _isLoading = false; + }); + + if (!_hasShownPointsWarning) { + final points = _licenseData?['points'] ?? 0; + final bool isApproachingSuspension = (points >= 20 && points <= 23) || + (points >= 45 && points <= 49) || + (points >= 80 && points <= 99); + + if (isApproachingSuspension) { + _hasShownPointsWarning = true; + Future.microtask(() => _showPointsWarning(points)); + } + } + } on DioException catch (e) { + if (!mounted) return; + setState(() { + _errorMessage = e.response?.data['message'] ?? 'Failed to load license details.'; + _isLoading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _errorMessage = 'An unexpected error occurred.'; + _isLoading = false; + }); + } + } + + void _showPointsWarning(int points) { + Color warningColor; + IconData warningIcon; + String warningTitle; + String warningMessage; + + if (points >= 80 && points <= 99) { + warningColor = Colors.red.shade400; + warningIcon = Icons.warning_rounded; + warningTitle = 'CRITICAL RISK'; + warningMessage = 'You have high demerit points ($points points). Reach 100 points and your license will be permanently revoked.'; + } else if (points >= 45 && points <= 49) { + warningColor = Colors.orange.shade400; + warningIcon = Icons.warning_rounded; + warningTitle = 'SEVERE RISK'; + warningMessage = 'You have high demerit points ($points points). Reach 50 points and your license will be suspended.'; + } else { + warningColor = Colors.amber.shade400; + warningIcon = Icons.info_outline_rounded; + warningTitle = 'WARNING'; + warningMessage = 'You have high demerit points ($points points). Reach 24 points and your license will be suspended.'; + } + + showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.black.withAlpha(160), + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white.withAlpha(40), Colors.white.withAlpha(15)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(25), + border: Border.all(color: Colors.white.withAlpha(80), width: 1.0), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(color: warningColor.withAlpha(30), shape: BoxShape.circle), + child: Icon(warningIcon, size: 40, color: warningColor), + ), + const SizedBox(height: 20), + Text(warningTitle, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: warningColor)), + const SizedBox(height: 15), + Text(warningMessage, textAlign: TextAlign.center, style: const TextStyle(fontSize: 15, color: Colors.white70, fontWeight: FontWeight.w500)), + const SizedBox(height: 25), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(40), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15), side: BorderSide(color: Colors.white.withAlpha(60))), + elevation: 0, + ), + onPressed: () { + HapticFeedback.lightImpact(); + Navigator.of(context).pop(); + }, + child: const Text('I Understand', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + void _showTemporaryLicenseSheet(Map tempLicense) { + _addRecentActivity('Viewed Temporary License', Icons.assignment_late_outlined); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white.withAlpha(50), Colors.white.withAlpha(20)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: const BorderRadius.only(topLeft: Radius.circular(35), topRight: Radius.circular(35)), + border: Border.all(color: Colors.white.withAlpha(60), width: 1.0), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: Container(width: 50, height: 5, decoration: BoxDecoration(color: Colors.white.withAlpha(60), borderRadius: BorderRadius.circular(10)))), + const SizedBox(height: 20), + const Row( + children: [ + Icon(Icons.assignment_late_outlined, color: Colors.white, size: 28), + SizedBox(width: 10), + Text('Temporary License', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white)), + ], + ), + const SizedBox(height: 24), + _buildTempInfoRow('Issued Date', _formatDate(tempLicense['issue_Date'])), + Divider(height: 20, color: Colors.white.withAlpha(20)), + _buildTempInfoRow('Valid Until', _formatDate(tempLicense['expiry_Date']), isHighlight: true), + Divider(height: 20, color: Colors.white.withAlpha(20)), + _buildTempInfoRow('Issued By (Officer)', tempLicense['issued_By'] ?? 'Unknown'), + const SizedBox(height: 30), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(30), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15), side: BorderSide(color: Colors.white.withAlpha(60))), + ), + onPressed: () { + HapticFeedback.mediumImpact(); + Navigator.pop(context); + }, + child: const Text('Close', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ); + }, + ); + } + + Widget _buildTempInfoRow(String title, String value, {bool isHighlight = false}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: const TextStyle(fontSize: 15, color: Colors.white70, fontWeight: FontWeight.w600)), + Text(value, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: isHighlight ? Colors.redAccent : Colors.white)), + ], + ); + } + + Future _generateQR() async { + showDialog(context: context, barrierDismissible: false, builder: (context) => const Center(child: CircularProgressIndicator(color: Colors.white))); + try { + final response = await ApiService.dio.get('/license/generate-qr'); + final String token = response.data['qrToken']; + if (mounted) Navigator.pop(context); + _addRecentActivity('Generated QR Code', Icons.qr_code_scanner); + if (mounted) showDialog(context: context, barrierDismissible: false, builder: (BuildContext context) => _QRDialog(qrToken: token)); + } catch (e) { + if (mounted) Navigator.pop(context); + _showGlassToast('Failed to generate QR Code.', isError: true); + } + } + + void _showGlassToast(String message, {bool isError = false}) { + final topPadding = MediaQuery.of(context).padding.top; + final entry = OverlayEntry( + builder: (context) => Positioned( + top: topPadding + 10, + left: 20, + right: 20, + child: Material( + color: Colors.transparent, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: isError ? Colors.redAccent.withAlpha(50) : Colors.green.shade600.withAlpha(50), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withAlpha(100), width: 1.0), + ), + child: Row( + children: [ + Icon(isError ? Icons.error_outline_rounded : Icons.check_circle_outline_rounded, color: Colors.white, size: 28), + const SizedBox(width: 12), + Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ], + ), + ), + ), + ), + ), + ), + ); + Navigator.of(context, rootNavigator: true).overlay?.insert(entry); + Future.delayed(const Duration(seconds: 3), () => entry.remove()); + } + + Future _logout() async { + await SecureStorage.deleteToken(); + if (mounted) Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const LoginScreen())); + } + + String _formatDate(String? isoString) { + if (isoString == null || isoString.isEmpty) return '---'; + try { + final date = DateTime.parse(isoString); + return '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}.${date.year}'; + } catch (e) { + return '---'; + } + } + + Widget _buildDetailText(String number, String value, {bool isBold = false}) { + return Text.rich( + TextSpan( + children: [ + TextSpan(text: number, style: TextStyle(fontSize: 8.5, color: Colors.blueGrey[200], fontWeight: FontWeight.bold)), + TextSpan(text: value, style: TextStyle(fontSize: 9.5, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: Colors.white)), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } + + Widget _buildFrontCard() { + final String name = _licenseData?['full_Name'] ?? 'N/A'; + final String address = _licenseData?['address'] ?? 'N/A'; + final String nicNo = _licenseData?['nic_No'] ?? 'N/A'; + final String licenseNo = _licenseData?['license_No'] ?? 'N/A'; + final String dob = _formatDate(_licenseData?['date_of_birth']); + final String bloodGroup = _licenseData?['blood_Group'] ?? '-'; + final String issueDate = _formatDate(_licenseData?['issue_Date']); + final String status = _licenseData?['status'] ?? 'UNKNOWN'; + final String? imageUrl = _licenseData?['image']; + final screenWidth = MediaQuery.of(context).size.width; + + return _buildLicenseGlassCard( + padding: EdgeInsets.zero, + child: SizedBox( + width: double.infinity, + height: screenWidth * 0.58, + child: Stack( + children: [ + Positioned.fill(child: Center(child: Opacity(opacity: 0.05, child: Image.asset('assets/emblem.png', width: screenWidth * 0.35, color: Colors.white, colorBlendMode: BlendMode.srcIn, errorBuilder: (c, e, s) => const SizedBox())))), + Positioned(bottom: 18, right: 48, child: Opacity(opacity: 0.10, child: Image.asset('assets/punkalasa.png', width: screenWidth * 0.15, color: Colors.white, colorBlendMode: BlendMode.srcIn, errorBuilder: (c, e, s) => Icon(Icons.security, size: screenWidth * 0.13, color: Colors.white)))), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container(decoration: BoxDecoration(border: Border.all(color: Colors.white24, width: 0.5)), child: Image.asset('assets/flag.png', width: screenWidth * 0.09, height: screenWidth * 0.055, fit: BoxFit.cover, errorBuilder: (c, e, s) => Container(width: screenWidth * 0.09, height: screenWidth * 0.055, color: Colors.grey[800]))), + const SizedBox(width: 8), + Expanded(child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [const Text('DRIVING LICENCE', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 1.0)), Container(margin: const EdgeInsets.symmetric(vertical: 2), height: 1.0, width: double.infinity, color: Colors.white12), const Text('DEMOCRATIC SOCIALIST REPUBLIC OF SRI LANKA', textAlign: TextAlign.center, style: TextStyle(fontSize: 8.5, fontWeight: FontWeight.w900, color: Colors.white70))])), + const SizedBox(width: 8), + Image.asset('assets/emblem.png', width: screenWidth * 0.08, height: screenWidth * 0.1, errorBuilder: (c, e, s) => SizedBox(width: screenWidth * 0.08, height: screenWidth * 0.1)), + ], + ), + const SizedBox(height: 10), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8.0, left: 10.0), + child: Column( + children: [ + Container(width: screenWidth * 0.18, height: screenWidth * 0.22, decoration: const BoxDecoration(color: Colors.transparent), child: imageUrl != null && imageUrl.isNotEmpty ? Image.network(imageUrl, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => const Icon(Icons.person, size: 40, color: Colors.white60)) : const Icon(Icons.person, size: 40, color: Colors.white60)), + const SizedBox(height: 4), + Text('4a. $issueDate', style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Colors.white70)), + const SizedBox(height: 12), + Builder( + builder: (context) { + List statusGradient; + if (status == 'ACTIVE') { statusGradient = [const Color(0xFF00b09b), const Color(0xFF96c93d)]; } else if (status == 'SUSPENDED') { statusGradient = [const Color(0xFFf12711), const Color(0xFFf5af19)]; } else { statusGradient = [const Color(0xFFcb2d3e), const Color(0xFFef473a)]; } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(gradient: LinearGradient(colors: statusGradient.map((c) => c.withAlpha(200)).toList()), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white24)), + child: Row(mainAxisSize: MainAxisSize.min, children: [Text(status, style: const TextStyle(color: Colors.white, fontSize: 9.5, fontWeight: FontWeight.w900, letterSpacing: 0.8))]), + ); + }, + ), + ], + ), + ), + const SizedBox(width: 15), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 6.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [Expanded(child: _buildDetailText('5. ', licenseNo, isBold: true)), const SizedBox(width: 8), Expanded(child: _buildDetailText('4c. ', nicNo))]), + const SizedBox(height: 8), + _buildDetailText('1, 2. ', name), const SizedBox(height: 8), _buildDetailText('8. ', address), const SizedBox(height: 8), _buildDetailText('3. ', dob), + const Spacer(), + Row(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [const Text('Blood Group ', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white70)), Text(bloodGroup, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white)), const Spacer(), Text('SL', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: Colors.purple.shade300))]), + const SizedBox(height: 6), + ], + ), + ), + ), + ], + ), + ) + ], + ), + ), + ], + ), + ), + ); + } + + TableRow _buildCategoryRow(String code, String icon) { + final categories = _licenseData?['vehicleCategories'] as List? ?? []; + final cat = categories.cast>().firstWhere((c) => c['vehicle_Class'] == code, orElse: () => {}); + if (cat.isNotEmpty) return _buildTableRow('$code $icon', _formatDate(cat['issue_Date']), _formatDate(cat['expiry_Date']), cat['restriction'] ?? '---'); + return _buildTableRow('$code $icon', '---', '---', '---'); + } + + Widget _buildLegendText(String text) => Padding(padding: const EdgeInsets.only(bottom: 2.5), child: Text(text, style: TextStyle(fontSize: 6.0, color: Colors.blueGrey[300], fontWeight: FontWeight.w600, height: 1.0))); + + TableRow _buildTableRow(String col1, String col2, String col3, String restriction, {bool isHeader = false}) { + return TableRow( + decoration: BoxDecoration(color: isHeader ? Colors.white.withAlpha(20) : Colors.transparent), + children: [ + Padding(padding: const EdgeInsets.all(1.0), child: Text(col1, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal, color: Colors.white))), + Padding(padding: const EdgeInsets.all(1.0), child: Text(col2, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal, color: Colors.white70))), + Padding(padding: const EdgeInsets.all(1.0), child: Text(col3, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal, color: Colors.white70))), + Padding(padding: const EdgeInsets.all(1.0), child: Text(restriction, textAlign: TextAlign.center, style: TextStyle(fontSize: isHeader ? 7.5 : 8.5, fontWeight: isHeader ? FontWeight.bold : FontWeight.normal, color: Colors.white60))), + ], + ); + } + + Widget _buildBackCard() { + final screenWidth = MediaQuery.of(context).size.width; + return _buildLicenseGlassCard( + padding: EdgeInsets.zero, + child: SizedBox( + width: double.infinity, + height: screenWidth * 0.58, + child: Stack( + children: [ + Positioned.fill(child: Center(child: Opacity(opacity: 0.03, child: Image.asset('assets/emblem.png', width: screenWidth * 0.35, color: Colors.white, colorBlendMode: BlendMode.srcIn, errorBuilder: (c, e, s) => const SizedBox())))), + Positioned(left: 6, top: 15, bottom: 15, child: Center(child: RotatedBox(quarterTurns: 3, child: Text('Department of Motor Traffic - Sri Lanka', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.blueGrey[300]))))), + Padding( + padding: const EdgeInsets.only(left: 28, right: 16, top: 12, bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(flex: 4, child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [_buildLegendText('1. Surname'), _buildLegendText('2. Other names'), _buildLegendText('3. Date of birth'), _buildLegendText('4a. Date of Issue of the License'), _buildLegendText('4b. Issuing Authority'), _buildLegendText('4c. Administrative Number'), _buildLegendText('5. Number of the LICENCE'), _buildLegendText('7. Signature of the holder'), _buildLegendText('8. Permanent place of residence'), _buildLegendText('9. Categories of vehicles'), _buildLegendText('10. Date of Issue per category'), _buildLegendText('11. Date of Expiry per category'), _buildLegendText('12. Restrictions in code form')])), + const SizedBox(width: 6), + Expanded( + flex: 7, + child: Align( + alignment: Alignment.centerLeft, + child: Table( + border: TableBorder.all(color: Colors.white.withAlpha(40), width: 0.5), + columnWidths: const {0: FlexColumnWidth(1.2), 1: FlexColumnWidth(2.2), 2: FlexColumnWidth(2.2), 3: FlexColumnWidth(1.2)}, + children: [_buildTableRow('9.', '10.', '11.', '12.', isHeader: true), _buildCategoryRow('A1', '🛺'), _buildCategoryRow('A', '🏍️'), _buildCategoryRow('B1', '🛺'), _buildCategoryRow('B', '🚗'), _buildCategoryRow('C1', '🚚'), _buildCategoryRow('C', '🚛'), _buildCategoryRow('CE', '🚛'), _buildCategoryRow('D1', '🚐'), _buildCategoryRow('D', '🚌'), _buildCategoryRow('DE', '🚌'), _buildCategoryRow('G1', '🚜'), _buildCategoryRow('G', '🚜'), _buildCategoryRow('J', '🏗️'), _buildCategoryRow('H', '♿')], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildLicenseGlassCard({required Widget child, EdgeInsetsGeometry? padding}) { + return ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 55, sigmaY: 55), + child: Container( + padding: padding ?? const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white.withAlpha(45), + Colors.white.withAlpha(18), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withAlpha(60), width: 1.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(30), + blurRadius: 25, + offset: const Offset(0, 10), + ), + ], + ), + child: child, + ), + ), + ); + } + + Widget _buildGlassButton({required String label, required IconData icon, required Color color, required VoidCallback onPressed}) { + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 45, sigmaY: 45), + child: InkWell( + onTap: onPressed, + child: Container( + width: double.infinity, + height: 60, + decoration: BoxDecoration(color: color.withAlpha(40), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white.withAlpha(60), width: 1.0)), + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(icon, size: 26, color: Colors.white), const SizedBox(width: 12), Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.white))]), + ), + ), + ), + ); + } + + Widget _buildRecentActivitiesFragment() { + return ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withAlpha(40), width: 1.5), + ), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.35, + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Recent Activities', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), + const SizedBox(height: 10), + Container(height: 1.0, color: Colors.white24), + const SizedBox(height: 10), + Expanded( + child: ListView.separated( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: _recentActivities.length, + separatorBuilder: (context, index) => Divider(color: Colors.white.withAlpha(15)), + itemBuilder: (context, index) { + final activity = _recentActivities[index]; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: Container(padding: const EdgeInsets.all(8), decoration: BoxDecoration(color: Colors.white.withAlpha(20), shape: BoxShape.circle), child: Icon(activity['icon'], color: Colors.white70, size: 20)), + title: Text(activity['title'], style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)), + subtitle: Text(activity['date'], style: const TextStyle(color: Colors.white60, fontSize: 12)), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildDashboard() { + if (_isLoading) return const Center(child: CircularProgressIndicator(color: Colors.white)); + if (_errorMessage.isNotEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.redAccent, size: 50), const SizedBox(height: 16), + Text(_errorMessage, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, color: Colors.white)), const SizedBox(height: 16), + ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: Colors.white.withAlpha(30)), onPressed: () { setState(() { _isLoading = true; _errorMessage = ''; }); _fetchLicenseData(); }, child: const Text('Retry', style: TextStyle(color: Colors.white))), + ], + ), + ), + ); + } + + final List tempLicenses = _licenseData?['temporaryLicenses'] ?? []; + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: kToolbarHeight + 40, bottom: 120.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 10), + const Text('Tap the card to rotate side', style: TextStyle(color: Colors.white60, fontSize: 13, fontWeight: FontWeight.w500)), + const SizedBox(height: 8), + GestureDetector(onTap: () { HapticFeedback.selectionClick(); setState(() { _isFront = !_isFront; }); }, child: AnimatedSwitcher(duration: const Duration(milliseconds: 150), child: _isFront ? _buildFrontCard() : _buildBackCard())), + const SizedBox(height: 24), + if (tempLicenses.isNotEmpty) ...[ + _buildGlassButton(label: 'VIEW TEMPORARY LICENSE', icon: Icons.assignment_late_outlined, color: Colors.orangeAccent, onPressed: () { HapticFeedback.lightImpact(); _showTemporaryLicenseSheet(tempLicenses.last as Map); }), + const SizedBox(height: 16), + ], + _buildGlassButton(label: 'SHOW QR TO OFFICER', icon: Icons.qr_code_scanner, color: Colors.blueAccent, onPressed: () { HapticFeedback.lightImpact(); _generateQR(); }), + const SizedBox(height: 24), + _buildRecentActivitiesFragment(), + ], + ), + ); + } + + Widget _buildNavItem(int index, String title, IconData icon) { + bool isSelected = _currentIndex == index; + return GestureDetector( + onTap: () { + if (_currentIndex == index) return; + HapticFeedback.selectionClick(); + + if (index == 0) { + _addRecentActivity('Viewed License Dashboard', Icons.credit_card); + } else if (index == 1) { + _addRecentActivity('Navigated to Fines', Icons.receipt_long); + } else if (index == 2) { + _addRecentActivity('Navigated to Profile', Icons.person); + } + + setState(() => _currentIndex = index); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + curve: Curves.easeInOut, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration(color: isSelected ? Colors.white.withAlpha(30) : Colors.transparent, borderRadius: BorderRadius.circular(20)), + child: Row(mainAxisSize: MainAxisSize.min, children: [Icon(icon, color: isSelected ? Colors.white : Colors.white60, size: 22), if (isSelected) ...[const SizedBox(width: 8), Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13))]]), + ), + ); + } + + Widget _buildGlassBackground() { + return RepaintBoundary( + child: Stack( + children: [ + Container(color: const Color(0xFF0B0F19)), + Positioned(top: -50, left: -50, child: Container(width: 300, height: 300, decoration: BoxDecoration(color: const Color(0xFF1E3A8A).withAlpha(140), shape: BoxShape.circle))), + Positioned(bottom: 100, right: -50, child: Container(width: 350, height: 350, decoration: BoxDecoration(color: Colors.teal.shade900.withAlpha(120), shape: BoxShape.circle))), + Positioned.fill(child: BackdropFilter(filter: ImageFilter.blur(sigmaX: 75, sigmaY: 75), child: Container(color: Colors.transparent))), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _buildGlassBackground(), + Scaffold( + backgroundColor: Colors.transparent, + extendBody: true, + extendBodyBehindAppBar: true, + appBar: _currentIndex == 0 ? AppBar( + automaticallyImplyLeading: false, + backgroundColor: const Color(0xFF0B0F19).withAlpha(120), + flexibleSpace: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.white.withAlpha(40), width: 1.0)), + ), + ), + ), + ), + foregroundColor: Colors.white, + title: const Text('Auto-Ledger Dashboard', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + elevation: 0, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 12.0, top: 6.0, bottom: 6.0), + child: Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: Colors.redAccent.withAlpha(30), + shape: BoxShape.circle, + border: Border.all(color: Colors.redAccent.withAlpha(80), width: 1.0), + ), + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.logout_rounded, color: Colors.redAccent, size: 20), + onPressed: _logout, + tooltip: 'Logout', + ), + ), + ) + ], + ) : null, + body: _currentIndex == 0 + ? _buildDashboard() + : _currentIndex == 1 + ? FinesScreen( + onLogActivity: _addRecentActivity, + onSelectionModeChanged: (isSelected) => setState(() => _isSelectionMode = isSelected), + ) + : ProfileScreen( + onLogActivity: _addRecentActivity, + ), + bottomNavigationBar: AnimatedSlide( + offset: _isSelectionMode ? const Offset(0, 2) : Offset.zero, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: SafeArea( + child: Container( + margin: const EdgeInsets.only(bottom: 16, left: 30, right: 30), + height: 65, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(35), boxShadow: [BoxShadow(color: Colors.black.withAlpha(40), blurRadius: 20, offset: const Offset(0, 10))]), + child: ClipRRect( + borderRadius: BorderRadius.circular(35), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + decoration: BoxDecoration(color: const Color(0xFF0B0F19).withAlpha(160), borderRadius: BorderRadius.circular(35), border: Border.all(color: Colors.white.withAlpha(40), width: 1.0)), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [_buildNavItem(0, 'License', Icons.credit_card), _buildNavItem(1, 'Fines', Icons.receipt_long), _buildNavItem(2, 'Profile', Icons.person)]), + ), + ), + ), + ), + ), + ), + ), + ], + ); + } +} + +class _QRDialog extends StatefulWidget { + final String qrToken; + const _QRDialog({required this.qrToken}); + @override State<_QRDialog> createState() => _QRDialogState(); +} + +class _QRDialogState extends State<_QRDialog> { + int _remainingSeconds = 180; Timer? _pollingTimer; Timer? _countdownTimer; bool _isScanned = false; + @override void initState() { super.initState(); _startPolling(); } + @override void dispose() { _pollingTimer?.cancel(); _countdownTimer?.cancel(); super.dispose(); } + void _startPolling() { _pollingTimer = Timer.periodic(const Duration(seconds: 3), (timer) async { try { final response = await ApiService.dio.get('/license/check-scan-status', queryParameters: {'qrToken': widget.qrToken}); if (response.data['scanned'] == true) { timer.cancel(); _onQrScanned(); } } catch (e) { return; } }); } + void _onQrScanned() { if (!mounted || _isScanned) return; setState(() { _isScanned = true; }); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_remainingSeconds > 0) { setState(() => _remainingSeconds--); } else { timer.cancel(); if (mounted) Navigator.pop(context); } }); } + String get _formattedTime { int minutes = _remainingSeconds ~/ 60; int seconds = _remainingSeconds % 60; return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } + + @override Widget build(BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient(colors: [Colors.white.withAlpha(60), Colors.white.withAlpha(30)]), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withAlpha(80), width: 1.0), + boxShadow: [BoxShadow(color: Colors.black.withAlpha(30), blurRadius: 25, offset: const Offset(0, 10))] + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Show this to the Officer', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 20), + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withAlpha(140), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withAlpha(80), width: 1.5) + ), + child: QrImageView(data: widget.qrToken, version: QrVersions.auto, size: 200.0) + ) + ) + ), + const SizedBox(height: 20), + Text(_isScanned ? _formattedTime : '03:00', style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: _isScanned ? Colors.redAccent : Colors.white)), + const SizedBox(height: 15), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.white.withAlpha(30), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15), side: BorderSide(color: Colors.white.withAlpha(40)))), + onPressed: () { HapticFeedback.mediumImpact(); Navigator.pop(context); }, + child: const Text('Close', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)) + ) + ) + ] + ) + ) + ) + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/screens/login_screen.dart b/frontend_apps/auto_ledger/lib/screens/login_screen.dart new file mode 100644 index 0000000..51705e3 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/screens/login_screen.dart @@ -0,0 +1,1055 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../services/auth_service.dart'; +import '../services/biometric_service.dart'; +import '../utils/device_info.dart'; +import '../utils/settings_util.dart'; +import '../utils/secure_storage.dart'; +import '../widgets/glass_container.dart'; +import 'home_screen.dart'; +import 'register_screen.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State with WidgetsBindingObserver { + final _nicController = TextEditingController(); + final _passwordController = TextEditingController(); + final _otpController = TextEditingController(); + + final _forgotNicController = TextEditingController(); + final _forgotPhoneController = TextEditingController(); + final _newPasswordController = TextEditingController(); + final _confirmNewPasswordController = TextEditingController(); + + final BiometricService _biometricService = BiometricService(); + bool _isBiometricAvailable = false; + + bool _isLoading = false; + bool _isBiometricEnabled = false; + bool _obscurePassword = true; + bool _obscureResetPassword = true; + bool _obscureConfirmResetPassword = true; + + String _verificationId = ''; + String? _registeredPhone; + bool _isDeviceVerification = false; + + OverlayEntry? _overlayEntry; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _checkBiometricStatus(); + _checkBiometricAvailability(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _overlayEntry?.remove(); + _nicController.dispose(); + _passwordController.dispose(); + _otpController.dispose(); + _forgotNicController.dispose(); + _forgotPhoneController.dispose(); + _newPasswordController.dispose(); + _confirmNewPasswordController.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _checkBiometricStatus(); + _checkBiometricAvailability(); + } + } + + Future _checkBiometricStatus() async { + final isEnabled = await SettingsUtil.isBiometricEnabled(); + if (mounted) { + setState(() { + _isBiometricEnabled = isEnabled; + }); + } + } + + Future _checkBiometricAvailability() async { + final available = await _biometricService.checkBiometricsAvailable(); + if (mounted) { + setState(() { + _isBiometricAvailable = available; + }); + } + } + + void _showToast(String message, {bool isError = false}) { + _overlayEntry?.remove(); + _overlayEntry = null; + + final topPadding = MediaQuery.of(context).padding.top; + + _overlayEntry = OverlayEntry( + builder: (context) => Positioned( + top: topPadding + 20, + left: 16, + right: 16, + child: Material( + color: Colors.transparent, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutBack, + builder: (context, value, child) { + return Opacity( + opacity: value.clamp(0.0, 1.0), + child: Transform.translate( + offset: Offset(0, -(1 - value) * 20), + child: child, + ), + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: isError ? Colors.redAccent.withAlpha(100) : Colors.black.withAlpha(80), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withAlpha(70), + width: 1.0, + ), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(20), blurRadius: 10, spreadRadius: 1) + ], + ), + child: Row( + children: [ + Icon(isError ? Icons.error_outline : Icons.info_outline, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ], + ), + ), + ), + ), + ), + ), + ), + ); + + Navigator.of(context, rootNavigator: true).overlay?.insert(_overlayEntry!); + + Future.delayed(const Duration(seconds: 3), () { + if (_overlayEntry != null && _overlayEntry!.mounted) { + _overlayEntry!.remove(); + _overlayEntry = null; + } + }); + } + + void _showGlassySuccessToast(OverlayState overlay, String message) { + late OverlayEntry entry; + entry = OverlayEntry( + builder: (context) => Positioned( + bottom: 120.0, + left: 16, + right: 16, + child: Material( + color: Colors.transparent, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutBack, + builder: (context, value, child) { + return Opacity( + opacity: value.clamp(0.0, 1.0), + child: Transform.translate( + offset: Offset(0, (1 - value) * 20), + child: child, + ), + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), + decoration: BoxDecoration( + color: Colors.green.shade600.withAlpha(40), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withAlpha(100), + width: 1.0, + ), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(20), blurRadius: 20, offset: const Offset(0, 10)) + ], + ), + child: Row( + children: [ + const Icon(Icons.check_circle_outline_rounded, color: Colors.white, size: 28), + const SizedBox(width: 12), + Expanded( + child: Text( + message, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ); + + overlay.insert(entry); + + Future.delayed(const Duration(seconds: 1), () { + if (entry.mounted) { + entry.remove(); + } + }); + } + + Future _handleBiometricLogin() async { + if (!mounted) return; + + final isEnabled = await SettingsUtil.isBiometricEnabled(); + if (!isEnabled) { + _showToast('Biometric login is not enabled in settings.', isError: true); + return; + } + + if (!_isBiometricAvailable) { + _showToast('Biometric hardware not available.', isError: true); + return; + } + + String? savedNic = await SecureStorage.getNic(); + String nic = savedNic ?? _nicController.text.trim(); + + if (nic.isEmpty) { + _showToast('Please enter your NIC first to save for biometric login.', isError: true); + return; + } + + final authenticated = await _biometricService.authenticate(); + if (!authenticated) { + _showToast('Authentication failed.', isError: true); + return; + } + + setState(() => _isLoading = true); + try { + final deviceId = await DeviceInfoUtil.getDeviceId(); + final result = await AuthService.biometricLogin(nic, deviceId); + if (!mounted) return; + + if (result['success'] == true) { + await SecureStorage.saveNic(nic); + final overlay = Navigator.of(context, rootNavigator: true).overlay; + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + if (overlay != null) { + _showGlassySuccessToast(overlay, 'Biometric Login Successful!'); + } + } else { + _showToast('Biometric login failed. Check NIC.', isError: true); + } + } catch (e) { + if (mounted) _showToast('An error occurred during biometric login.', isError: true); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + Future _handleLogin() async { + if (!mounted) return; + + FocusScope.of(context).unfocus(); + + if (_nicController.text.trim().isEmpty) { + _showToast('NIC Number is required', isError: true); + return; + } + if (_passwordController.text.trim().isEmpty) { + _showToast('Password is required', isError: true); + return; + } + + setState(() => _isLoading = true); + try { + final deviceId = await DeviceInfoUtil.getDeviceId(); + final result = await AuthService.loginUser( + _nicController.text.trim(), + _passwordController.text.trim(), + deviceId, + ); + + if (!mounted) return; + + if (result['success'] == true) { + await SecureStorage.saveNic(_nicController.text.trim()); + final overlay = Navigator.of(context, rootNavigator: true).overlay; + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + if (overlay != null) { + _showGlassySuccessToast(overlay, 'Login Successful!'); + } + } else if (result['isDeviceMismatch'] == true) { + _showToast('New device detected. Verification required.', isError: true); + _registeredPhone = result['phone']; + await _sendOTP(_registeredPhone!, isDeviceVerification: true); + } else { + _showToast('Login Failed. Check credentials.', isError: true); + setState(() => _isLoading = false); + } + } catch (e) { + if (mounted) { + _showToast('Login Failed. Check credentials or connection.', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _sendOTP(String phone, {required bool isDeviceVerification}) async { + if (!mounted) return; + + _isDeviceVerification = isDeviceVerification; + await FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: phone, + verificationCompleted: (PhoneAuthCredential credential) async { + await FirebaseAuth.instance.signInWithCredential(credential); + if (isDeviceVerification) { + await _verifyNewDeviceBackend(); + } + }, + verificationFailed: (FirebaseAuthException e) { + if (mounted) { + _showToast(e.message ?? 'Verification Failed', isError: true); + setState(() => _isLoading = false); + } + }, + codeSent: (String verificationId, int? resendToken) { + if (mounted) { + setState(() { + _verificationId = verificationId; + _isLoading = false; + }); + _showOTPDialog(phoneNumber: phone); + } + }, + codeAutoRetrievalTimeout: (String verificationId) { + _verificationId = verificationId; + }, + ); + } + + Future _verifyOTP() async { + if (!mounted) return; + + FocusScope.of(context).unfocus(); + + if (_otpController.text.trim().isEmpty) { + _showToast('Please enter the OTP', isError: true); + return; + } + + setState(() => _isLoading = true); + try { + final credential = PhoneAuthProvider.credential( + verificationId: _verificationId, + smsCode: _otpController.text.trim(), + ); + await FirebaseAuth.instance.signInWithCredential(credential); + if (!mounted) return; + + Navigator.pop(context); + + if (_isDeviceVerification) { + await _verifyNewDeviceBackend(); + } else { + setState(() => _isLoading = false); + _showResetPasswordDialog(); + } + } catch (e) { + if (mounted) { + _showToast('Invalid OTP', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _verifyNewDeviceBackend() async { + if (!mounted) return; + + try { + final deviceId = await DeviceInfoUtil.getDeviceId(); + final isVerified = await AuthService.verifyNewDevice( + _nicController.text.trim(), + deviceId + ); + if (!mounted) return; + + if (isVerified) { + await SecureStorage.saveNic(_nicController.text.trim()); + final overlay = Navigator.of(context, rootNavigator: true).overlay; + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + if (overlay != null) { + _showGlassySuccessToast(overlay, 'Device verified successfully!'); + } + } + } catch (e) { + if (mounted) { + _showToast('Backend Verification Failed', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _handleForgotPasswordCheck() async { + if (!mounted) return; + + FocusScope.of(context).unfocus(); + + final nic = _forgotNicController.text.trim(); + final phoneDigits = _forgotPhoneController.text.trim(); + + if (nic.isEmpty) { + _showToast('NIC Number is required', isError: true); + return; + } + if (phoneDigits.isEmpty || phoneDigits.length != 9) { + _showToast('Please enter exactly 9 digits for the phone number.', isError: true); + return; + } + final phone = '+94$phoneDigits'; + + setState(() => _isLoading = true); + try { + final isValid = await AuthService.forgotPasswordCheck(nic, phone); + if (!mounted) return; + + if (isValid) { + Navigator.pop(context); + _isDeviceVerification = false; + await _sendOTP(phone, isDeviceVerification: false); + } + } catch (e) { + if (mounted) { + _showToast('Verification failed. Check NIC and Phone.', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _handlePasswordReset() async { + if (!mounted) return; + + FocusScope.of(context).unfocus(); + + final newPassword = _newPasswordController.text.trim(); + final confirmPassword = _confirmNewPasswordController.text.trim(); + + if (newPassword.isEmpty) { + _showToast('New Password is required', isError: true); + return; + } + final passwordRegex = RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$'); + if (!passwordRegex.hasMatch(newPassword)) { + _showToast('Password must be at least 8 characters, contain uppercase, lowercase, number, and special character.', isError: true); + return; + } + if (confirmPassword.isEmpty) { + _showToast('Please confirm your new password.', isError: true); + return; + } + if (newPassword != confirmPassword) { + _showToast('New password and confirm password do not match.', isError: true); + return; + } + + setState(() => _isLoading = true); + try { + final success = await AuthService.resetPassword( + _forgotNicController.text.trim(), + '+94${_forgotPhoneController.text.trim()}', + newPassword, + ); + + if (!mounted) return; + + if (success) { + Navigator.pop(context); + + final overlay = Navigator.of(context, rootNavigator: true).overlay; + if (overlay != null) { + _showGlassySuccessToast(overlay, 'Password reset successfully! Please login.'); + } + + _forgotNicController.clear(); + _forgotPhoneController.clear(); + _newPasswordController.clear(); + _confirmNewPasswordController.clear(); + setState(() => _isLoading = false); + } + } catch (e) { + if (mounted) { + _showToast('Failed to reset password.', isError: true); + setState(() => _isLoading = false); + } + } + } + + void _showForgotPasswordInitialDialog() { + _forgotPhoneController.clear(); + showDialog( + context: context, + barrierColor: Colors.black.withAlpha(200), + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white.withAlpha(25), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withAlpha(50), width: 1.5), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.lock_reset, color: Colors.white, size: 40), + const SizedBox(height: 16), + const Text('Reset Password', style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + const Text('Enter your details to receive an OTP', textAlign: TextAlign.center, style: TextStyle(color: Colors.white70, fontSize: 14)), + const SizedBox(height: 24), + TextField( + controller: _forgotNicController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'NIC Number', + labelStyle: const TextStyle(color: Colors.white54), + filled: true, + fillColor: Colors.white.withAlpha(20), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.cyanAccent)), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 18), + decoration: BoxDecoration( + color: Colors.white.withAlpha(20), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + bottomLeft: Radius.circular(12), + ), + border: Border.all(color: Colors.white.withAlpha(40)), + ), + child: const Text('+94', style: TextStyle(color: Colors.white70, fontWeight: FontWeight.bold)), + ), + Expanded( + child: TextField( + controller: _forgotPhoneController, + keyboardType: TextInputType.phone, + maxLength: 9, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Phone Number (9 digits)', + labelStyle: const TextStyle(color: Colors.white54), + counterText: '', + filled: true, + fillColor: Colors.white.withAlpha(20), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.cyanAccent), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.white.withAlpha(100), width: 1.5), + ), + ), + onPressed: () => Navigator.pop(context), + child: const Text('Cancel', style: TextStyle(color: Colors.white)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(50), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.white.withAlpha(150), width: 1.5), + ), + ), + onPressed: _handleForgotPasswordCheck, + child: const Text('Next', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } + + void _showOTPDialog({required String phoneNumber}) { + _otpController.clear(); + showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.black.withAlpha(200), + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white.withAlpha(25), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withAlpha(50), width: 1.5), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(_isDeviceVerification ? Icons.phonelink_lock : Icons.message, color: Colors.white, size: 40), + const SizedBox(height: 15), + Text(_isDeviceVerification ? 'Device Verification' : 'Enter OTP', + style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 15), + Text( + _isDeviceVerification + ? 'A new device was detected. Enter the OTP sent to $phoneNumber.' + : 'Enter the OTP sent to $phoneNumber to reset your password.', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white70, fontSize: 14), + ), + const SizedBox(height: 20), + TextField( + controller: _otpController, + keyboardType: TextInputType.number, + style: const TextStyle(color: Colors.white, fontSize: 20, letterSpacing: 8), + textAlign: TextAlign.center, + decoration: InputDecoration( + filled: true, + fillColor: Colors.white.withAlpha(20), + hintText: '••••••', + hintStyle: const TextStyle(color: Colors.white54, letterSpacing: 8), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.white.withAlpha(100), width: 1.5), + ), + ), + onPressed: () { + Navigator.pop(context); + setState(() => _isLoading = false); + }, + child: const Text('Cancel', style: TextStyle(color: Colors.white)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(50), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.white.withAlpha(150), width: 1.5), + ), + ), + onPressed: _verifyOTP, + child: const Text('Verify', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } + + void _showResetPasswordDialog() { + _newPasswordController.clear(); + _confirmNewPasswordController.clear(); + showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.black.withAlpha(200), + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setModalState) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white.withAlpha(25), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withAlpha(50), width: 1.5), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.password, color: Colors.white, size: 40), + const SizedBox(height: 16), + const Text('New Password', style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 24), + TextField( + controller: _newPasswordController, + obscureText: _obscureResetPassword, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'New Password', + labelStyle: const TextStyle(color: Colors.white54), + filled: true, + fillColor: Colors.white.withAlpha(20), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.cyanAccent)), + suffixIcon: IconButton( + icon: Icon( + _obscureResetPassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + setModalState(() { + _obscureResetPassword = !_obscureResetPassword; + }); + }, + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _confirmNewPasswordController, + obscureText: _obscureConfirmResetPassword, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Confirm New Password', + labelStyle: const TextStyle(color: Colors.white54), + filled: true, + fillColor: Colors.white.withAlpha(20), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.cyanAccent)), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmResetPassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + setModalState(() { + _obscureConfirmResetPassword = !_obscureConfirmResetPassword; + }); + }, + ), + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(50), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.white.withAlpha(150), width: 1.5), + ), + ), + onPressed: _handlePasswordReset, + child: const Text('Save Password', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + const _LoginBackground(), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: GlassContainer( + width: double.infinity, + padding: const EdgeInsets.all(28.0), + borderRadius: 24.0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.directions_car, size: 60, color: Colors.white), + const SizedBox(height: 16), + const Text( + 'Auto-Ledger', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 40), + TextField( + controller: _nicController, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500), + decoration: InputDecoration( + labelText: 'NIC Number', + labelStyle: const TextStyle(color: Colors.white70), + prefixIcon: const Icon(Icons.badge, color: Colors.white70), + filled: true, + fillColor: Colors.white.withAlpha(20), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.white.withAlpha(40)), + borderRadius: BorderRadius.circular(16), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.cyanAccent, width: 1.5), + borderRadius: BorderRadius.circular(16), + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500), + decoration: InputDecoration( + labelText: 'Password', + labelStyle: const TextStyle(color: Colors.white70), + prefixIcon: const Icon(Icons.lock, color: Colors.white70), + filled: true, + fillColor: Colors.white.withAlpha(20), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.white.withAlpha(40)), + borderRadius: BorderRadius.circular(16), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.cyanAccent, width: 1.5), + borderRadius: BorderRadius.circular(16), + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: _showForgotPasswordInitialDialog, + child: const Text( + 'Forgot Password?', + style: TextStyle(color: Colors.cyanAccent, fontWeight: FontWeight.w600), + ), + ), + ), + const SizedBox(height: 8), + if (_isLoading) + const CircularProgressIndicator(color: Colors.cyanAccent) + else + SizedBox( + width: double.infinity, + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF0F2027), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + onPressed: _handleLogin, + child: const Text( + 'LOGIN', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 1.5), + ), + ), + ), + const SizedBox(height: 24), + if (_isBiometricEnabled && _isBiometricAvailable) ...[ + IconButton( + icon: const Icon(Icons.fingerprint, color: Colors.white, size: 45), + onPressed: _isLoading ? null : _handleBiometricLogin, + ), + const SizedBox(height: 16), + ], + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const RegisterScreen()), + ); + }, + child: RichText( + text: const TextSpan( + text: 'New Driver? ', + style: TextStyle(color: Colors.white70, fontSize: 13), + children: [ + TextSpan( + text: 'Register Here', + style: TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 13), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class _LoginBackground extends StatelessWidget { + const _LoginBackground(); + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: Stack( + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF0F2027), Color(0xFF203A43), Color(0xFF2C5364)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), + Positioned( + top: 100, + left: -80, + child: Container( + width: 300, + height: 300, + decoration: BoxDecoration( + color: Colors.cyanAccent.withAlpha(40), + shape: BoxShape.circle, + ), + ), + ), + Positioned( + bottom: -50, + right: -50, + child: Container( + width: 250, + height: 250, + decoration: BoxDecoration( + color: Colors.deepPurpleAccent.withAlpha(60), + shape: BoxShape.circle, + ), + ), + ), + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), + child: const SizedBox(), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/screens/profile_screen.dart b/frontend_apps/auto_ledger/lib/screens/profile_screen.dart new file mode 100644 index 0000000..d5421d3 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/screens/profile_screen.dart @@ -0,0 +1,973 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:dio/dio.dart'; +import '../services/api_service.dart'; +import '../utils/secure_storage.dart'; +import '../utils/settings_util.dart'; +import 'login_screen.dart'; + +class ProfileScreen extends StatefulWidget { + final void Function(String, IconData) onLogActivity; + + const ProfileScreen({super.key, required this.onLogActivity}); + + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + bool _isLoading = true; + String _errorMessage = ''; + + String _fullName = ''; + String _nic = ''; + String _address = ''; + int _points = 0; + String? _imageUrl; + + bool _isBiometricEnabled = false; + bool _isChangingPassword = false; + + final _oldPwController = TextEditingController(); + final _newPwController = TextEditingController(); + final _confirmPwController = TextEditingController(); + + OverlayEntry? _overlayEntry; + + @override + void initState() { + super.initState(); + _checkBiometricStatus(); + _fetchUserProfile(); + } + + @override + void dispose() { + _overlayEntry?.remove(); + _oldPwController.dispose(); + _newPwController.dispose(); + _confirmPwController.dispose(); + super.dispose(); + } + + void _showGlassToast(String message, {bool isError = false}) { + _overlayEntry?.remove(); + _overlayEntry = null; + + final topPadding = MediaQuery.of(context).padding.top; + + _overlayEntry = OverlayEntry( + builder: (context) => Positioned( + top: topPadding + 10, + left: 20, + right: 20, + child: Material( + color: Colors.transparent, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutBack, + builder: (context, value, child) { + return Opacity( + opacity: value.clamp(0.0, 1.0), + child: Transform.translate( + offset: Offset(0, -(1 - value) * 20), + child: child, + ), + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: isError ? Colors.redAccent.withAlpha(50) : Colors.green.shade600.withAlpha(50), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withAlpha(100), width: 1.0), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(20), blurRadius: 20, offset: const Offset(0, 5)) + ], + ), + child: Row( + children: [ + Icon(isError ? Icons.error_outline_rounded : Icons.check_circle_outline_rounded, color: Colors.white, size: 28), + const SizedBox(width: 12), + Expanded( + child: Text( + message, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ); + + Navigator.of(context, rootNavigator: true).overlay?.insert(_overlayEntry!); + + Future.delayed(const Duration(seconds: 3), () { + if (_overlayEntry != null && _overlayEntry!.mounted) { + _overlayEntry!.remove(); + _overlayEntry = null; + } + }); + } + + Future _checkBiometricStatus() async { + final isEnabled = await SettingsUtil.isBiometricEnabled(); + setState(() { + _isBiometricEnabled = isEnabled; + }); + } + + Future _fetchUserProfile() async { + setState(() { + _isLoading = true; + _errorMessage = ''; + }); + + try { + final response = await ApiService.dio.get('/license/my-license'); + final data = response.data; + + if (mounted) { + setState(() { + _fullName = data['full_Name'] ?? 'Unknown User'; + _nic = data['nic_No'] ?? 'N/A'; + _address = data['address'] ?? 'N/A'; + _points = data['points'] ?? 0; + _imageUrl = data['image']; + _isLoading = false; + }); + } + } on DioException catch (e) { + if (mounted) { + setState(() { + _errorMessage = e.response?.data['message'] ?? 'Failed to load profile.'; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _errorMessage = 'An unexpected error occurred.'; + _isLoading = false; + }); + } + } + } + + Future _changePassword(BuildContext dialogContext, StateSetter setModalState) async { + final oldPw = _oldPwController.text.trim(); + final newPw = _newPwController.text.trim(); + final confirmPw = _confirmPwController.text.trim(); + + if (oldPw.isEmpty) { + _showGlassToast('Current password is required.', isError: true); + return; + } + if (newPw.isEmpty) { + _showGlassToast('Please enter a new password.', isError: true); + return; + } + final passwordRegex = RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$'); + if (!passwordRegex.hasMatch(newPw)) { + _showGlassToast('Password must be at least 8 characters, contain uppercase, lowercase, number, and special character.', isError: true); + return; + } + if (newPw == oldPw) { + _showGlassToast('New password cannot be the same as your current password.', isError: true); + return; + } + if (confirmPw.isEmpty) { + _showGlassToast('Please confirm your new password.', isError: true); + return; + } + if (newPw != confirmPw) { + _showGlassToast('New password and confirm password do not match.', isError: true); + return; + } + + FocusManager.instance.primaryFocus?.unfocus(); + + setModalState(() { + _isChangingPassword = true; + }); + + try { + await ApiService.dio.patch('/auth/user/change-password', data: { + 'oldPassword': oldPw, + 'newPassword': newPw, + }); + + if (mounted) { + setModalState(() { + _isChangingPassword = false; + }); + + if (dialogContext.mounted) { + Navigator.pop(dialogContext); + } + + _oldPwController.clear(); + _newPwController.clear(); + _confirmPwController.clear(); + + _showGlassToast('Password updated successfully!'); + widget.onLogActivity('Password Changed Successfully', Icons.password_rounded); + } + } on DioException catch (e) { + if (mounted) { + setModalState(() { + _isChangingPassword = false; + }); + _showGlassToast(e.response?.data['message'] ?? 'Failed to change password. Check your current password.', isError: true); + } + } catch (e) { + if (mounted) { + setModalState(() { + _isChangingPassword = false; + }); + _showGlassToast('An unexpected error occurred. Try again.', isError: true); + } + } + } + + void _showBiometricPasswordDialog() { + final TextEditingController pwController = TextEditingController(); + bool isObscured = true; + bool _isVerifying = false; + + showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.black.withAlpha(160), + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setModalState) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white.withAlpha(50), Colors.white.withAlpha(20)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withAlpha(80), width: 1.0), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(30), blurRadius: 40, offset: const Offset(0, 10)) + ] + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withAlpha(30), + shape: BoxShape.circle, + ), + child: const Icon(Icons.fingerprint_rounded, color: Colors.white, size: 50), + ), + const SizedBox(height: 16), + const Text('Security Verification', style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w900)), + const SizedBox(height: 10), + const Text( + 'Please enter your current password to enable Biometric Authentication.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 20), + Container( + decoration: BoxDecoration( + color: Colors.white.withAlpha(20), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withAlpha(50), width: 1.0), + ), + child: TextField( + controller: pwController, + obscureText: isObscured, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + hintText: 'Current Password', + hintStyle: const TextStyle(color: Colors.white38, fontWeight: FontWeight.w500), + suffixIcon: IconButton( + icon: Icon(isObscured ? Icons.visibility_off : Icons.visibility, color: Colors.white70), + onPressed: () { + setModalState(() { + isObscured = !isObscured; + }); + }, + ), + ), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + onPressed: () { + Navigator.pop(context); + setState(() => _isBiometricEnabled = false); + }, + child: const Text('Cancel', style: TextStyle(color: Colors.white70, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + backgroundColor: Colors.white.withAlpha(40), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.white.withAlpha(80), width: 1) + ), + ), + onPressed: _isVerifying + ? null + : () async { + final enteredPassword = pwController.text.trim(); + if (enteredPassword.isEmpty) { + _showGlassToast('Password is required!', isError: true); + return; + } + + setModalState(() => _isVerifying = true); + + try { + final tempPassword = enteredPassword + '_verify_temp'; + + await ApiService.dio.patch('/auth/user/change-password', data: { + 'oldPassword': enteredPassword, + 'newPassword': tempPassword, + }); + + await ApiService.dio.patch('/auth/user/change-password', data: { + 'oldPassword': tempPassword, + 'newPassword': enteredPassword, + }); + + if (mounted) { + Navigator.pop(context); + SettingsUtil.setBiometricEnabled(true); + setState(() => _isBiometricEnabled = true); + SecureStorage.saveNic(_nic); + _showGlassToast('Biometrics Enabled Successfully!'); + widget.onLogActivity('Enabled Biometric Login', Icons.fingerprint_rounded); + } + } on DioException catch (e) { + setModalState(() => _isVerifying = false); + final errorMsg = e.response?.data['message'] ?? 'Invalid password. Please try again.'; + _showGlassToast(errorMsg, isError: true); + } catch (e) { + setModalState(() => _isVerifying = false); + _showGlassToast('An error occurred. Please try again.', isError: true); + } + }, + child: _isVerifying + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + 'Enable', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + } + + void _showChangePasswordDialog() { + widget.onLogActivity('Initiated Password Change', Icons.password_rounded); + + bool oldPwVis = false; + bool newPwVis = false; + bool confPwVis = false; + _oldPwController.clear(); + _newPwController.clear(); + _confirmPwController.clear(); + + showDialog( + context: context, + barrierColor: Colors.black.withAlpha(160), + builder: (BuildContext dialogContext) { + return StatefulBuilder( + builder: (context, setModalState) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + insetPadding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white.withAlpha(40), Colors.white.withAlpha(15)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withAlpha(80), width: 1.0), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(30), blurRadius: 40, offset: const Offset(0, 10)) + ] + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white.withAlpha(30), + shape: BoxShape.circle, + ), + child: const Icon(Icons.password_rounded, color: Colors.white, size: 40), + ), + const SizedBox(height: 16), + const Text('Change Password', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: Colors.white)), + const SizedBox(height: 24), + + _buildDialogPasswordField('Current Password', _oldPwController, oldPwVis, () => setModalState(() => oldPwVis = !oldPwVis)), + _buildDialogPasswordField('New Password', _newPwController, newPwVis, () => setModalState(() => newPwVis = !newPwVis)), + _buildDialogPasswordField('Confirm New Password', _confirmPwController, confPwVis, () => setModalState(() => confPwVis = !confPwVis)), + + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.pop(dialogContext); + }, + child: const Text('Cancel', style: TextStyle(color: Colors.white70, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 2, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + backgroundColor: Colors.white.withAlpha(40), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.white.withAlpha(80), width: 1) + ), + elevation: 0, + ), + onPressed: _isChangingPassword ? null : () => _changePassword(dialogContext, setModalState), + child: _isChangingPassword + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + : const Text('Save Password', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)), + ), + ), + ], + ) + ], + ), + ), + ), + ), + ); + }, + ); + }, + ); + } + + Future _logout() async { + showDialog( + context: context, + barrierColor: Colors.black.withAlpha(160), + builder: (BuildContext dialogContext) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white.withAlpha(40), Colors.white.withAlpha(15)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withAlpha(80), width: 1.0), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: Colors.red.withAlpha(30), shape: BoxShape.circle), + child: const Icon(Icons.logout_rounded, color: Colors.redAccent, size: 45), + ), + const SizedBox(height: 16), + const Text('Logout', style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w900)), + const SizedBox(height: 10), + const Text('Are you sure you want to logout from your account?', textAlign: TextAlign.center, style: TextStyle(color: Colors.white70, fontWeight: FontWeight.w600)), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(20), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.white.withAlpha(40), width: 1) + ), + ), + onPressed: () => Navigator.pop(dialogContext), + child: const Text('Cancel', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade700.withAlpha(180), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.red.shade400.withAlpha(100), width: 1) + ), + ), + onPressed: () async { + HapticFeedback.heavyImpact(); + widget.onLogActivity('Logged Out', Icons.logout_rounded); + await SecureStorage.deleteToken(); + final isBiometricEnabled = await SettingsUtil.isBiometricEnabled(); + if (!isBiometricEnabled) { + await SecureStorage.deleteNic(); + } + if (mounted) { + Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const LoginScreen()), (route) => false); + } + }, + child: const Text('Logout', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ) + ], + ), + ), + ), + ); + }, + ); + } + + Color _getPointsColor() { + if (_points >= 80) return Colors.red.shade400; + if (_points >= 50) return Colors.orange.shade400; + if (_points >= 24) return Colors.amber.shade400; + return Colors.green.shade400; + } + + Widget _buildGlassBackground() { + return RepaintBoundary( + child: Stack( + children: [ + Container(color: const Color(0xFF0B0F19)), + Positioned(top: -40, left: -60, child: Container(width: 280, height: 280, decoration: BoxDecoration(color: const Color(0xFF1E3A8A).withAlpha(140), shape: BoxShape.circle))), + Positioned(top: 250, right: -80, child: Container(width: 240, height: 240, decoration: BoxDecoration(color: Colors.purple.shade900.withAlpha(120), shape: BoxShape.circle))), + Positioned(bottom: 80, left: -40, child: Container(width: 320, height: 320, decoration: BoxDecoration(color: Colors.teal.shade900.withAlpha(120), shape: BoxShape.circle))), + Positioned.fill(child: BackdropFilter(filter: ImageFilter.blur(sigmaX: 75, sigmaY: 75), child: Container(color: Colors.transparent))), + ], + ), + ); + } + + Widget _buildGlassCard({required Widget child, EdgeInsetsGeometry? padding}) { + return ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 55, sigmaY: 55), + child: Container( + padding: padding ?? const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white.withAlpha(45), Colors.white.withAlpha(20)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withAlpha(40), width: 1.0), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(25), blurRadius: 25, offset: const Offset(0, 8)) + ], + ), + child: child, + ), + ), + ); + } + + Widget _buildProfileHeader() { + String initials = _fullName.isNotEmpty ? _fullName.substring(0, 1).toUpperCase() : '?'; + if (_fullName.contains(' ')) { + final parts = _fullName.split(' '); + if (parts.length > 1 && parts[1].isNotEmpty) { + initials += parts[1].substring(0, 1).toUpperCase(); + } + } + + return _buildGlassCard( + padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 18), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withAlpha(15), + border: Border.all(color: Colors.white.withAlpha(120), width: 2.0), + boxShadow: [BoxShadow(color: Colors.black.withAlpha(35), blurRadius: 15, offset: const Offset(0, 4))], + image: _imageUrl != null && _imageUrl!.isNotEmpty + ? DecorationImage( + image: NetworkImage(_imageUrl!), + fit: BoxFit.cover, + ) + : null, + ), + child: _imageUrl == null || _imageUrl!.isEmpty + ? Center( + child: Text(initials, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w900, color: Colors.white)), + ) + : null, + ), + const SizedBox(width: 18), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _fullName, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: Colors.white, height: 1.2, letterSpacing: 0.3), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withAlpha(15), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.white.withAlpha(50), width: 1.0) + ), + child: Text('NIC: $_nic', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white70, letterSpacing: 0.5)), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPointsWidget() { + final color = _getPointsColor(); + + return _buildGlassCard( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Demerit Points', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: Colors.white)), + const SizedBox(height: 4), + const Text('Accumulated penalty points', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white60)), + ], + ), + ), + Container( + width: 65, + height: 65, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color.withAlpha(40), + border: Border.all(color: color.withAlpha(180), width: 2.5), + boxShadow: [ + BoxShadow( + color: color.withAlpha(50), + blurRadius: 12, + spreadRadius: 2 + ) + ], + ), + child: Center( + child: Text(_points.toString(), style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: color)), + ), + ), + ], + ), + ); + } + + Widget _buildBiometricToggle() { + return _buildGlassCard( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Row( + children: [ + Icon(Icons.fingerprint, color: Colors.white, size: 26), + SizedBox(width: 12), + Text('Biometric Login', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: Colors.white)), + ], + ), + Switch( + value: _isBiometricEnabled, + activeColor: Colors.white, + activeTrackColor: const Color(0xFF1A2980).withAlpha(200), + inactiveThumbColor: Colors.grey.shade400, + inactiveTrackColor: Colors.white.withAlpha(20), + trackOutlineColor: WidgetStateProperty.resolveWith((states) { + if (!states.contains(WidgetState.selected)) { + return Colors.white.withAlpha(40); + } + return null; + }), + onChanged: (bool value) { + if (value) { + _showBiometricPasswordDialog(); + } else { + SettingsUtil.setBiometricEnabled(false); + setState(() => _isBiometricEnabled = false); + SecureStorage.deleteNic(); + _showGlassToast('Biometric login disabled.', isError: true); + widget.onLogActivity('Disabled Biometric Login', Icons.fingerprint_rounded); + } + }, + ), + ], + ), + ); + } + + Widget _buildDialogPasswordField(String label, TextEditingController controller, bool isVisible, VoidCallback onVisibilityToggle) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Container( + decoration: BoxDecoration( + color: Colors.white.withAlpha(15), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withAlpha(40), width: 1.0), + ), + child: TextField( + controller: controller, + obscureText: !isVisible, + style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 15), + decoration: InputDecoration( + isDense: true, + labelText: label, + labelStyle: const TextStyle(color: Colors.white60, fontWeight: FontWeight.w600, fontSize: 13), + prefixIcon: const Icon(Icons.lock_outline_rounded, color: Colors.white70, size: 18), + suffixIcon: IconButton( + icon: Icon(isVisible ? Icons.visibility_off_rounded : Icons.visibility_rounded, color: Colors.white70, size: 18), + onPressed: onVisibilityToggle, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _buildGlassBackground(), + Scaffold( + backgroundColor: Colors.transparent, + extendBodyBehindAppBar: true, + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: const Color(0xFF0B0F19).withAlpha(120), + flexibleSpace: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.white.withAlpha(40), width: 1.0)), + ), + ), + ), + ), + foregroundColor: Colors.white, + elevation: 0, + centerTitle: true, + title: const Text('My Profile', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, letterSpacing: 0.5)), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator(color: Colors.white)) + : _errorMessage.isNotEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, color: Colors.red.shade400, size: 50), + const SizedBox(height: 16), + Text(_errorMessage, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.white.withAlpha(30), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + onPressed: _fetchUserProfile, + child: const Text('Retry', style: TextStyle(color: Colors.white)) + ) + ], + ), + ) + : SafeArea( + bottom: false, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 20, right: 20, top: 20, bottom: 120), + child: Column( + children: [ + _buildProfileHeader(), + const SizedBox(height: 20), + _buildPointsWidget(), + const SizedBox(height: 20), + + _buildGlassCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.location_on_rounded, color: Colors.white.withAlpha(200), size: 20), + const SizedBox(width: 8), + const Text('Registered Address', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 14)), + ], + ), + const SizedBox(height: 10), + Text(_address, style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.w600, fontSize: 14, height: 1.4)), + ], + ), + ), + + const SizedBox(height: 20), + + // Biometric Toggle + _buildBiometricToggle(), + + const SizedBox(height: 20), + + // Change Password + _buildGlassCard( + padding: const EdgeInsets.all(4), + child: ListTile( + onTap: _showChangePasswordDialog, + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration(color: Colors.white.withAlpha(15), shape: BoxShape.circle), + child: const Icon(Icons.password_rounded, color: Colors.white), + ), + title: const Text('Change Password', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.white, fontSize: 15)), + trailing: const Icon(Icons.arrow_forward_ios_rounded, color: Colors.white70, size: 16), + ), + ), + + const SizedBox(height: 30), + + // Logout Button + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade50.withAlpha(15), + foregroundColor: Colors.red.shade300, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Colors.redAccent.withAlpha(50), width: 1.0) + ), + ), + onPressed: () { + HapticFeedback.lightImpact(); + _logout(); + }, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.logout_rounded, size: 22), + SizedBox(width: 10), + Text('Log Out', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16)), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/screens/register_screen.dart b/frontend_apps/auto_ledger/lib/screens/register_screen.dart new file mode 100644 index 0000000..e8a175a --- /dev/null +++ b/frontend_apps/auto_ledger/lib/screens/register_screen.dart @@ -0,0 +1,666 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../services/auth_service.dart'; +import '../utils/device_info.dart'; +import '../widgets/glass_container.dart'; +import 'login_screen.dart'; +import 'home_screen.dart'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final _nicController = TextEditingController(); + final _nameController = TextEditingController(); + final _mobileController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _otpController = TextEditingController(); + + bool _isLoading = false; + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + String _verificationId = ''; + Map? _registeredData; + + OverlayEntry? _overlayEntry; + + @override + void dispose() { + _overlayEntry?.remove(); + _nicController.dispose(); + _nameController.dispose(); + _mobileController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _otpController.dispose(); + super.dispose(); + } + + void _showToast(String message, {bool isError = false}) { + _overlayEntry?.remove(); + _overlayEntry = null; + + final topPadding = MediaQuery.of(context).padding.top; + + _overlayEntry = OverlayEntry( + builder: (context) => Positioned( + top: topPadding + 20, + left: 16, + right: 16, + child: Material( + color: Colors.transparent, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutBack, + builder: (context, value, child) { + return Opacity( + opacity: value.clamp(0.0, 1.0), + child: Transform.translate( + offset: Offset(0, -(1 - value) * 20), + child: child, + ), + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: isError ? Colors.redAccent.withAlpha(100) : Colors.black.withAlpha(80), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withAlpha(70), + width: 1.0, + ), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(20), blurRadius: 10, spreadRadius: 1) + ], + ), + child: Row( + children: [ + Icon(isError ? Icons.error_outline : Icons.info_outline, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ], + ), + ), + ), + ), + ), + ), + ), + ); + + Navigator.of(context, rootNavigator: true).overlay?.insert(_overlayEntry!); + + Future.delayed(const Duration(seconds: 3), () { + if (_overlayEntry != null && _overlayEntry!.mounted) { + _overlayEntry!.remove(); + _overlayEntry = null; + } + }); + } + + void _showGlobalSuccessToast(OverlayState overlay, String message) { + late OverlayEntry entry; + entry = OverlayEntry( + builder: (context) => Positioned( + bottom: 120.0, + left: 16, + right: 16, + child: Material( + color: Colors.transparent, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutBack, + builder: (context, value, child) { + return Opacity( + opacity: value.clamp(0.0, 1.0), + child: Transform.translate( + offset: Offset(0, (1 - value) * 20), + child: child, + ), + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: Colors.green.shade800.withAlpha(230), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withAlpha(70), + width: 1.0, + ), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(20), blurRadius: 10, spreadRadius: 1) + ], + ), + child: Row( + children: [ + const Icon(Icons.check_circle_outline, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ], + ), + ), + ), + ), + ), + ), + ), + ); + + overlay.insert(entry); + + Future.delayed(const Duration(seconds: 1), () { + if (entry.mounted) { + entry.remove(); + } + }); + } + + bool _validateInputs() { + FocusScope.of(context).unfocus(); + + final nic = _nicController.text.trim(); + final name = _nameController.text.trim(); + final mobile = _mobileController.text.trim(); + final password = _passwordController.text.trim(); + final confirmPassword = _confirmPasswordController.text.trim(); + + if (nic.isEmpty) { + _showToast('NIC Number is required', isError: true); + return false; + } + if (name.isEmpty) { + _showToast('Full Name is required', isError: true); + return false; + } + if (mobile.isEmpty || mobile.length != 9) { + _showToast('Please enter exactly 9 digits for the phone number.', isError: true); + return false; + } + if (password.isEmpty) { + _showToast('Password is required', isError: true); + return false; + } + final passwordRegex = RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$'); + if (!passwordRegex.hasMatch(password)) { + _showToast('Password must be at least 8 characters, contain uppercase, lowercase, number, and special character.', isError: true); + return false; + } + if (confirmPassword.isEmpty) { + _showToast('Please confirm your password.', isError: true); + return false; + } + if (password != confirmPassword) { + _showToast('Passwords do not match.', isError: true); + return false; + } + return true; + } + + Future _handleRegister() async { + if (!_validateInputs()) return; + + setState(() => _isLoading = true); + try { + final deviceId = await DeviceInfoUtil.getDeviceId(); + final nic = _nicController.text.trim(); + final phone = '+94${_mobileController.text.trim()}'; + + _registeredData = { + "nicNo": nic, + "name": _nameController.text.trim(), + "mobilePhoneNo": phone, + "password": _passwordController.text.trim(), + "deviceId": deviceId + }; + + final success = await AuthService.registerUser(_registeredData!); + + if (success && mounted) { + await _sendOTP(phone); + } + } catch (e) { + if (mounted) { + _showToast('Registration Failed. Check NIC.', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _sendOTP(String phone) async { + await FirebaseAuth.instance.verifyPhoneNumber( + phoneNumber: phone, + verificationCompleted: (PhoneAuthCredential credential) async { + await FirebaseAuth.instance.signInWithCredential(credential); + await _verifyBackendAndLogin(); + }, + verificationFailed: (FirebaseAuthException e) { + if (mounted) { + _showToast(e.message ?? 'Verification Failed', isError: true); + setState(() => _isLoading = false); + } + }, + codeSent: (String verificationId, int? resendToken) { + setState(() { + _verificationId = verificationId; + _isLoading = false; + }); + _showOTPDialog(); + }, + codeAutoRetrievalTimeout: (String verificationId) { + _verificationId = verificationId; + }, + ); + } + + Future _verifyOTP() async { + FocusScope.of(context).unfocus(); + + if (_otpController.text.trim().isEmpty) { + _showToast('Please enter the OTP', isError: true); + return; + } + + setState(() => _isLoading = true); + try { + final credential = PhoneAuthProvider.credential( + verificationId: _verificationId, + smsCode: _otpController.text.trim(), + ); + await FirebaseAuth.instance.signInWithCredential(credential); + if (mounted) { + Navigator.pop(context); + } + await _verifyBackendAndLogin(); + } catch (e) { + if (mounted) { + _showToast('Invalid OTP', isError: true); + setState(() => _isLoading = false); + } + } + } + + Future _verifyBackendAndLogin() async { + try { + final isVerified = await AuthService.verifyRegistration(_registeredData!['nicNo']); + if (isVerified && mounted) { + final overlay = Navigator.of(context, rootNavigator: true).overlay; + + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + + if (overlay != null) { + _showGlobalSuccessToast(overlay, 'Registration Successful!'); + } + } + } catch (e) { + if (mounted) { + _showToast('Backend Verification Failed', isError: true); + setState(() => _isLoading = false); + } + } + } + + void _showOTPDialog() { + showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.black.withAlpha(200), + builder: (BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white.withAlpha(25), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withAlpha(50), width: 1.5), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.message, color: Colors.white, size: 40), + const SizedBox(height: 15), + const Text('Enter OTP', style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 15), + const Text( + 'We sent an OTP to your mobile number. Please enter it to complete registration.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70, fontSize: 14), + ), + const SizedBox(height: 20), + TextField( + controller: _otpController, + keyboardType: TextInputType.number, + style: const TextStyle(color: Colors.white, fontSize: 20, letterSpacing: 8), + textAlign: TextAlign.center, + decoration: InputDecoration( + filled: true, + fillColor: Colors.white.withAlpha(20), + hintText: '••••••', + hintStyle: const TextStyle(color: Colors.white54, letterSpacing: 8), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: TextButton( + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.white.withAlpha(100), width: 1.5), + ), + ), + onPressed: () { + Navigator.pop(context); + setState(() => _isLoading = false); + }, + child: const Text('Cancel', style: TextStyle(color: Colors.white)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withAlpha(50), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.white.withAlpha(150), width: 1.5), + ), + ), + onPressed: _verifyOTP, + child: const Text('Verify', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String labelText, + required IconData icon, + bool isPassword = false, + TextInputType keyboardType = TextInputType.text, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextField( + controller: controller, + obscureText: isPassword && _obscurePassword, + keyboardType: keyboardType, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500), + decoration: InputDecoration( + labelText: labelText, + labelStyle: const TextStyle(color: Colors.white70), + prefixIcon: Icon(icon, color: Colors.white70), + filled: true, + fillColor: Colors.white.withAlpha(20), + suffixIcon: isPassword + ? IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ) + : null, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.white.withAlpha(40)), + borderRadius: BorderRadius.circular(16), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.cyanAccent, width: 1.5), + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: true, + backgroundColor: Colors.transparent, + body: Stack( + children: [ + const _RegisterBackground(), + Center( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 40.0), + child: GlassContainer( + width: double.infinity, + padding: const EdgeInsets.all(28.0), + borderRadius: 24.0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Register Driver', + style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 1.2), + ), + const SizedBox(height: 32), + _buildTextField(controller: _nicController, labelText: 'NIC Number', icon: Icons.badge), + _buildTextField(controller: _nameController, labelText: 'Full Name', icon: Icons.person), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 18), + decoration: BoxDecoration( + color: Colors.white.withAlpha(20), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), + border: Border.all(color: Colors.white.withAlpha(40)), + ), + child: const Text('+94', style: TextStyle(color: Colors.white70, fontWeight: FontWeight.bold)), + ), + Expanded( + child: TextField( + controller: _mobileController, + keyboardType: TextInputType.phone, + maxLength: 9, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500), + decoration: InputDecoration( + labelText: 'Phone Number (9 digits)', + labelStyle: const TextStyle(color: Colors.white70), + counterText: '', + filled: true, + fillColor: Colors.white.withAlpha(20), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.white.withAlpha(40)), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.cyanAccent, width: 1.5), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + ), + ), + ), + ], + ), + ), + _buildTextField(controller: _passwordController, labelText: 'Password', icon: Icons.lock, isPassword: true), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextField( + controller: _confirmPasswordController, + obscureText: _obscureConfirmPassword, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500), + decoration: InputDecoration( + labelText: 'Confirm Password', + labelStyle: const TextStyle(color: Colors.white70), + prefixIcon: const Icon(Icons.lock_outline, color: Colors.white70), + filled: true, + fillColor: Colors.white.withAlpha(20), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.white.withAlpha(40)), + borderRadius: BorderRadius.circular(16), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.cyanAccent, width: 1.5), + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ), + const SizedBox(height: 8), + if (_isLoading) + const CircularProgressIndicator(color: Colors.cyanAccent) + else + SizedBox( + width: double.infinity, + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF0F2027), + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + onPressed: _handleRegister, + child: const Text('REGISTER', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 1.5)), + ), + ), + const SizedBox(height: 24), + TextButton( + onPressed: () => Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const LoginScreen()), + ), + child: RichText( + text: const TextSpan( + text: 'Already have an account? ', + style: TextStyle(color: Colors.white70, fontSize: 13), + children: [ + TextSpan( + text: 'Login', + style: TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 13), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class _RegisterBackground extends StatelessWidget { + const _RegisterBackground(); + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: Stack( + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF0F2027), Color(0xFF203A43), Color(0xFF2C5364)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), + Positioned( + top: 50, + right: -80, + child: Container( + width: 300, + height: 300, + decoration: BoxDecoration( + color: Colors.tealAccent.withAlpha(40), + shape: BoxShape.circle, + ), + ), + ), + Positioned( + bottom: -100, + left: -50, + child: Container( + width: 350, + height: 350, + decoration: BoxDecoration( + color: Colors.blueAccent.withAlpha(50), + shape: BoxShape.circle, + ), + ), + ), + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), + child: const SizedBox(), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/screens/splash_screen.dart b/frontend_apps/auto_ledger/lib/screens/splash_screen.dart new file mode 100644 index 0000000..7858c80 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/screens/splash_screen.dart @@ -0,0 +1,296 @@ +import 'dart:ui'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import '../utils/secure_storage.dart'; +import 'login_screen.dart'; +import 'home_screen.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State + with TickerProviderStateMixin { // ✅ Changed from SingleTickerProviderStateMixin + late AnimationController _animationController; + late Animation _scaleAnimation; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + late AnimationController _dotsController; + + @override + void initState() { + super.initState(); + _initAnimations(); + _checkAuthStatus(); + } + + void _initAnimations() { + _animationController = AnimationController( + duration: const Duration(milliseconds: 2500), + vsync: this, + ); + + _scaleAnimation = Tween(begin: 0.6, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.5, curve: Curves.easeOutBack), + ), + ); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOut), + ), + ); + + _slideAnimation = Tween(begin: 30.0, end: 0.0).animate( + CurvedAnimation( + parent: _animationController, + curve: const Interval(0.3, 0.7, curve: Curves.easeOut), + ), + ); + + _animationController.forward(); + + _dotsController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + )..repeat(); + } + + @override + void dispose() { + _animationController.dispose(); + _dotsController.dispose(); + super.dispose(); + } + + Future _checkAuthStatus() async { + await Future.delayed(const Duration(seconds: 3)); + final token = await SecureStorage.getToken(); + + if (!mounted) return; + + if (token != null) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + _buildBackground(), + Center( + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Opacity( + opacity: _fadeAnimation.value, + child: Transform.translate( + offset: Offset(0, _slideAnimation.value), + child: Transform.scale( + scale: _scaleAnimation.value, + child: child, + ), + ), + ); + }, + child: _buildGlassContent(), + ), + ), + ], + ), + ); + } + + Widget _buildBackground() { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFF0F2027), + Color(0xFF203A43), + Color(0xFF2C5364), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ); + } + + Widget _buildGlassContent() { + return ClipRRect( + borderRadius: BorderRadius.circular(32), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25), + child: Container( + width: MediaQuery.of(context).size.width * 0.75, + padding: const EdgeInsets.symmetric(vertical: 48, horizontal: 32), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white.withAlpha(30), + Colors.white.withAlpha(15), + Colors.white.withAlpha(8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(32), + border: Border.all( + color: Colors.white.withAlpha(60), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(40), + blurRadius: 50, + spreadRadius: 10, + offset: const Offset(0, 20), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildAnimatedIcon(), + const SizedBox(height: 24), + _buildTitleText(), + const SizedBox(height: 12), + _buildSubtitleText(), + const SizedBox(height: 32), + _buildLoadingIndicator(), + ], + ), + ), + ), + ); + } + + Widget _buildAnimatedIcon() { + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 2000), + curve: Curves.easeOutBack, + builder: (context, value, child) { + return Transform.scale( + scale: 0.7 + (value * 0.3), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.cyanAccent.withAlpha(40), + Colors.blueAccent.withAlpha(20), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withAlpha(60), + width: 2.0, + ), + boxShadow: [ + BoxShadow( + color: Colors.cyanAccent.withAlpha(30), + blurRadius: 40, + spreadRadius: 5, + ), + ], + ), + child: const Icon( + Icons.directions_car, + size: 64, + color: Colors.white, + ), + ), + ); + }, + ); + } + + Widget _buildTitleText() { + return ShaderMask( + shaderCallback: (bounds) => const LinearGradient( + colors: [Colors.white, Colors.cyanAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ).createShader(bounds), + child: const Text( + 'Auto-Ledger', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w900, + color: Colors.white, + letterSpacing: 1.5, + ), + ), + ); + } + + Widget _buildSubtitleText() { + return const Text( + 'Your Digital Driving Companion', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white70, + letterSpacing: 0.5, + ), + ); + } + + Widget _buildLoadingIndicator() { + return AnimatedBuilder( + animation: _dotsController, + builder: (context, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(3, (index) { + final phase = index * 0.33; + final rawValue = (_dotsController.value + phase) % 1.0; + final pulse = (1 - math.cos(rawValue * 2 * math.pi)) / 2; + final size = 12 + pulse * 8; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 6), + width: size, + height: size, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Colors.cyanAccent, Colors.blueAccent], + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.cyanAccent.withAlpha(30), + blurRadius: 15, + spreadRadius: 2, + ), + ], + ), + ); + }), + ); + }, + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/services/api_service.dart b/frontend_apps/auto_ledger/lib/services/api_service.dart new file mode 100644 index 0000000..8b79c84 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/services/api_service.dart @@ -0,0 +1,25 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../utils/secure_storage.dart'; + +class ApiService { + static final Dio _dio = Dio(BaseOptions( + baseUrl: dotenv.env['API_URL'] ?? 'http://localhost:3000', + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), + )); + + static void init() { + _dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) async { + final token = await SecureStorage.getToken(); + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } + return handler.next(options); + }, + )); + } + + static Dio get dio => _dio; +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/services/auth_service.dart b/frontend_apps/auto_ledger/lib/services/auth_service.dart new file mode 100644 index 0000000..c01107a --- /dev/null +++ b/frontend_apps/auto_ledger/lib/services/auth_service.dart @@ -0,0 +1,130 @@ +import 'package:dio/dio.dart'; +import 'api_service.dart'; +import '../utils/secure_storage.dart'; + +class AuthService { + static Future registerUser(Map data) async { + try { + final response = await ApiService.dio.post('/auth/user/register', data: data); + return response.statusCode == 201 || response.statusCode == 200; + } on DioException { + rethrow; + } + } + + static Future verifyRegistration(String nicNo) async { + try { + final response = await ApiService.dio.post( + '/auth/user/verify-registration', + data: {'nicNo': nicNo}, + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final token = response.data['accessToken']; + await SecureStorage.saveToken(token); + return true; + } + return false; + } on DioException { + rethrow; + } + } + + static Future> loginUser(String nicNo, String password, String deviceId) async { + try { + final response = await ApiService.dio.post( + '/auth/user/login', + data: { + 'nicNo': nicNo, + 'password': password, + 'deviceId': deviceId, + }, + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final token = response.data['accessToken']; + await SecureStorage.saveToken(token); + return {'success': true}; + } + return {'success': false}; + } on DioException catch (e) { + if (e.response?.statusCode == 403 && e.response?.data['code'] == 'DEVICE_MISMATCH') { + return { + 'success': false, + 'isDeviceMismatch': true, + 'phone': e.response?.data['phone'] + }; + } + rethrow; + } + } + + static Future verifyNewDevice(String nicNo, String deviceId) async { + try { + final response = await ApiService.dio.post( + '/auth/user/verify-device', + data: { + 'nicNo': nicNo, + 'deviceId': deviceId, + }, + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final token = response.data['accessToken']; + await SecureStorage.saveToken(token); + return true; + } + return false; + } on DioException { + rethrow; + } + } + + static Future forgotPasswordCheck(String nicNo, String phone) async { + try { + final response = await ApiService.dio.post( + '/auth/user/forgot-password-check', + data: { + 'nicNo': nicNo, + 'mobilePhoneNo': phone, + }, + ); + return response.statusCode == 200 || response.statusCode == 201; + } on DioException { + rethrow; + } + } + + static Future resetPassword(String nicNo, String phone, String newPassword) async { + try { + final response = await ApiService.dio.post( + '/auth/user/reset-password', + data: { + 'nicNo': nicNo, + 'mobilePhoneNo': phone, + 'newPassword': newPassword, + }, + ); + return response.statusCode == 200 || response.statusCode == 201; + } on DioException { + rethrow; + } + } + + static Future> biometricLogin(String nicNo, String deviceId) async { + try { + final response = await ApiService.dio.post( + '/auth/user/biometric-login', + data: { + 'nicNo': nicNo, + 'deviceId': deviceId, + }, + ); + if (response.statusCode == 200 || response.statusCode == 201) { + final token = response.data['accessToken']; + await SecureStorage.saveToken(token); + return {'success': true}; + } + return {'success': false}; + } on DioException { + rethrow; + } + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/services/biometric_service.dart b/frontend_apps/auto_ledger/lib/services/biometric_service.dart new file mode 100644 index 0000000..f8a1f78 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/services/biometric_service.dart @@ -0,0 +1,35 @@ +import 'package:local_auth/local_auth.dart'; + +class BiometricService { + final LocalAuthentication _auth = LocalAuthentication(); + + Future checkBiometricsAvailable() async { + try { + final isDeviceSupported = await _auth.isDeviceSupported(); + if (!isDeviceSupported) { + return false; + } + return await _auth.canCheckBiometrics; + } catch (e) { + return false; + } + } + + Future authenticate() async { + try { + final isDeviceSupported = await _auth.isDeviceSupported(); + if (!isDeviceSupported) { + return false; + } + return await _auth.authenticate( + localizedReason: 'Please authenticate to login to Auto-Ledger', + options: const AuthenticationOptions( + stickyAuth: true, + biometricOnly: true, + ), + ); + } catch (e) { + return false; + } + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/services/pdf_service.dart b/frontend_apps/auto_ledger/lib/services/pdf_service.dart new file mode 100644 index 0000000..8bc2e04 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/services/pdf_service.dart @@ -0,0 +1,117 @@ +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:printing/printing.dart'; + +class PdfService { + static Future generateAndPrintReceipt(Map fine) async { + final pdf = pw.Document(); + + final totalAmount = fine['amount'] ?? 0.0; + final fineId = fine['id'] ?? 'N/A'; + final issueDate = fine['date'] ?? DateTime.now().toIso8601String(); + final officer = fine['officer'] ?? 'Unknown Officer'; + final offenses = fine['offenses'] as List? ?? []; + + pdf.addPage( + pw.Page( + build: (pw.Context context) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Center( + child: pw.Column( + children: [ + pw.Text( + 'AUTO-LEDGER', + style: pw.TextStyle( + fontSize: 28, + fontWeight: pw.FontWeight.bold, + color: PdfColors.blue900, + ), + ), + pw.Text( + 'Payment Receipt', + style: pw.TextStyle( + fontSize: 18, + fontWeight: pw.FontWeight.bold, + color: PdfColors.grey700, + ), + ), + ], + ), + ), + pw.SizedBox(height: 20), + pw.Divider(thickness: 2), + pw.SizedBox(height: 20), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('Receipt ID:', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.Text('#${fineId.substring(0, 8).toUpperCase()}'), + ], + ), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('Date:', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.Text(_formatDate(issueDate)), + ], + ), + pw.SizedBox(height: 20), + pw.Text('OFFICER', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.Text(officer), + pw.SizedBox(height: 20), + pw.Text('OFFENSES', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 8), + ...offenses.map((o) { + return pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text('${o['code']} - ${o['name']}'), + pw.Text('+${o['points']} pts'), + ], + ); + }).toList(), + pw.SizedBox(height: 20), + pw.Divider(thickness: 1), + pw.SizedBox(height: 10), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text( + 'TOTAL AMOUNT', + style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 16), + ), + pw.Text( + 'Rs. ${totalAmount.toStringAsFixed(2)}', + style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 16, color: PdfColors.green700), + ), + ], + ), + pw.SizedBox(height: 20), + pw.Center( + child: pw.Text( + 'Thank you for paying your traffic fine.', + style: pw.TextStyle(color: PdfColors.grey500), + ), + ), + ], + ); + }, + ), + ); + + await Printing.layoutPdf( + onLayout: (PdfPageFormat format) async => pdf.save(), + ); + } + + static String _formatDate(String isoString) { + try { + final date = DateTime.parse(isoString); + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; + } catch (_) { + return '---'; + } + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/services/user_service.dart b/frontend_apps/auto_ledger/lib/services/user_service.dart new file mode 100644 index 0000000..8b64a8a --- /dev/null +++ b/frontend_apps/auto_ledger/lib/services/user_service.dart @@ -0,0 +1,31 @@ +import 'package:dio/dio.dart'; +import 'api_service.dart'; + +class UserService { + static Future?> getUserProfile() async { + try { + final response = await ApiService.dio.get('/users/profile'); + if (response.statusCode == 200) { + return response.data; + } + return null; + } on DioException { + rethrow; + } + } + + static Future changePassword(String oldPassword, String newPassword) async { + try { + final response = await ApiService.dio.patch( + '/auth/user/change-password', + data: { + 'oldPassword': oldPassword, + 'newPassword': newPassword, + }, + ); + return response.statusCode == 200; + } on DioException { + rethrow; + } + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/utils/device_info.dart b/frontend_apps/auto_ledger/lib/utils/device_info.dart new file mode 100644 index 0000000..0666db4 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/utils/device_info.dart @@ -0,0 +1,16 @@ +import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; + +class DeviceInfoUtil { + static Future getDeviceId() async { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + return androidInfo.id; + } else if (Platform.isIOS) { + final IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + return iosInfo.identifierForVendor ?? 'UNKNOWN_IOS_DEVICE'; + } + return 'UNKNOWN_DEVICE'; + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/utils/secure_storage.dart b/frontend_apps/auto_ledger/lib/utils/secure_storage.dart new file mode 100644 index 0000000..4c64c12 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/utils/secure_storage.dart @@ -0,0 +1,23 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class SecureStorage { + static const _storage = FlutterSecureStorage(); + + static Future saveToken(String token) async => + await _storage.write(key: 'jwt_token', value: token); + + static Future getToken() async => + await _storage.read(key: 'jwt_token'); + + static Future deleteToken() async => + await _storage.delete(key: 'jwt_token'); + + static Future saveNic(String nic) async => + await _storage.write(key: 'nic_no', value: nic); + + static Future getNic() async => + await _storage.read(key: 'nic_no'); + + static Future deleteNic() async => + await _storage.delete(key: 'nic_no'); +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/utils/settings_util.dart b/frontend_apps/auto_ledger/lib/utils/settings_util.dart new file mode 100644 index 0000000..537e5de --- /dev/null +++ b/frontend_apps/auto_ledger/lib/utils/settings_util.dart @@ -0,0 +1,15 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingsUtil { + static const String _biometricKey = 'biometric_enabled'; + + static Future isBiometricEnabled() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_biometricKey) ?? false; + } + + static Future setBiometricEnabled(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_biometricKey, value); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/widgets/glass_card.dart b/frontend_apps/auto_ledger/lib/widgets/glass_card.dart new file mode 100644 index 0000000..9bc5470 --- /dev/null +++ b/frontend_apps/auto_ledger/lib/widgets/glass_card.dart @@ -0,0 +1,42 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; + +class GlassCard extends StatelessWidget { + final Widget child; + final Color borderColor; + final Color backgroundColor; + + const GlassCard({ + Key? key, + required this.child, + this.borderColor = Colors.white30, + this.backgroundColor = const Color(0x26FFFFFF), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: borderColor, width: 1.5), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + spreadRadius: -5, + ) + ], + ), + child: child, + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/lib/widgets/glass_container.dart b/frontend_apps/auto_ledger/lib/widgets/glass_container.dart new file mode 100644 index 0000000..c0f56bb --- /dev/null +++ b/frontend_apps/auto_ledger/lib/widgets/glass_container.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; + +class GlassContainer extends StatelessWidget { + final Widget child; + final double? width; + final double? height; + final double borderRadius; + final EdgeInsetsGeometry padding; + + const GlassContainer({ + super.key, + required this.child, + this.width = double.infinity, + this.height, + this.borderRadius = 16.0, + this.padding = const EdgeInsets.all(16.0), + }); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: Container( + width: width, + height: height, + padding: padding, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(borderRadius), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1.5, + ), + ), + child: child, + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend_apps/auto_ledger/linux/.gitignore b/frontend_apps/auto_ledger/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/frontend_apps/auto_ledger/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/frontend_apps/auto_ledger/linux/CMakeLists.txt b/frontend_apps/auto_ledger/linux/CMakeLists.txt new file mode 100644 index 0000000..701aa7d --- /dev/null +++ b/frontend_apps/auto_ledger/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "auto_ledger") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.auto_ledger") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/frontend_apps/auto_ledger/linux/flutter/CMakeLists.txt b/frontend_apps/auto_ledger/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/frontend_apps/auto_ledger/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/frontend_apps/auto_ledger/linux/flutter/generated_plugin_registrant.cc b/frontend_apps/auto_ledger/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..47a9c3d --- /dev/null +++ b/frontend_apps/auto_ledger/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) printing_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); + printing_plugin_register_with_registrar(printing_registrar); +} diff --git a/frontend_apps/auto_ledger/linux/flutter/generated_plugin_registrant.h b/frontend_apps/auto_ledger/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/frontend_apps/auto_ledger/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/frontend_apps/auto_ledger/linux/flutter/generated_plugins.cmake b/frontend_apps/auto_ledger/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..3518f0c --- /dev/null +++ b/frontend_apps/auto_ledger/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux + printing +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/frontend_apps/auto_ledger/linux/runner/CMakeLists.txt b/frontend_apps/auto_ledger/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/frontend_apps/auto_ledger/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/frontend_apps/auto_ledger/linux/runner/main.cc b/frontend_apps/auto_ledger/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/frontend_apps/auto_ledger/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/frontend_apps/auto_ledger/linux/runner/my_application.cc b/frontend_apps/auto_ledger/linux/runner/my_application.cc new file mode 100644 index 0000000..6c81082 --- /dev/null +++ b/frontend_apps/auto_ledger/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/frontend_apps/auto_ledger/linux/runner/my_application.h b/frontend_apps/auto_ledger/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/frontend_apps/auto_ledger/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/frontend_apps/auto_ledger/macos/.gitignore b/frontend_apps/auto_ledger/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/frontend_apps/auto_ledger/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/frontend_apps/auto_ledger/macos/Flutter/Flutter-Debug.xcconfig b/frontend_apps/auto_ledger/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend_apps/auto_ledger/macos/Flutter/Flutter-Release.xcconfig b/frontend_apps/auto_ledger/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend_apps/auto_ledger/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend_apps/auto_ledger/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..36af76d --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import device_info_plus +import firebase_auth +import firebase_core +import flutter_secure_storage_darwin +import local_auth_darwin +import path_provider_foundation +import printing +import shared_preferences_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) +} diff --git a/frontend_apps/auto_ledger/macos/Runner.xcodeproj/project.pbxproj b/frontend_apps/auto_ledger/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..fa6c7c1 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* auto_ledger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "auto_ledger.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* auto_ledger.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* auto_ledger.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/auto_ledger.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/auto_ledger"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/auto_ledger.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/auto_ledger"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/auto_ledger.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/auto_ledger"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/frontend_apps/auto_ledger/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend_apps/auto_ledger/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend_apps/auto_ledger/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend_apps/auto_ledger/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..b572e5d --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend_apps/auto_ledger/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend_apps/auto_ledger/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend_apps/auto_ledger/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend_apps/auto_ledger/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend_apps/auto_ledger/macos/Runner/AppDelegate.swift b/frontend_apps/auto_ledger/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/frontend_apps/auto_ledger/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/frontend_apps/auto_ledger/macos/Runner/Base.lproj/MainMenu.xib b/frontend_apps/auto_ledger/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend_apps/auto_ledger/macos/Runner/Configs/AppInfo.xcconfig b/frontend_apps/auto_ledger/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..6101c66 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = auto_ledger + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.autoLedger + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/frontend_apps/auto_ledger/macos/Runner/Configs/Debug.xcconfig b/frontend_apps/auto_ledger/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/frontend_apps/auto_ledger/macos/Runner/Configs/Release.xcconfig b/frontend_apps/auto_ledger/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/frontend_apps/auto_ledger/macos/Runner/Configs/Warnings.xcconfig b/frontend_apps/auto_ledger/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/frontend_apps/auto_ledger/macos/Runner/DebugProfile.entitlements b/frontend_apps/auto_ledger/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/frontend_apps/auto_ledger/macos/Runner/Info.plist b/frontend_apps/auto_ledger/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/frontend_apps/auto_ledger/macos/Runner/MainFlutterWindow.swift b/frontend_apps/auto_ledger/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/frontend_apps/auto_ledger/macos/Runner/Release.entitlements b/frontend_apps/auto_ledger/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/frontend_apps/auto_ledger/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/frontend_apps/auto_ledger/macos/RunnerTests/RunnerTests.swift b/frontend_apps/auto_ledger/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/frontend_apps/auto_ledger/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/frontend_apps/auto_ledger/pubspec.lock b/frontend_apps/auto_ledger/pubspec.lock new file mode 100644 index 0000000..e231c62 --- /dev/null +++ b/frontend_apps/auto_ledger/pubspec.lock @@ -0,0 +1,786 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "0d1f0adfabbab9f46a1a80ce84a4d8b852b6e4dbf53ce413b30e0cf7d3631b71" + url: "https://pub.dev" + source: hosted + version: "1.3.72" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.dev" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" + url: "https://pub.dev" + source: hosted + version: "2.0.13" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "9905a315f1235a649e29df19b246adde7350a11234837cd605254b8e25144711" + url: "https://pub.dev" + source: hosted + version: "6.5.2" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: f757dc5636ce2b204a82383f7246ef0d9c925f9e96c8c9a4a6c315d673d662bb + url: "https://pub.dev" + source: hosted + version: "9.0.2" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: "1c44330eef3c82068e0c2919b6b015897d49211dcccdf64f90a2ed73ce216ecb" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: ec46a100a560d3bd5f97f2d89ba7492cb09b6dd0a4a28753d1258f360d6bd9f9 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "5ad1be848692ec148f2d6a8ad2a3838cb852ea5f3c9e6479a7afce479e1854f8" + url: "https://pub.dev" + source: hosted + version: "3.8.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: d41da11fb497314fbf89811ec30af02d1d898b47980a129f0a8c0a1720460ba2 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 + url: "https://pub.dev" + source: hosted + version: "2.0.31" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e" + url: "https://pub.dev" + source: hosted + version: "10.3.1" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149" + url: "https://pub.dev" + source: hosted + version: "0.3.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + local_auth: + dependency: "direct main" + description: + name: local_auth + sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3" + url: "https://pub.dev" + source: hosted + version: "1.0.52" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122 + url: "https://pub.dev" + source: hosted + version: "1.1.0" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" + url: "https://pub.dev" + source: hosted + version: "2.2.19" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" + url: "https://pub.dev" + source: hosted + version: "3.11.3" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + printing: + dependency: "direct main" + description: + name: printing + sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692" + url: "https://pub.dev" + source: hosted + version: "5.14.3" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e + url: "https://pub.dev" + source: hosted + version: "2.4.13" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/frontend_apps/auto_ledger/pubspec.yaml b/frontend_apps/auto_ledger/pubspec.yaml new file mode 100644 index 0000000..5f27fd7 --- /dev/null +++ b/frontend_apps/auto_ledger/pubspec.yaml @@ -0,0 +1,49 @@ +name: example +description: "A new Flutter project." +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ">=3.5.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.8 + firebase_core: ^4.9.0 + dio: ^5.9.2 + shared_preferences: ^2.5.3 + flutter_secure_storage: ^10.3.1 + device_info_plus: ^11.0.0 + local_auth: ^2.3.0 + qr_flutter: ^4.1.0 + google_fonts: ^6.2.1 + flutter_dotenv: ^6.0.1 + firebase_auth: ^6.5.2 + + # PDF Receipt + pdf: ^3.10.4 + printing: ^5.11.1 + path_provider: ^2.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + flutter_launcher_icons: ^0.14.4 + +flutter: + uses-material-design: true + + assets: + - assets/flag.png + - assets/emblem.png + - assets/punkalasa.png + - assets/icon/app_icon.png + - .env + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/icon/app_icon.png" \ No newline at end of file diff --git a/frontend_apps/auto_ledger/web/favicon.png b/frontend_apps/auto_ledger/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/frontend_apps/auto_ledger/web/favicon.png differ diff --git a/frontend_apps/auto_ledger/web/icons/Icon-192.png b/frontend_apps/auto_ledger/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/frontend_apps/auto_ledger/web/icons/Icon-192.png differ diff --git a/frontend_apps/auto_ledger/web/icons/Icon-512.png b/frontend_apps/auto_ledger/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/frontend_apps/auto_ledger/web/icons/Icon-512.png differ diff --git a/frontend_apps/auto_ledger/web/icons/Icon-maskable-192.png b/frontend_apps/auto_ledger/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/frontend_apps/auto_ledger/web/icons/Icon-maskable-192.png differ diff --git a/frontend_apps/auto_ledger/web/icons/Icon-maskable-512.png b/frontend_apps/auto_ledger/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/frontend_apps/auto_ledger/web/icons/Icon-maskable-512.png differ diff --git a/frontend_apps/auto_ledger/web/index.html b/frontend_apps/auto_ledger/web/index.html new file mode 100644 index 0000000..a135495 --- /dev/null +++ b/frontend_apps/auto_ledger/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + auto_ledger + + + + + + diff --git a/frontend_apps/auto_ledger/web/manifest.json b/frontend_apps/auto_ledger/web/manifest.json new file mode 100644 index 0000000..0b1b629 --- /dev/null +++ b/frontend_apps/auto_ledger/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "auto_ledger", + "short_name": "auto_ledger", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/frontend_apps/auto_ledger/windows/.gitignore b/frontend_apps/auto_ledger/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/frontend_apps/auto_ledger/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/frontend_apps/auto_ledger/windows/CMakeLists.txt b/frontend_apps/auto_ledger/windows/CMakeLists.txt new file mode 100644 index 0000000..1d544ff --- /dev/null +++ b/frontend_apps/auto_ledger/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(auto_ledger LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "auto_ledger") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/frontend_apps/auto_ledger/windows/flutter/CMakeLists.txt b/frontend_apps/auto_ledger/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/frontend_apps/auto_ledger/windows/flutter/generated_plugin_registrant.cc b/frontend_apps/auto_ledger/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..0c6316b --- /dev/null +++ b/frontend_apps/auto_ledger/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + LocalAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("LocalAuthPlugin")); + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); +} diff --git a/frontend_apps/auto_ledger/windows/flutter/generated_plugin_registrant.h b/frontend_apps/auto_ledger/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/frontend_apps/auto_ledger/windows/flutter/generated_plugins.cmake b/frontend_apps/auto_ledger/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..da70073 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + firebase_auth + firebase_core + flutter_secure_storage_windows + local_auth_windows + printing +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/frontend_apps/auto_ledger/windows/runner/CMakeLists.txt b/frontend_apps/auto_ledger/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/frontend_apps/auto_ledger/windows/runner/Runner.rc b/frontend_apps/auto_ledger/windows/runner/Runner.rc new file mode 100644 index 0000000..0e3e27b --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/frontend_apps/auto_ledger/windows/runner/flutter_window.cpp b/frontend_apps/auto_ledger/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/frontend_apps/auto_ledger/windows/runner/flutter_window.h b/frontend_apps/auto_ledger/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/frontend_apps/auto_ledger/windows/runner/main.cpp b/frontend_apps/auto_ledger/windows/runner/main.cpp new file mode 100644 index 0000000..a61bf80 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/frontend_apps/auto_ledger/windows/runner/resource.h b/frontend_apps/auto_ledger/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/frontend_apps/auto_ledger/windows/runner/resources/app_icon.ico b/frontend_apps/auto_ledger/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/frontend_apps/auto_ledger/windows/runner/resources/app_icon.ico differ diff --git a/frontend_apps/auto_ledger/windows/runner/runner.exe.manifest b/frontend_apps/auto_ledger/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/frontend_apps/auto_ledger/windows/runner/utils.cpp b/frontend_apps/auto_ledger/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/frontend_apps/auto_ledger/windows/runner/utils.h b/frontend_apps/auto_ledger/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/frontend_apps/auto_ledger/windows/runner/win32_window.cpp b/frontend_apps/auto_ledger/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/frontend_apps/auto_ledger/windows/runner/win32_window.h b/frontend_apps/auto_ledger/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/frontend_apps/auto_ledger/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_