feat(auth): complete Phase 1 passwordless identity and encrypted DB foundation
This commit is contained in:
parent
98e0d318f3
commit
7b6b646d2c
|
|
@ -1,20 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../scaffold/adaptive_home_scaffold.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../../../core/network/supabase_status.dart';
|
||||
import '../../features/auth/presentation/controllers/auth_controller.dart';
|
||||
import '../../features/auth/presentation/screens/login_screen.dart';
|
||||
import '../../features/medication/presentation/screens/medication_screen.dart';
|
||||
import '../../features/nutrition/presentation/screens/nutrition_screen.dart';
|
||||
import '../../features/meal_planner/presentation/screens/meal_planner_screen.dart';
|
||||
import '../../features/pantry/presentation/screens/pantry_screen.dart';
|
||||
import '../scaffold/adaptive_home_scaffold.dart';
|
||||
|
||||
/// Locations that may be visited without an authenticated session.
|
||||
const Set<String> _publicPaths = {'/login'};
|
||||
|
||||
final _rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final _shellNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
/// Application router with a reactive authentication guard.
|
||||
///
|
||||
/// The guard re-evaluates on every GoTrue auth transition (bridged into
|
||||
/// [refreshListenable] below), so a successful magic-link or OAuth round trip
|
||||
/// immediately navigates into the protected shell, and signing out bounces the
|
||||
/// user back to `/login`.
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
final authRefreshNotifier = ValueNotifier<int>(0);
|
||||
ref.onDispose(authRefreshNotifier.dispose);
|
||||
ref.listen(authStateProvider, (_, __) => authRefreshNotifier.value++);
|
||||
|
||||
return GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
initialLocation: '/medication',
|
||||
refreshListenable: authRefreshNotifier,
|
||||
redirect: _authGuard,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
|
|
@ -47,3 +66,21 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||
],
|
||||
);
|
||||
});
|
||||
|
||||
/// Redirect guard enforcing the passwordless session lifecycle:
|
||||
/// * Unauthenticated sessions on a protected route -> `/login`.
|
||||
/// * Authenticated sessions on `/login` -> `/medication`.
|
||||
String? _authGuard(BuildContext context, GoRouterState state) {
|
||||
final location = state.matchedLocation;
|
||||
final isAuthenticated = isSupabaseAvailable &&
|
||||
Supabase.instance.client.auth.currentSession != null;
|
||||
|
||||
if (!isAuthenticated && !_publicPaths.contains(location)) {
|
||||
return '/login';
|
||||
}
|
||||
if (isAuthenticated && location == '/login') {
|
||||
return '/medication';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'connection/unsupported.dart'
|
||||
if (dart.library.ffi) 'connection/native.dart'
|
||||
if (dart.library.js_interop) 'connection/web.dart';
|
||||
|
||||
part 'app_database.g.dart';
|
||||
|
||||
/// Immutable log of GLP-1 injections and scheduled doses.
|
||||
///
|
||||
/// [injectionSite] stores the anatomical rotation quadrant as a stable string
|
||||
/// value (the `InjectionQuadrant.name` identifier of the medication domain
|
||||
/// enum) so the persistence schema stays decoupled from domain types and the
|
||||
/// `core` layer never imports feature code.
|
||||
@DataClassName('MedicationDoseRecord')
|
||||
class MedicationDosesTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
/// Commercial or compounded name of the GLP-1/GIP agonist (e.g. Tirzepatide).
|
||||
TextColumn get compoundName => text().withLength(min: 1, max: 128)();
|
||||
|
||||
/// Total injected quantity in milligrams.
|
||||
RealColumn get doseMg => real()();
|
||||
|
||||
/// When the dose was prescribed to be taken (titration schedule anchor).
|
||||
DateTimeColumn get scheduledTime => dateTime()();
|
||||
|
||||
/// When the dose was actually administered; `null` while still pending.
|
||||
DateTimeColumn get administeredTime => dateTime().nullable()();
|
||||
|
||||
/// Rotation quadrant identifier (e.g. `abdomenLeftLower`).
|
||||
TextColumn get injectionSite => text().nullable()();
|
||||
|
||||
TextColumn get notes => text().nullable()();
|
||||
}
|
||||
|
||||
/// Root drift database for the GLP-1 Companion.
|
||||
///
|
||||
/// On native platforms the connection is backed by SQLCipher: the 256-bit AES
|
||||
/// master key provisioned by `CryptoManager` is applied via `PRAGMA key`
|
||||
/// immediately after the database opens (see `connection/native.dart`). Web
|
||||
/// builds transparently fall back to a WASM SQLite database, since SQLCipher
|
||||
/// cannot run inside the browser.
|
||||
@DriftDatabase(tables: [MedicationDosesTable])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
/// Opens the platform-appropriate (encrypted on native) database connection.
|
||||
AppDatabase() : super(openDatabaseConnection());
|
||||
|
||||
/// Injects a custom executor; used by unit tests with an in-memory database.
|
||||
AppDatabase.forTesting(super.executor);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
|
||||
/// Application-wide database instance; closed when the container is disposed.
|
||||
final appDatabaseProvider = Provider<AppDatabase>((ref) {
|
||||
final database = AppDatabase();
|
||||
ref.onDispose(database.close);
|
||||
return database;
|
||||
});
|
||||
|
|
@ -0,0 +1,654 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_database.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
class $MedicationDosesTableTable extends MedicationDosesTable
|
||||
with TableInfo<$MedicationDosesTableTable, MedicationDoseRecord> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$MedicationDosesTableTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const VerificationMeta _compoundNameMeta =
|
||||
const VerificationMeta('compoundName');
|
||||
@override
|
||||
late final GeneratedColumn<String> compoundName = GeneratedColumn<String>(
|
||||
'compound_name', aliasedName, false,
|
||||
additionalChecks:
|
||||
GeneratedColumn.checkTextLength(minTextLength: 1, maxTextLength: 128),
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true);
|
||||
static const VerificationMeta _doseMgMeta = const VerificationMeta('doseMg');
|
||||
@override
|
||||
late final GeneratedColumn<double> doseMg = GeneratedColumn<double>(
|
||||
'dose_mg', aliasedName, false,
|
||||
type: DriftSqlType.double, requiredDuringInsert: true);
|
||||
static const VerificationMeta _scheduledTimeMeta =
|
||||
const VerificationMeta('scheduledTime');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> scheduledTime =
|
||||
GeneratedColumn<DateTime>('scheduled_time', aliasedName, false,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: true);
|
||||
static const VerificationMeta _administeredTimeMeta =
|
||||
const VerificationMeta('administeredTime');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> administeredTime =
|
||||
GeneratedColumn<DateTime>('administered_time', aliasedName, true,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||
static const VerificationMeta _injectionSiteMeta =
|
||||
const VerificationMeta('injectionSite');
|
||||
@override
|
||||
late final GeneratedColumn<String> injectionSite = GeneratedColumn<String>(
|
||||
'injection_site', aliasedName, true,
|
||||
type: DriftSqlType.string, requiredDuringInsert: false);
|
||||
static const VerificationMeta _notesMeta = const VerificationMeta('notes');
|
||||
@override
|
||||
late final GeneratedColumn<String> notes = GeneratedColumn<String>(
|
||||
'notes', aliasedName, true,
|
||||
type: DriftSqlType.string, requiredDuringInsert: false);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
compoundName,
|
||||
doseMg,
|
||||
scheduledTime,
|
||||
administeredTime,
|
||||
injectionSite,
|
||||
notes
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'medication_doses_table';
|
||||
@override
|
||||
VerificationContext validateIntegrity(
|
||||
Insertable<MedicationDoseRecord> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('compound_name')) {
|
||||
context.handle(
|
||||
_compoundNameMeta,
|
||||
compoundName.isAcceptableOrUnknown(
|
||||
data['compound_name']!, _compoundNameMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_compoundNameMeta);
|
||||
}
|
||||
if (data.containsKey('dose_mg')) {
|
||||
context.handle(_doseMgMeta,
|
||||
doseMg.isAcceptableOrUnknown(data['dose_mg']!, _doseMgMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_doseMgMeta);
|
||||
}
|
||||
if (data.containsKey('scheduled_time')) {
|
||||
context.handle(
|
||||
_scheduledTimeMeta,
|
||||
scheduledTime.isAcceptableOrUnknown(
|
||||
data['scheduled_time']!, _scheduledTimeMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_scheduledTimeMeta);
|
||||
}
|
||||
if (data.containsKey('administered_time')) {
|
||||
context.handle(
|
||||
_administeredTimeMeta,
|
||||
administeredTime.isAcceptableOrUnknown(
|
||||
data['administered_time']!, _administeredTimeMeta));
|
||||
}
|
||||
if (data.containsKey('injection_site')) {
|
||||
context.handle(
|
||||
_injectionSiteMeta,
|
||||
injectionSite.isAcceptableOrUnknown(
|
||||
data['injection_site']!, _injectionSiteMeta));
|
||||
}
|
||||
if (data.containsKey('notes')) {
|
||||
context.handle(
|
||||
_notesMeta, notes.isAcceptableOrUnknown(data['notes']!, _notesMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
MedicationDoseRecord map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return MedicationDoseRecord(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
compoundName: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}compound_name'])!,
|
||||
doseMg: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.double, data['${effectivePrefix}dose_mg'])!,
|
||||
scheduledTime: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime, data['${effectivePrefix}scheduled_time'])!,
|
||||
administeredTime: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime, data['${effectivePrefix}administered_time']),
|
||||
injectionSite: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}injection_site']),
|
||||
notes: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}notes']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$MedicationDosesTableTable createAlias(String alias) {
|
||||
return $MedicationDosesTableTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class MedicationDoseRecord extends DataClass
|
||||
implements Insertable<MedicationDoseRecord> {
|
||||
final int id;
|
||||
|
||||
/// Commercial or compounded name of the GLP-1/GIP agonist (e.g. Tirzepatide).
|
||||
final String compoundName;
|
||||
|
||||
/// Total injected quantity in milligrams.
|
||||
final double doseMg;
|
||||
|
||||
/// When the dose was prescribed to be taken (titration schedule anchor).
|
||||
final DateTime scheduledTime;
|
||||
|
||||
/// When the dose was actually administered; `null` while still pending.
|
||||
final DateTime? administeredTime;
|
||||
|
||||
/// Rotation quadrant identifier (e.g. `abdomenLeftLower`).
|
||||
final String? injectionSite;
|
||||
final String? notes;
|
||||
const MedicationDoseRecord(
|
||||
{required this.id,
|
||||
required this.compoundName,
|
||||
required this.doseMg,
|
||||
required this.scheduledTime,
|
||||
this.administeredTime,
|
||||
this.injectionSite,
|
||||
this.notes});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
map['compound_name'] = Variable<String>(compoundName);
|
||||
map['dose_mg'] = Variable<double>(doseMg);
|
||||
map['scheduled_time'] = Variable<DateTime>(scheduledTime);
|
||||
if (!nullToAbsent || administeredTime != null) {
|
||||
map['administered_time'] = Variable<DateTime>(administeredTime);
|
||||
}
|
||||
if (!nullToAbsent || injectionSite != null) {
|
||||
map['injection_site'] = Variable<String>(injectionSite);
|
||||
}
|
||||
if (!nullToAbsent || notes != null) {
|
||||
map['notes'] = Variable<String>(notes);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
MedicationDosesTableCompanion toCompanion(bool nullToAbsent) {
|
||||
return MedicationDosesTableCompanion(
|
||||
id: Value(id),
|
||||
compoundName: Value(compoundName),
|
||||
doseMg: Value(doseMg),
|
||||
scheduledTime: Value(scheduledTime),
|
||||
administeredTime: administeredTime == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(administeredTime),
|
||||
injectionSite: injectionSite == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(injectionSite),
|
||||
notes:
|
||||
notes == null && nullToAbsent ? const Value.absent() : Value(notes),
|
||||
);
|
||||
}
|
||||
|
||||
factory MedicationDoseRecord.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return MedicationDoseRecord(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
compoundName: serializer.fromJson<String>(json['compoundName']),
|
||||
doseMg: serializer.fromJson<double>(json['doseMg']),
|
||||
scheduledTime: serializer.fromJson<DateTime>(json['scheduledTime']),
|
||||
administeredTime:
|
||||
serializer.fromJson<DateTime?>(json['administeredTime']),
|
||||
injectionSite: serializer.fromJson<String?>(json['injectionSite']),
|
||||
notes: serializer.fromJson<String?>(json['notes']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'compoundName': serializer.toJson<String>(compoundName),
|
||||
'doseMg': serializer.toJson<double>(doseMg),
|
||||
'scheduledTime': serializer.toJson<DateTime>(scheduledTime),
|
||||
'administeredTime': serializer.toJson<DateTime?>(administeredTime),
|
||||
'injectionSite': serializer.toJson<String?>(injectionSite),
|
||||
'notes': serializer.toJson<String?>(notes),
|
||||
};
|
||||
}
|
||||
|
||||
MedicationDoseRecord copyWith(
|
||||
{int? id,
|
||||
String? compoundName,
|
||||
double? doseMg,
|
||||
DateTime? scheduledTime,
|
||||
Value<DateTime?> administeredTime = const Value.absent(),
|
||||
Value<String?> injectionSite = const Value.absent(),
|
||||
Value<String?> notes = const Value.absent()}) =>
|
||||
MedicationDoseRecord(
|
||||
id: id ?? this.id,
|
||||
compoundName: compoundName ?? this.compoundName,
|
||||
doseMg: doseMg ?? this.doseMg,
|
||||
scheduledTime: scheduledTime ?? this.scheduledTime,
|
||||
administeredTime: administeredTime.present
|
||||
? administeredTime.value
|
||||
: this.administeredTime,
|
||||
injectionSite:
|
||||
injectionSite.present ? injectionSite.value : this.injectionSite,
|
||||
notes: notes.present ? notes.value : this.notes,
|
||||
);
|
||||
MedicationDoseRecord copyWithCompanion(MedicationDosesTableCompanion data) {
|
||||
return MedicationDoseRecord(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
compoundName: data.compoundName.present
|
||||
? data.compoundName.value
|
||||
: this.compoundName,
|
||||
doseMg: data.doseMg.present ? data.doseMg.value : this.doseMg,
|
||||
scheduledTime: data.scheduledTime.present
|
||||
? data.scheduledTime.value
|
||||
: this.scheduledTime,
|
||||
administeredTime: data.administeredTime.present
|
||||
? data.administeredTime.value
|
||||
: this.administeredTime,
|
||||
injectionSite: data.injectionSite.present
|
||||
? data.injectionSite.value
|
||||
: this.injectionSite,
|
||||
notes: data.notes.present ? data.notes.value : this.notes,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('MedicationDoseRecord(')
|
||||
..write('id: $id, ')
|
||||
..write('compoundName: $compoundName, ')
|
||||
..write('doseMg: $doseMg, ')
|
||||
..write('scheduledTime: $scheduledTime, ')
|
||||
..write('administeredTime: $administeredTime, ')
|
||||
..write('injectionSite: $injectionSite, ')
|
||||
..write('notes: $notes')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, compoundName, doseMg, scheduledTime,
|
||||
administeredTime, injectionSite, notes);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is MedicationDoseRecord &&
|
||||
other.id == this.id &&
|
||||
other.compoundName == this.compoundName &&
|
||||
other.doseMg == this.doseMg &&
|
||||
other.scheduledTime == this.scheduledTime &&
|
||||
other.administeredTime == this.administeredTime &&
|
||||
other.injectionSite == this.injectionSite &&
|
||||
other.notes == this.notes);
|
||||
}
|
||||
|
||||
class MedicationDosesTableCompanion
|
||||
extends UpdateCompanion<MedicationDoseRecord> {
|
||||
final Value<int> id;
|
||||
final Value<String> compoundName;
|
||||
final Value<double> doseMg;
|
||||
final Value<DateTime> scheduledTime;
|
||||
final Value<DateTime?> administeredTime;
|
||||
final Value<String?> injectionSite;
|
||||
final Value<String?> notes;
|
||||
const MedicationDosesTableCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.compoundName = const Value.absent(),
|
||||
this.doseMg = const Value.absent(),
|
||||
this.scheduledTime = const Value.absent(),
|
||||
this.administeredTime = const Value.absent(),
|
||||
this.injectionSite = const Value.absent(),
|
||||
this.notes = const Value.absent(),
|
||||
});
|
||||
MedicationDosesTableCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required String compoundName,
|
||||
required double doseMg,
|
||||
required DateTime scheduledTime,
|
||||
this.administeredTime = const Value.absent(),
|
||||
this.injectionSite = const Value.absent(),
|
||||
this.notes = const Value.absent(),
|
||||
}) : compoundName = Value(compoundName),
|
||||
doseMg = Value(doseMg),
|
||||
scheduledTime = Value(scheduledTime);
|
||||
static Insertable<MedicationDoseRecord> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? compoundName,
|
||||
Expression<double>? doseMg,
|
||||
Expression<DateTime>? scheduledTime,
|
||||
Expression<DateTime>? administeredTime,
|
||||
Expression<String>? injectionSite,
|
||||
Expression<String>? notes,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (compoundName != null) 'compound_name': compoundName,
|
||||
if (doseMg != null) 'dose_mg': doseMg,
|
||||
if (scheduledTime != null) 'scheduled_time': scheduledTime,
|
||||
if (administeredTime != null) 'administered_time': administeredTime,
|
||||
if (injectionSite != null) 'injection_site': injectionSite,
|
||||
if (notes != null) 'notes': notes,
|
||||
});
|
||||
}
|
||||
|
||||
MedicationDosesTableCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
Value<String>? compoundName,
|
||||
Value<double>? doseMg,
|
||||
Value<DateTime>? scheduledTime,
|
||||
Value<DateTime?>? administeredTime,
|
||||
Value<String?>? injectionSite,
|
||||
Value<String?>? notes}) {
|
||||
return MedicationDosesTableCompanion(
|
||||
id: id ?? this.id,
|
||||
compoundName: compoundName ?? this.compoundName,
|
||||
doseMg: doseMg ?? this.doseMg,
|
||||
scheduledTime: scheduledTime ?? this.scheduledTime,
|
||||
administeredTime: administeredTime ?? this.administeredTime,
|
||||
injectionSite: injectionSite ?? this.injectionSite,
|
||||
notes: notes ?? this.notes,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
}
|
||||
if (compoundName.present) {
|
||||
map['compound_name'] = Variable<String>(compoundName.value);
|
||||
}
|
||||
if (doseMg.present) {
|
||||
map['dose_mg'] = Variable<double>(doseMg.value);
|
||||
}
|
||||
if (scheduledTime.present) {
|
||||
map['scheduled_time'] = Variable<DateTime>(scheduledTime.value);
|
||||
}
|
||||
if (administeredTime.present) {
|
||||
map['administered_time'] = Variable<DateTime>(administeredTime.value);
|
||||
}
|
||||
if (injectionSite.present) {
|
||||
map['injection_site'] = Variable<String>(injectionSite.value);
|
||||
}
|
||||
if (notes.present) {
|
||||
map['notes'] = Variable<String>(notes.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('MedicationDosesTableCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('compoundName: $compoundName, ')
|
||||
..write('doseMg: $doseMg, ')
|
||||
..write('scheduledTime: $scheduledTime, ')
|
||||
..write('administeredTime: $administeredTime, ')
|
||||
..write('injectionSite: $injectionSite, ')
|
||||
..write('notes: $notes')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
_$AppDatabase(QueryExecutor e) : super(e);
|
||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||
late final $MedicationDosesTableTable medicationDosesTable =
|
||||
$MedicationDosesTableTable(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [medicationDosesTable];
|
||||
}
|
||||
|
||||
typedef $$MedicationDosesTableTableCreateCompanionBuilder
|
||||
= MedicationDosesTableCompanion Function({
|
||||
Value<int> id,
|
||||
required String compoundName,
|
||||
required double doseMg,
|
||||
required DateTime scheduledTime,
|
||||
Value<DateTime?> administeredTime,
|
||||
Value<String?> injectionSite,
|
||||
Value<String?> notes,
|
||||
});
|
||||
typedef $$MedicationDosesTableTableUpdateCompanionBuilder
|
||||
= MedicationDosesTableCompanion Function({
|
||||
Value<int> id,
|
||||
Value<String> compoundName,
|
||||
Value<double> doseMg,
|
||||
Value<DateTime> scheduledTime,
|
||||
Value<DateTime?> administeredTime,
|
||||
Value<String?> injectionSite,
|
||||
Value<String?> notes,
|
||||
});
|
||||
|
||||
class $$MedicationDosesTableTableFilterComposer
|
||||
extends Composer<_$AppDatabase, $MedicationDosesTableTable> {
|
||||
$$MedicationDosesTableTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<String> get compoundName => $composableBuilder(
|
||||
column: $table.compoundName, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<double> get doseMg => $composableBuilder(
|
||||
column: $table.doseMg, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<DateTime> get scheduledTime => $composableBuilder(
|
||||
column: $table.scheduledTime, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<DateTime> get administeredTime => $composableBuilder(
|
||||
column: $table.administeredTime,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<String> get injectionSite => $composableBuilder(
|
||||
column: $table.injectionSite, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<String> get notes => $composableBuilder(
|
||||
column: $table.notes, builder: (column) => ColumnFilters(column));
|
||||
}
|
||||
|
||||
class $$MedicationDosesTableTableOrderingComposer
|
||||
extends Composer<_$AppDatabase, $MedicationDosesTableTable> {
|
||||
$$MedicationDosesTableTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get compoundName => $composableBuilder(
|
||||
column: $table.compoundName,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<double> get doseMg => $composableBuilder(
|
||||
column: $table.doseMg, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<DateTime> get scheduledTime => $composableBuilder(
|
||||
column: $table.scheduledTime,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<DateTime> get administeredTime => $composableBuilder(
|
||||
column: $table.administeredTime,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get injectionSite => $composableBuilder(
|
||||
column: $table.injectionSite,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get notes => $composableBuilder(
|
||||
column: $table.notes, builder: (column) => ColumnOrderings(column));
|
||||
}
|
||||
|
||||
class $$MedicationDosesTableTableAnnotationComposer
|
||||
extends Composer<_$AppDatabase, $MedicationDosesTableTable> {
|
||||
$$MedicationDosesTableTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<int> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get compoundName => $composableBuilder(
|
||||
column: $table.compoundName, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<double> get doseMg =>
|
||||
$composableBuilder(column: $table.doseMg, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get scheduledTime => $composableBuilder(
|
||||
column: $table.scheduledTime, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get administeredTime => $composableBuilder(
|
||||
column: $table.administeredTime, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get injectionSite => $composableBuilder(
|
||||
column: $table.injectionSite, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get notes =>
|
||||
$composableBuilder(column: $table.notes, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$MedicationDosesTableTableTableManager extends RootTableManager<
|
||||
_$AppDatabase,
|
||||
$MedicationDosesTableTable,
|
||||
MedicationDoseRecord,
|
||||
$$MedicationDosesTableTableFilterComposer,
|
||||
$$MedicationDosesTableTableOrderingComposer,
|
||||
$$MedicationDosesTableTableAnnotationComposer,
|
||||
$$MedicationDosesTableTableCreateCompanionBuilder,
|
||||
$$MedicationDosesTableTableUpdateCompanionBuilder,
|
||||
(
|
||||
MedicationDoseRecord,
|
||||
BaseReferences<_$AppDatabase, $MedicationDosesTableTable,
|
||||
MedicationDoseRecord>
|
||||
),
|
||||
MedicationDoseRecord,
|
||||
PrefetchHooks Function()> {
|
||||
$$MedicationDosesTableTableTableManager(
|
||||
_$AppDatabase db, $MedicationDosesTableTable table)
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
$$MedicationDosesTableTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
$$MedicationDosesTableTableOrderingComposer(
|
||||
$db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
$$MedicationDosesTableTableAnnotationComposer(
|
||||
$db: db, $table: table),
|
||||
updateCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<String> compoundName = const Value.absent(),
|
||||
Value<double> doseMg = const Value.absent(),
|
||||
Value<DateTime> scheduledTime = const Value.absent(),
|
||||
Value<DateTime?> administeredTime = const Value.absent(),
|
||||
Value<String?> injectionSite = const Value.absent(),
|
||||
Value<String?> notes = const Value.absent(),
|
||||
}) =>
|
||||
MedicationDosesTableCompanion(
|
||||
id: id,
|
||||
compoundName: compoundName,
|
||||
doseMg: doseMg,
|
||||
scheduledTime: scheduledTime,
|
||||
administeredTime: administeredTime,
|
||||
injectionSite: injectionSite,
|
||||
notes: notes,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
required String compoundName,
|
||||
required double doseMg,
|
||||
required DateTime scheduledTime,
|
||||
Value<DateTime?> administeredTime = const Value.absent(),
|
||||
Value<String?> injectionSite = const Value.absent(),
|
||||
Value<String?> notes = const Value.absent(),
|
||||
}) =>
|
||||
MedicationDosesTableCompanion.insert(
|
||||
id: id,
|
||||
compoundName: compoundName,
|
||||
doseMg: doseMg,
|
||||
scheduledTime: scheduledTime,
|
||||
administeredTime: administeredTime,
|
||||
injectionSite: injectionSite,
|
||||
notes: notes,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (
|
||||
e.readTable<$MedicationDosesTableTable,
|
||||
MedicationDoseRecord>(table),
|
||||
BaseReferences<_$AppDatabase, $MedicationDosesTableTable,
|
||||
MedicationDoseRecord>(db, table, e)
|
||||
))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
));
|
||||
}
|
||||
|
||||
typedef $$MedicationDosesTableTableProcessedTableManager
|
||||
= ProcessedTableManager<
|
||||
_$AppDatabase,
|
||||
$MedicationDosesTableTable,
|
||||
MedicationDoseRecord,
|
||||
$$MedicationDosesTableTableFilterComposer,
|
||||
$$MedicationDosesTableTableOrderingComposer,
|
||||
$$MedicationDosesTableTableAnnotationComposer,
|
||||
$$MedicationDosesTableTableCreateCompanionBuilder,
|
||||
$$MedicationDosesTableTableUpdateCompanionBuilder,
|
||||
(
|
||||
MedicationDoseRecord,
|
||||
BaseReferences<_$AppDatabase, $MedicationDosesTableTable,
|
||||
MedicationDoseRecord>
|
||||
),
|
||||
MedicationDoseRecord,
|
||||
PrefetchHooks Function()>;
|
||||
|
||||
class $AppDatabaseManager {
|
||||
final _$AppDatabase _db;
|
||||
$AppDatabaseManager(this._db);
|
||||
$$MedicationDosesTableTableTableManager get medicationDosesTable =>
|
||||
$$MedicationDosesTableTableTableManager(_db, _db.medicationDosesTable);
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../security/crypto_manager.dart';
|
||||
|
||||
/// Opens the local SQLCipher-encrypted database file.
|
||||
///
|
||||
/// The hardware-backed 256-bit AES master key is generated on first launch and
|
||||
/// stored in the OS secure vault (Keychain / Keystore / DPAPI) by
|
||||
/// [CryptoManager]; it is handed to SQLCipher through `PRAGMA key` before any
|
||||
/// statement executes.
|
||||
///
|
||||
/// The native SQLite engine is compiled as SQLCipher through the `sqlite3`
|
||||
/// build-hook define in `pubspec.yaml` (`hooks.user_defines.sqlite3.source`),
|
||||
/// which ships the encrypted engine on every native platform (Android, iOS,
|
||||
/// macOS, Windows, Linux) without dynamic-library override code.
|
||||
QueryExecutor openDatabaseConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final supportDirectory = await getApplicationSupportDirectory();
|
||||
final databaseFile = File(
|
||||
p.join(supportDirectory.path, 'glp1_companion.db'),
|
||||
);
|
||||
|
||||
final masterKey = await CryptoManager().getOrCreateDatabaseEncryptionKey();
|
||||
|
||||
return NativeDatabase(
|
||||
databaseFile,
|
||||
setup: (db) {
|
||||
// SQLCipher derives the AES-256 page key from the master-key
|
||||
// passphrase before drift issues its first statement.
|
||||
db.execute("PRAGMA key = '$masterKey';");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
/// Fallback for platforms without a drift backend.
|
||||
///
|
||||
/// This branch is never selected at runtime on the supported targets: native
|
||||
/// platforms compile to `native.dart` (SQLCipher) and the web compiler picks
|
||||
/// `web.dart` (WASM) through the conditional import in `app_database.dart`.
|
||||
QueryExecutor openDatabaseConnection() {
|
||||
throw UnimplementedError(
|
||||
'Local persistence is only implemented for native (SQLCipher) '
|
||||
'and web (WASM SQLite) platforms.',
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/wasm.dart';
|
||||
|
||||
/// Opens the browser database via SQLite compiled to WebAssembly.
|
||||
///
|
||||
/// SQLCipher cannot run in the browser, so web builds persist an unencrypted
|
||||
/// database in OPFS/IndexedDB. [WasmDatabase.open] expects `sqlite3.wasm`
|
||||
/// (from the `sqlite3.dart` releases) and `drift_worker.js` (from the drift
|
||||
/// releases) to be copied into the `web/` directory of the Flutter host
|
||||
/// project.
|
||||
QueryExecutor openDatabaseConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final result = await WasmDatabase.open(
|
||||
databaseName: 'glp1_companion',
|
||||
sqlite3Uri: Uri.parse('sqlite3.wasm'),
|
||||
driftWorkerUri: Uri.parse('drift_worker.js'),
|
||||
);
|
||||
return result.resolvedExecutor;
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
/// Whether the global [Supabase] SDK has been initialized.
|
||||
///
|
||||
/// [Supabase.instance] throws an [AssertionError] in debug builds when
|
||||
/// [Supabase.initialize] has not run (for example when the `.env` file is
|
||||
/// missing or incomplete during local development). Routing guards and the
|
||||
/// provider graph must not depend on the SDK being configured, so this helper
|
||||
/// probes the singleton defensively and reports `false` instead of throwing.
|
||||
bool get isSupabaseAvailable {
|
||||
try {
|
||||
return Supabase.instance.isInitialized;
|
||||
} on AssertionError {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../domain/auth_service.dart';
|
||||
|
||||
/// Custom URL scheme used to bring the user back into the app after an OAuth
|
||||
/// or magic-link round trip on native platforms.
|
||||
///
|
||||
/// The scheme mirrors the platform bundle identifiers
|
||||
/// (`com.glpcompanion.glpcompanion`) and must be registered in the iOS/macOS
|
||||
/// `Info.plist`, the Android manifest intent filters, and the Windows runner
|
||||
/// so the operating system can route the redirect callback back to the app.
|
||||
/// [SupabaseAuth] (bundled with `supabase_flutter`) observes the deep link and
|
||||
/// exchanges the returned auth code for a session automatically.
|
||||
const kAuthCallbackRedirectUrl =
|
||||
'com.glpcompanion.glpcompanion://login-callback/';
|
||||
|
||||
/// Resolves the redirect URL embedded in OAuth and magic-link flows.
|
||||
///
|
||||
/// On web the browser returns to the current origin; native platforms use the
|
||||
/// registered deep-link scheme above.
|
||||
String resolveAuthRedirectUrl() {
|
||||
return kIsWeb ? Uri.base.origin : kAuthCallbackRedirectUrl;
|
||||
}
|
||||
|
||||
/// [IAuthService] implementation backed by Supabase GoTrue.
|
||||
///
|
||||
/// Only passwordless sign-in modes are modeled: federated OAuth (Google,
|
||||
/// Apple, Microsoft Identity via Supabase's `azure` provider) and email magic
|
||||
/// links. Direct username/password credentials are intentionally absent in
|
||||
/// line with the platform's security posture.
|
||||
class SupabaseAuthService implements IAuthService {
|
||||
/// Creates the service. Pass [client] in tests to inject a mock; production
|
||||
/// code resolves the global Supabase client lazily on first use.
|
||||
SupabaseAuthService({SupabaseClient? client})
|
||||
: _client = client ?? Supabase.instance.client;
|
||||
|
||||
final SupabaseClient _client;
|
||||
|
||||
@override
|
||||
Future<void> signInWithGoogle() => _signInWithOAuth(OAuthProvider.google);
|
||||
|
||||
@override
|
||||
Future<void> signInWithApple() => _signInWithOAuth(OAuthProvider.apple);
|
||||
|
||||
/// Microsoft Identity (Entra ID) is exposed by Supabase Auth as the
|
||||
/// `azure` OAuth provider.
|
||||
@override
|
||||
Future<void> signInWithMicrosoft() => _signInWithOAuth(OAuthProvider.azure);
|
||||
|
||||
@override
|
||||
Future<void> signInWithMagicLink(String email) async {
|
||||
await _client.auth.signInWithOtp(
|
||||
email: email,
|
||||
emailRedirectTo: resolveAuthRedirectUrl(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signOut() => _client.auth.signOut();
|
||||
|
||||
/// Emits the authenticated user's id on every auth transition and `null`
|
||||
/// once the user is signed out.
|
||||
@override
|
||||
Stream<String?> get authStateChanges =>
|
||||
_client.auth.onAuthStateChange.map((state) => state.session?.user.id);
|
||||
|
||||
Future<void> _signInWithOAuth(OAuthProvider provider) async {
|
||||
final launched = await _client.auth.signInWithOAuth(
|
||||
provider,
|
||||
redirectTo: resolveAuthRedirectUrl(),
|
||||
);
|
||||
if (!launched) {
|
||||
throw AuthException('Could not launch the ${provider.name} sign-in page.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../../../../core/network/supabase_status.dart';
|
||||
import '../../data/supabase_auth_service.dart';
|
||||
import '../../domain/auth_service.dart';
|
||||
|
||||
/// Provides the application-wide [IAuthService].
|
||||
///
|
||||
/// The concrete [SupabaseAuthService] is resolved behind the domain contract
|
||||
/// so widget/unit tests can override this provider with a fake without touching
|
||||
/// the presentation layer.
|
||||
final authServiceProvider = Provider<IAuthService>(
|
||||
(ref) => SupabaseAuthService(),
|
||||
);
|
||||
|
||||
/// Raw GoTrue auth transitions (sign-in, sign-out, token refresh, ...).
|
||||
///
|
||||
/// The router subscribes to this stream through `refreshListenable` so route
|
||||
/// guards react immediately after an OAuth or magic-link round trip completes.
|
||||
/// Falls back to an empty stream when the Supabase SDK has not been
|
||||
/// initialized (e.g. missing `.env` during local development).
|
||||
final authStateProvider = StreamProvider<AuthState>((ref) {
|
||||
if (!isSupabaseAvailable) return const Stream<AuthState>.empty();
|
||||
return Supabase.instance.client.auth.onAuthStateChange;
|
||||
});
|
||||
|
||||
/// Orchestrates asynchronous passwordless authentication flows.
|
||||
///
|
||||
/// Every method transitions through [AsyncValue.loading] and records failures
|
||||
/// in [state] as [AsyncValue.error], so the presentation layer only needs to
|
||||
/// watch/listen to the provider to render progress and error feedback.
|
||||
class AuthController extends StateNotifier<AsyncValue<void>> {
|
||||
AuthController(this._authService) : super(const AsyncValue.data(null));
|
||||
|
||||
final IAuthService _authService;
|
||||
|
||||
/// Dispatches a passwordless OTP/magic link to [email].
|
||||
Future<void> signInWithMagicLink(String email) =>
|
||||
_run(() => _authService.signInWithMagicLink(email));
|
||||
|
||||
Future<void> signInWithGoogle() => _run(_authService.signInWithGoogle);
|
||||
|
||||
Future<void> signInWithApple() => _run(_authService.signInWithApple);
|
||||
|
||||
Future<void> signInWithMicrosoft() => _run(_authService.signInWithMicrosoft);
|
||||
|
||||
Future<void> signOut() => _run(_authService.signOut);
|
||||
|
||||
Future<void> _run(Future<void> Function() action) async {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
await action();
|
||||
if (mounted) state = const AsyncValue.data(null);
|
||||
} catch (error, stackTrace) {
|
||||
if (mounted) state = AsyncValue.error(error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exposes the [AuthController] to the widget tree.
|
||||
final authControllerProvider =
|
||||
StateNotifierProvider<AuthController, AsyncValue<void>>(
|
||||
(ref) => AuthController(ref.watch(authServiceProvider)),
|
||||
);
|
||||
|
|
@ -1,19 +1,60 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart' show AuthException;
|
||||
|
||||
class LoginScreen extends StatelessWidget {
|
||||
import '../controllers/auth_controller.dart';
|
||||
|
||||
/// Which authentication flow is currently executing, so the relevant button
|
||||
/// can render a spinner while the request is in flight.
|
||||
enum _AuthAction { google, apple, microsoft, magicLink }
|
||||
|
||||
/// Passwordless sign-in surface: federated OAuth (Google, Apple, Microsoft)
|
||||
/// and email magic links. Wired to [authControllerProvider] for execution and
|
||||
/// progress/error feedback.
|
||||
class LoginScreen extends ConsumerStatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
|
||||
/// The action currently awaiting completion, or `null` when idle.
|
||||
_AuthAction? _pendingAction;
|
||||
|
||||
static final RegExp _emailPattern = RegExp(
|
||||
r"^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$",
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final emailController = TextEditingController();
|
||||
// The controller records failures in its AsyncValue state instead of
|
||||
// throwing, so a single listener surfaces every flow's error feedback.
|
||||
ref.listen<AsyncValue<void>>(authControllerProvider, (previous, next) {
|
||||
final error = next.error;
|
||||
if (error == null) return;
|
||||
_showSnack(_messageFor(error), isError: true);
|
||||
});
|
||||
|
||||
final isBusy = ref.watch(authControllerProvider).isLoading;
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
|
@ -32,23 +73,14 @@ class LoginScreen extends StatelessWidget {
|
|||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.g_mobiledata, size: 28),
|
||||
label: const Text('Continue with Google'),
|
||||
),
|
||||
_oauthButton(_AuthAction.google, Icons.g_mobiledata,
|
||||
'Continue with Google'),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.apple),
|
||||
label: const Text('Continue with Apple'),
|
||||
),
|
||||
_oauthButton(
|
||||
_AuthAction.apple, Icons.apple, 'Continue with Apple'),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.window),
|
||||
label: const Text('Continue with Microsoft'),
|
||||
),
|
||||
_oauthButton(_AuthAction.microsoft, Icons.window,
|
||||
'Continue with Microsoft'),
|
||||
const SizedBox(height: 24),
|
||||
const Row(
|
||||
children: [
|
||||
|
|
@ -62,7 +94,10 @@ class LoginScreen extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
TextField(
|
||||
controller: emailController,
|
||||
controller: _emailController,
|
||||
enabled: !isBusy,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email Address',
|
||||
border: OutlineInputBorder(),
|
||||
|
|
@ -71,14 +106,110 @@ class LoginScreen extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Send Magic Link'),
|
||||
onPressed: isBusy ? null : _sendMagicLink,
|
||||
child: _pendingAction == _AuthAction.magicLink
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Send Magic Link'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds an OAuth button bound to [action]; disabled while any flow runs
|
||||
/// and swapped for a spinner while [action] itself is executing.
|
||||
Widget _oauthButton(_AuthAction action, IconData icon, String label) {
|
||||
final isBusy = ref.watch(authControllerProvider).isLoading;
|
||||
return ElevatedButton.icon(
|
||||
onPressed: isBusy ? null : () => _start(action, _runnerFor(action)),
|
||||
icon: _pendingAction == action
|
||||
? const SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Icon(icon),
|
||||
label: Text(label),
|
||||
);
|
||||
}
|
||||
|
||||
/// Resolves the controller method backing [action].
|
||||
Future<void> Function() _runnerFor(_AuthAction action) {
|
||||
final notifier = ref.read(authControllerProvider.notifier);
|
||||
return switch (action) {
|
||||
_AuthAction.google => notifier.signInWithGoogle,
|
||||
_AuthAction.apple => notifier.signInWithApple,
|
||||
_AuthAction.microsoft => notifier.signInWithMicrosoft,
|
||||
_AuthAction.magicLink =>
|
||||
() => notifier.signInWithMagicLink(_emailController.text.trim()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Runs [execute] while marking [action] as pending.
|
||||
///
|
||||
/// A completed flow without a recorded error counts as success; optional
|
||||
/// [successMessage] is surfaced via SnackBar. Errors are reported by the
|
||||
/// listener in [build], so they are not repeated here.
|
||||
Future<void> _start(
|
||||
_AuthAction action,
|
||||
Future<void> Function() execute, {
|
||||
String? successMessage,
|
||||
}) async {
|
||||
if (_pendingAction != null) return;
|
||||
setState(() => _pendingAction = action);
|
||||
try {
|
||||
await execute();
|
||||
if (!mounted) return;
|
||||
final result = ref.read(authControllerProvider);
|
||||
if (!result.hasError && successMessage != null) {
|
||||
_showSnack(successMessage);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _pendingAction = null);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendMagicLink() async {
|
||||
final email = _emailController.text.trim();
|
||||
if (!_emailPattern.hasMatch(email)) {
|
||||
_showSnack(
|
||||
'Enter a valid email address to receive a magic link.',
|
||||
isError: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _start(
|
||||
_AuthAction.magicLink,
|
||||
_runnerFor(_AuthAction.magicLink),
|
||||
successMessage:
|
||||
'Magic link sent to $email. Check your inbox to finish signing in.',
|
||||
);
|
||||
}
|
||||
|
||||
void _showSnack(String message, {bool isError = false}) {
|
||||
if (!mounted) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: isError ? Theme.of(context).colorScheme.error : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _messageFor(Object error) {
|
||||
if (error is AuthException) return error.message;
|
||||
return 'Authentication failed. Please try again.';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,33 @@ import 'dart:io';
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'app/router/app_router.dart';
|
||||
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Load environment variables from .env
|
||||
await dotenv.load(fileName: '.env');
|
||||
|
||||
final supabaseUrl = dotenv.env['SUPABASE_URL'] ?? '';
|
||||
final supabaseKey = dotenv.env['SUPABASE_PUBLISHABLE_KEY'] ??
|
||||
dotenv.env['SUPABASE_ANON_KEY'] ??
|
||||
'';
|
||||
|
||||
if (supabaseUrl.isNotEmpty && supabaseKey.isNotEmpty) {
|
||||
await Supabase.initialize(
|
||||
url: supabaseUrl,
|
||||
publishableKey: supabaseKey,
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
'WARNING: SUPABASE_URL or SUPABASE_PUBLISHABLE_KEY missing in .env. Supabase client uninitialized.',
|
||||
);
|
||||
}
|
||||
|
||||
runApp(const ProviderScope(child: GLP1CompanionApp()));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,18 @@
|
|||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import app_links
|
||||
import device_info_plus
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import sqlite3_flutter_libs
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
|
|
|||
408
pubspec.lock
408
pubspec.lock
|
|
@ -5,26 +5,50 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
||||
sha256: "48e48f73a431eb269eec66d243a63a07ed90ef9417a74047a3b98d52338626ad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "85.0.0"
|
||||
version: "109.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
|
||||
sha256: "6d377901d198536b648172974d9d4f277910502cad92193466869cac0ff34990"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.6.0"
|
||||
analyzer_plugin:
|
||||
version: "14.5.0"
|
||||
app_links:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer_plugin
|
||||
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
|
||||
name: app_links
|
||||
sha256: "08ba88bf19d9edea33130cb71c8ced2cb982b4e00b82efef4a434d1357bc69c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.4"
|
||||
version: "7.2.2"
|
||||
app_links_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_linux
|
||||
sha256: "9ccb5802085a1e57bb07b6256fd1d61a78c7ec104944d4965e62938b4fefd9cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
app_links_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_platform_interface
|
||||
sha256: "7546f09a6e93f4a2df2fe2bd40a5c6c64310ac461b036d82b43033be7a59f809"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
app_links_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_web
|
||||
sha256: "382d32c6f486f15b1d7f5d6a37a70a481a4fc08382eef419d373169921a3e765"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -53,18 +77,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
|
||||
sha256: "8a5c5761af8e31748bba3c82f68925ace40f3225c3eea25be9beb57eca7cd7a8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
version: "4.0.11"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
sha256: d466ed2dc9c6cd1d169948879b84ee061eb5e22c64a7c6089879c6296d272a8d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.3.3"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -73,30 +97,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.7"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
|
||||
sha256: "6feadd32d90a68253c1f7b4fcff76adb4b41ad32a53f8bad835dc65f01ac9dc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.2"
|
||||
version: "2.16.2"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -169,14 +177,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "811709c1badd02fc054039a13f66e0db1961865f9a314175f68c3eaeebec786f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.13.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -217,30 +217,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
custom_lint_core:
|
||||
dart_jsonwebtoken:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_core
|
||||
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
|
||||
name: dart_jsonwebtoken
|
||||
sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.5"
|
||||
custom_lint_visitor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_visitor
|
||||
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+7.7.0"
|
||||
version: "3.4.1"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
|
||||
sha256: a0e4f65339b31eb6a9e6a12e14b4682a48d46fbc54a82f4a702396fbdbac63a7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.14"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -269,26 +261,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1"
|
||||
sha256: ef9598e1785c18db37209e8554fc79aff94632209d384ec98984b9e3184af0f7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.28.2"
|
||||
version: "2.35.2"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "68c138e884527d2bd61df2ade276c3a144df84d1adeb0ab8f3196b5afe021bd4"
|
||||
sha256: e0d16aaa21f3408cbe2dd4bfc56c36cea43e53a537e6613b91380c0924b3bd30
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.28.0"
|
||||
version: "2.35.1"
|
||||
drift_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift_flutter
|
||||
sha256: "0bc2f1dde59e59cedde0df67a4ff7bd8f0d42274f18b50bf7e7dae7ca3d77801"
|
||||
sha256: "91acf4bee7c3c84467cba46455aa70e5292a3b889f4582645d74f2e5a8c106f2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
version: "0.3.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -326,6 +318,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_dotenv:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_dotenv
|
||||
sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -429,22 +429,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
freezed_annotation:
|
||||
functions_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
||||
name: functions_client
|
||||
sha256: "931e822688d2e97867b2300e2fdbb08c9db73f8653fc83efc7a259d2e0cca769"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: "404dbf9bce29beafbe9dcb19219447b6f7d49900cd42b8d3b7af102b24508e25"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "2.7.1"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -461,6 +453,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.8.1"
|
||||
gotrue:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gotrue
|
||||
sha256: cca816226824160e40d149816fd89740cc64dda1eaa3d9161e334330b18bba89
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.27.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -645,6 +645,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "90eaad8059ebc2004a63810a4696ea9acdb02473e47ed3b6b3bafb100ac77e31"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.5"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -661,8 +669,16 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
passkeys_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: passkeys_platform_interface
|
||||
sha256: "5fca044a6f1a3c5abff645ca5a5417b613c130d1c4e08513ba9182c7f4834619"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
|
|
@ -670,7 +686,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||
|
|
@ -741,6 +757,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -749,6 +773,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.3"
|
||||
postgrest:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: postgrest
|
||||
sha256: b7f957273ba6a1c26cefa987ac524f4138e9ab7542fbb9e6abdfddacb9895a21
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.1"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "4242ba3508d37e01808bdf71ad1d5bb93a8d671bf2e7450e6b1b353fb0808891"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.6"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -765,6 +805,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
realtime_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: realtime_client
|
||||
sha256: "2ef680d83ba2c0f76019d16194ca7067fa2458b2d4091b52323a9ca4a964fd40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.2"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -789,14 +837,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
riverpod_analyzer_utils:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod_analyzer_utils
|
||||
sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.10"
|
||||
riverpod_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -805,14 +845,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
riverpod_generator:
|
||||
dependency: "direct dev"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod_generator
|
||||
sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36"
|
||||
name: shared_preferences
|
||||
sha256: "1e85e0dccf5b4a016077fea7dfbb4a7cb39c759ac1596fcf60c06deadb7da91f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.5"
|
||||
version: "2.5.6"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.28"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.7"
|
||||
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: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
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"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -838,10 +926,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||
sha256: "833ea7d15b5be79949d3d6c077da0267a0bd93c16897b36a771395a91e63086e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "4.3.1"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -850,30 +938,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
sqlite3:
|
||||
sqlcipher_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
|
||||
name: sqlcipher_flutter_libs
|
||||
sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.4"
|
||||
version: "0.7.0+eol"
|
||||
sqlite3:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "144132790ec1947fad9f3ddceffd0af2ab80a1f0d1e81a4d094ee6707ec3df0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.1"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
|
||||
sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.42"
|
||||
version: "0.6.0+eol"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67"
|
||||
sha256: "772bb2f6f5bce0631a60f26b57d6e8882e1105c22345b05c163ee6ee5d7ba32d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.41.2"
|
||||
version: "0.45.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -890,6 +986,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
storage_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: storage_client
|
||||
sha256: b951281b8e2b293460f0e6cc4e866f2546681390f0e37d24fbbd7b4a0deb27a3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -914,6 +1018,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
supabase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: supabase
|
||||
sha256: "123fe08792b24e5e7c4b9a01145de8e95ebaa9183cd5f3c9d48dd2ff890f28a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.16.5"
|
||||
supabase_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: supabase_common
|
||||
sha256: a7961a44953cf8d9da049381fc3377516348255042fbb12b9d4f8540816f2fe9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
supabase_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: supabase_flutter
|
||||
sha256: "48bbde226e68bdb293020859a1bfa42d0f3bd2239992fba0ec4f9194cbb842e6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.18.3"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -938,14 +1066,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -954,14 +1074,70 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
url_launcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||
name: url_launcher
|
||||
sha256: e8cc955c97e06df9879140a1fe301f3d493f01b7c62dec7f46834be31d7e9fda
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
version: "6.3.3"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.33"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.2"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.6"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1050,6 +1226,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
yaml_edit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml_edit
|
||||
sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
yet_another_json_isolate:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yet_another_json_isolate
|
||||
sha256: eaa26beb5990b25a49d942374fd5a0c5aa67a837e03b14b4c26134aaa1ed01a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
sdks:
|
||||
dart: ">=3.13.0 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
flutter: ">=3.44.0"
|
||||
|
|
|
|||
31
pubspec.yaml
31
pubspec.yaml
|
|
@ -28,21 +28,42 @@ dependencies:
|
|||
html: ^0.15.4
|
||||
|
||||
# Persistence & Secure Storage
|
||||
drift: ^2.16.0
|
||||
drift_flutter: ^0.1.0
|
||||
# drift 2.35+ pairs with package:sqlite3 3.x, which bundles SQLite through
|
||||
# build hooks (see `hooks:` below) instead of the retired platform plugins.
|
||||
drift: ^2.35.2
|
||||
drift_flutter: ^0.3.1
|
||||
flutter_secure_storage: ^9.0.0
|
||||
|
||||
# Multi-Platform Health & Background Tasks
|
||||
health: 13.3.1
|
||||
flutter_local_notifications: ^17.1.0
|
||||
|
||||
flutter_dotenv: ^5.1.0
|
||||
supabase_flutter: ^2.5.0
|
||||
path: ^1.9.1
|
||||
path_provider: ^2.1.6
|
||||
sqlite3: ^3.4.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^3.0.0
|
||||
build_runner: ^2.4.9
|
||||
riverpod_generator: ^2.4.0
|
||||
drift_dev: ^2.16.0
|
||||
# NOTE: riverpod_generator was removed because the 2.x code-generation line
|
||||
# caps `analyzer <8.0.0`, which cannot parse the Dart 3.13 SDK and crashes
|
||||
# build_runner. All providers in this codebase are declared manually with
|
||||
# `Provider`/`StateNotifierProvider`; re-adding codegen requires the
|
||||
# Riverpod 3 migration.
|
||||
build_runner: ^2.16.0
|
||||
drift_dev: ^2.35.1
|
||||
|
||||
# Compile SQLCipher into the SQLite engine bundled by package:sqlite3's build
|
||||
# hooks (the sqlcipher_flutter_libs plugin is retired on sqlite3 3.x).
|
||||
hooks:
|
||||
user_defines:
|
||||
sqlite3:
|
||||
source: sqlcipher
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- .env
|
||||
|
|
@ -6,12 +6,15 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
flutter_secure_storage_windows
|
||||
sqlite3_flutter_libs
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
|
|
|||
Loading…
Reference in New Issue