mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: oauth loopback and persistence

* scaffold scripts

* justfile

+1902 -321
+3
justfile
··· 1 + export FLUTTER_SUPPRESS_ANALYTICS := "true" 2 + export DART_SUPPRESS_ANALYTICS := "true" 3 + 1 4 # Format all Dart files 2 5 format: 3 6 dart format lib test
+54 -4
lib/core/database/app_database.dart
··· 2 2 import 'package:drift_flutter/drift_flutter.dart'; 3 3 import 'package:path_provider/path_provider.dart'; 4 4 5 - import 'tables.dart'; 5 + import 'package:lazurite/core/database/tables.dart'; 6 6 7 7 part 'app_database.g.dart'; 8 8 9 - @DriftDatabase(tables: [Accounts, Settings]) 9 + @DriftDatabase(tables: [Accounts, CachedProfiles, CachedPosts, Settings]) 10 10 class AppDatabase extends _$AppDatabase { 11 11 AppDatabase({QueryExecutor? executor}) : super(executor ?? _openConnection()); 12 12 13 13 @override 14 - int get schemaVersion => 1; 14 + int get schemaVersion => 2; 15 + 16 + @override 17 + MigrationStrategy get migration => MigrationStrategy( 18 + onCreate: (migrator) async { 19 + await migrator.createAll(); 20 + }, 21 + onUpgrade: (migrator, from, to) async { 22 + if (from < 2) { 23 + await migrator.addColumn(accounts, accounts.service); 24 + await migrator.addColumn(accounts, accounts.dpopPublicKey); 25 + await migrator.addColumn(accounts, accounts.dpopNonce); 26 + await migrator.createTable(cachedProfiles); 27 + await migrator.createTable(cachedPosts); 28 + } 29 + }, 30 + ); 15 31 16 32 static QueryExecutor _openConnection() { 17 33 return driftDatabase( ··· 23 39 Future<Account?> getAccount(String did) => (select(accounts)..where((a) => a.did.equals(did))).getSingleOrNull(); 24 40 25 41 Future<Account?> getActiveAccount() async { 26 - final all = await select(accounts).get(); 42 + final all = await (select(accounts)..orderBy([(a) => OrderingTerm.desc(a.updatedAt)])).get(); 27 43 return all.isNotEmpty ? all.first : null; 28 44 } 29 45 ··· 40 56 required String accessToken, 41 57 String? refreshToken, 42 58 DateTime? expiresAt, 59 + String? dpopNonce, 43 60 }) async { 44 61 final query = update(accounts)..where((a) => a.did.equals(did)); 45 62 final rowsAffected = await query.write( ··· 47 64 accessToken: Value(accessToken), 48 65 refreshToken: refreshToken != null ? Value(refreshToken) : const Value.absent(), 49 66 expiresAt: expiresAt != null ? Value(expiresAt) : const Value.absent(), 67 + dpopNonce: dpopNonce != null ? Value(dpopNonce) : const Value.absent(), 50 68 updatedAt: Value(DateTime.now()), 51 69 ), 52 70 ); 53 71 return rowsAffected > 0; 54 72 } 73 + 74 + Future<int> cacheProfile({ 75 + required String did, 76 + required String handle, 77 + required String payload, 78 + DateTime? fetchedAt, 79 + }) => into(cachedProfiles).insert( 80 + CachedProfilesCompanion( 81 + did: Value(did), 82 + handle: Value(handle), 83 + payload: Value(payload), 84 + fetchedAt: Value(fetchedAt ?? DateTime.now()), 85 + ), 86 + mode: InsertMode.replace, 87 + ); 88 + 89 + Future<int> cachePost({ 90 + required String uri, 91 + required String authorDid, 92 + required String payload, 93 + DateTime? createdAt, 94 + DateTime? fetchedAt, 95 + }) => into(cachedPosts).insert( 96 + CachedPostsCompanion( 97 + uri: Value(uri), 98 + authorDid: Value(authorDid), 99 + payload: Value(payload), 100 + createdAt: createdAt != null ? Value(createdAt) : const Value.absent(), 101 + fetchedAt: Value(fetchedAt ?? DateTime.now()), 102 + ), 103 + mode: InsertMode.replace, 104 + ); 55 105 56 106 Future<String?> getSetting(String key) async { 57 107 final setting = await (select(settings)..where((s) => s.key.equals(key))).getSingleOrNull();
+1060 -3
lib/core/database/app_database.g.dart
··· 35 35 type: DriftSqlType.string, 36 36 requiredDuringInsert: false, 37 37 ); 38 + static const VerificationMeta _serviceMeta = const VerificationMeta('service'); 39 + @override 40 + late final GeneratedColumn<String> service = GeneratedColumn<String>( 41 + 'service', 42 + aliasedName, 43 + true, 44 + type: DriftSqlType.string, 45 + requiredDuringInsert: false, 46 + ); 38 47 static const VerificationMeta _accessTokenMeta = const VerificationMeta('accessToken'); 39 48 @override 40 49 late final GeneratedColumn<String> accessToken = GeneratedColumn<String>( ··· 53 62 type: DriftSqlType.string, 54 63 requiredDuringInsert: false, 55 64 ); 65 + static const VerificationMeta _dpopPublicKeyMeta = const VerificationMeta('dpopPublicKey'); 66 + @override 67 + late final GeneratedColumn<String> dpopPublicKey = GeneratedColumn<String>( 68 + 'dpop_public_key', 69 + aliasedName, 70 + true, 71 + type: DriftSqlType.string, 72 + requiredDuringInsert: false, 73 + ); 56 74 static const VerificationMeta _dpopPrivateKeyMeta = const VerificationMeta('dpopPrivateKey'); 57 75 @override 58 76 late final GeneratedColumn<String> dpopPrivateKey = GeneratedColumn<String>( ··· 62 80 type: DriftSqlType.string, 63 81 requiredDuringInsert: false, 64 82 ); 83 + static const VerificationMeta _dpopNonceMeta = const VerificationMeta('dpopNonce'); 84 + @override 85 + late final GeneratedColumn<String> dpopNonce = GeneratedColumn<String>( 86 + 'dpop_nonce', 87 + aliasedName, 88 + true, 89 + type: DriftSqlType.string, 90 + requiredDuringInsert: false, 91 + ); 65 92 static const VerificationMeta _expiresAtMeta = const VerificationMeta('expiresAt'); 66 93 @override 67 94 late final GeneratedColumn<DateTime> expiresAt = GeneratedColumn<DateTime>( ··· 96 123 did, 97 124 handle, 98 125 displayName, 126 + service, 99 127 accessToken, 100 128 refreshToken, 129 + dpopPublicKey, 101 130 dpopPrivateKey, 131 + dpopNonce, 102 132 expiresAt, 103 133 createdAt, 104 134 updatedAt, ··· 125 155 if (data.containsKey('display_name')) { 126 156 context.handle(_displayNameMeta, displayName.isAcceptableOrUnknown(data['display_name']!, _displayNameMeta)); 127 157 } 158 + if (data.containsKey('service')) { 159 + context.handle(_serviceMeta, service.isAcceptableOrUnknown(data['service']!, _serviceMeta)); 160 + } 128 161 if (data.containsKey('access_token')) { 129 162 context.handle(_accessTokenMeta, accessToken.isAcceptableOrUnknown(data['access_token']!, _accessTokenMeta)); 130 163 } else if (isInserting) { ··· 133 166 if (data.containsKey('refresh_token')) { 134 167 context.handle(_refreshTokenMeta, refreshToken.isAcceptableOrUnknown(data['refresh_token']!, _refreshTokenMeta)); 135 168 } 169 + if (data.containsKey('dpop_public_key')) { 170 + context.handle( 171 + _dpopPublicKeyMeta, 172 + dpopPublicKey.isAcceptableOrUnknown(data['dpop_public_key']!, _dpopPublicKeyMeta), 173 + ); 174 + } 136 175 if (data.containsKey('dpop_private_key')) { 137 176 context.handle( 138 177 _dpopPrivateKeyMeta, 139 178 dpopPrivateKey.isAcceptableOrUnknown(data['dpop_private_key']!, _dpopPrivateKeyMeta), 140 179 ); 180 + } 181 + if (data.containsKey('dpop_nonce')) { 182 + context.handle(_dpopNonceMeta, dpopNonce.isAcceptableOrUnknown(data['dpop_nonce']!, _dpopNonceMeta)); 141 183 } 142 184 if (data.containsKey('expires_at')) { 143 185 context.handle(_expiresAtMeta, expiresAt.isAcceptableOrUnknown(data['expires_at']!, _expiresAtMeta)); ··· 160 202 did: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}did'])!, 161 203 handle: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}handle'])!, 162 204 displayName: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}display_name']), 205 + service: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}service']), 163 206 accessToken: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}access_token'])!, 164 207 refreshToken: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}refresh_token']), 208 + dpopPublicKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}dpop_public_key']), 165 209 dpopPrivateKey: attachedDatabase.typeMapping.read( 166 210 DriftSqlType.string, 167 211 data['${effectivePrefix}dpop_private_key'], 168 212 ), 213 + dpopNonce: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}dpop_nonce']), 169 214 expiresAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}expires_at']), 170 215 createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, 171 216 updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, ··· 182 227 final String did; 183 228 final String handle; 184 229 final String? displayName; 230 + final String? service; 185 231 final String accessToken; 186 232 final String? refreshToken; 233 + final String? dpopPublicKey; 187 234 final String? dpopPrivateKey; 235 + final String? dpopNonce; 188 236 final DateTime? expiresAt; 189 237 final DateTime createdAt; 190 238 final DateTime updatedAt; ··· 192 240 required this.did, 193 241 required this.handle, 194 242 this.displayName, 243 + this.service, 195 244 required this.accessToken, 196 245 this.refreshToken, 246 + this.dpopPublicKey, 197 247 this.dpopPrivateKey, 248 + this.dpopNonce, 198 249 this.expiresAt, 199 250 required this.createdAt, 200 251 required this.updatedAt, ··· 207 258 if (!nullToAbsent || displayName != null) { 208 259 map['display_name'] = Variable<String>(displayName); 209 260 } 261 + if (!nullToAbsent || service != null) { 262 + map['service'] = Variable<String>(service); 263 + } 210 264 map['access_token'] = Variable<String>(accessToken); 211 265 if (!nullToAbsent || refreshToken != null) { 212 266 map['refresh_token'] = Variable<String>(refreshToken); 213 267 } 268 + if (!nullToAbsent || dpopPublicKey != null) { 269 + map['dpop_public_key'] = Variable<String>(dpopPublicKey); 270 + } 214 271 if (!nullToAbsent || dpopPrivateKey != null) { 215 272 map['dpop_private_key'] = Variable<String>(dpopPrivateKey); 216 273 } 274 + if (!nullToAbsent || dpopNonce != null) { 275 + map['dpop_nonce'] = Variable<String>(dpopNonce); 276 + } 217 277 if (!nullToAbsent || expiresAt != null) { 218 278 map['expires_at'] = Variable<DateTime>(expiresAt); 219 279 } ··· 227 287 did: Value(did), 228 288 handle: Value(handle), 229 289 displayName: displayName == null && nullToAbsent ? const Value.absent() : Value(displayName), 290 + service: service == null && nullToAbsent ? const Value.absent() : Value(service), 230 291 accessToken: Value(accessToken), 231 292 refreshToken: refreshToken == null && nullToAbsent ? const Value.absent() : Value(refreshToken), 293 + dpopPublicKey: dpopPublicKey == null && nullToAbsent ? const Value.absent() : Value(dpopPublicKey), 232 294 dpopPrivateKey: dpopPrivateKey == null && nullToAbsent ? const Value.absent() : Value(dpopPrivateKey), 295 + dpopNonce: dpopNonce == null && nullToAbsent ? const Value.absent() : Value(dpopNonce), 233 296 expiresAt: expiresAt == null && nullToAbsent ? const Value.absent() : Value(expiresAt), 234 297 createdAt: Value(createdAt), 235 298 updatedAt: Value(updatedAt), ··· 242 305 did: serializer.fromJson<String>(json['did']), 243 306 handle: serializer.fromJson<String>(json['handle']), 244 307 displayName: serializer.fromJson<String?>(json['displayName']), 308 + service: serializer.fromJson<String?>(json['service']), 245 309 accessToken: serializer.fromJson<String>(json['accessToken']), 246 310 refreshToken: serializer.fromJson<String?>(json['refreshToken']), 311 + dpopPublicKey: serializer.fromJson<String?>(json['dpopPublicKey']), 247 312 dpopPrivateKey: serializer.fromJson<String?>(json['dpopPrivateKey']), 313 + dpopNonce: serializer.fromJson<String?>(json['dpopNonce']), 248 314 expiresAt: serializer.fromJson<DateTime?>(json['expiresAt']), 249 315 createdAt: serializer.fromJson<DateTime>(json['createdAt']), 250 316 updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), ··· 257 323 'did': serializer.toJson<String>(did), 258 324 'handle': serializer.toJson<String>(handle), 259 325 'displayName': serializer.toJson<String?>(displayName), 326 + 'service': serializer.toJson<String?>(service), 260 327 'accessToken': serializer.toJson<String>(accessToken), 261 328 'refreshToken': serializer.toJson<String?>(refreshToken), 329 + 'dpopPublicKey': serializer.toJson<String?>(dpopPublicKey), 262 330 'dpopPrivateKey': serializer.toJson<String?>(dpopPrivateKey), 331 + 'dpopNonce': serializer.toJson<String?>(dpopNonce), 263 332 'expiresAt': serializer.toJson<DateTime?>(expiresAt), 264 333 'createdAt': serializer.toJson<DateTime>(createdAt), 265 334 'updatedAt': serializer.toJson<DateTime>(updatedAt), ··· 270 339 String? did, 271 340 String? handle, 272 341 Value<String?> displayName = const Value.absent(), 342 + Value<String?> service = const Value.absent(), 273 343 String? accessToken, 274 344 Value<String?> refreshToken = const Value.absent(), 345 + Value<String?> dpopPublicKey = const Value.absent(), 275 346 Value<String?> dpopPrivateKey = const Value.absent(), 347 + Value<String?> dpopNonce = const Value.absent(), 276 348 Value<DateTime?> expiresAt = const Value.absent(), 277 349 DateTime? createdAt, 278 350 DateTime? updatedAt, ··· 280 352 did: did ?? this.did, 281 353 handle: handle ?? this.handle, 282 354 displayName: displayName.present ? displayName.value : this.displayName, 355 + service: service.present ? service.value : this.service, 283 356 accessToken: accessToken ?? this.accessToken, 284 357 refreshToken: refreshToken.present ? refreshToken.value : this.refreshToken, 358 + dpopPublicKey: dpopPublicKey.present ? dpopPublicKey.value : this.dpopPublicKey, 285 359 dpopPrivateKey: dpopPrivateKey.present ? dpopPrivateKey.value : this.dpopPrivateKey, 360 + dpopNonce: dpopNonce.present ? dpopNonce.value : this.dpopNonce, 286 361 expiresAt: expiresAt.present ? expiresAt.value : this.expiresAt, 287 362 createdAt: createdAt ?? this.createdAt, 288 363 updatedAt: updatedAt ?? this.updatedAt, ··· 292 367 did: data.did.present ? data.did.value : this.did, 293 368 handle: data.handle.present ? data.handle.value : this.handle, 294 369 displayName: data.displayName.present ? data.displayName.value : this.displayName, 370 + service: data.service.present ? data.service.value : this.service, 295 371 accessToken: data.accessToken.present ? data.accessToken.value : this.accessToken, 296 372 refreshToken: data.refreshToken.present ? data.refreshToken.value : this.refreshToken, 373 + dpopPublicKey: data.dpopPublicKey.present ? data.dpopPublicKey.value : this.dpopPublicKey, 297 374 dpopPrivateKey: data.dpopPrivateKey.present ? data.dpopPrivateKey.value : this.dpopPrivateKey, 375 + dpopNonce: data.dpopNonce.present ? data.dpopNonce.value : this.dpopNonce, 298 376 expiresAt: data.expiresAt.present ? data.expiresAt.value : this.expiresAt, 299 377 createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, 300 378 updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, ··· 307 385 ..write('did: $did, ') 308 386 ..write('handle: $handle, ') 309 387 ..write('displayName: $displayName, ') 388 + ..write('service: $service, ') 310 389 ..write('accessToken: $accessToken, ') 311 390 ..write('refreshToken: $refreshToken, ') 391 + ..write('dpopPublicKey: $dpopPublicKey, ') 312 392 ..write('dpopPrivateKey: $dpopPrivateKey, ') 393 + ..write('dpopNonce: $dpopNonce, ') 313 394 ..write('expiresAt: $expiresAt, ') 314 395 ..write('createdAt: $createdAt, ') 315 396 ..write('updatedAt: $updatedAt') ··· 318 399 } 319 400 320 401 @override 321 - int get hashCode => 322 - Object.hash(did, handle, displayName, accessToken, refreshToken, dpopPrivateKey, expiresAt, createdAt, updatedAt); 402 + int get hashCode => Object.hash( 403 + did, 404 + handle, 405 + displayName, 406 + service, 407 + accessToken, 408 + refreshToken, 409 + dpopPublicKey, 410 + dpopPrivateKey, 411 + dpopNonce, 412 + expiresAt, 413 + createdAt, 414 + updatedAt, 415 + ); 323 416 @override 324 417 bool operator ==(Object other) => 325 418 identical(this, other) || ··· 327 420 other.did == this.did && 328 421 other.handle == this.handle && 329 422 other.displayName == this.displayName && 423 + other.service == this.service && 330 424 other.accessToken == this.accessToken && 331 425 other.refreshToken == this.refreshToken && 426 + other.dpopPublicKey == this.dpopPublicKey && 332 427 other.dpopPrivateKey == this.dpopPrivateKey && 428 + other.dpopNonce == this.dpopNonce && 333 429 other.expiresAt == this.expiresAt && 334 430 other.createdAt == this.createdAt && 335 431 other.updatedAt == this.updatedAt); ··· 339 435 final Value<String> did; 340 436 final Value<String> handle; 341 437 final Value<String?> displayName; 438 + final Value<String?> service; 342 439 final Value<String> accessToken; 343 440 final Value<String?> refreshToken; 441 + final Value<String?> dpopPublicKey; 344 442 final Value<String?> dpopPrivateKey; 443 + final Value<String?> dpopNonce; 345 444 final Value<DateTime?> expiresAt; 346 445 final Value<DateTime> createdAt; 347 446 final Value<DateTime> updatedAt; ··· 350 449 this.did = const Value.absent(), 351 450 this.handle = const Value.absent(), 352 451 this.displayName = const Value.absent(), 452 + this.service = const Value.absent(), 353 453 this.accessToken = const Value.absent(), 354 454 this.refreshToken = const Value.absent(), 455 + this.dpopPublicKey = const Value.absent(), 355 456 this.dpopPrivateKey = const Value.absent(), 457 + this.dpopNonce = const Value.absent(), 356 458 this.expiresAt = const Value.absent(), 357 459 this.createdAt = const Value.absent(), 358 460 this.updatedAt = const Value.absent(), ··· 362 464 required String did, 363 465 required String handle, 364 466 this.displayName = const Value.absent(), 467 + this.service = const Value.absent(), 365 468 required String accessToken, 366 469 this.refreshToken = const Value.absent(), 470 + this.dpopPublicKey = const Value.absent(), 367 471 this.dpopPrivateKey = const Value.absent(), 472 + this.dpopNonce = const Value.absent(), 368 473 this.expiresAt = const Value.absent(), 369 474 this.createdAt = const Value.absent(), 370 475 this.updatedAt = const Value.absent(), ··· 376 481 Expression<String>? did, 377 482 Expression<String>? handle, 378 483 Expression<String>? displayName, 484 + Expression<String>? service, 379 485 Expression<String>? accessToken, 380 486 Expression<String>? refreshToken, 487 + Expression<String>? dpopPublicKey, 381 488 Expression<String>? dpopPrivateKey, 489 + Expression<String>? dpopNonce, 382 490 Expression<DateTime>? expiresAt, 383 491 Expression<DateTime>? createdAt, 384 492 Expression<DateTime>? updatedAt, ··· 388 496 if (did != null) 'did': did, 389 497 if (handle != null) 'handle': handle, 390 498 if (displayName != null) 'display_name': displayName, 499 + if (service != null) 'service': service, 391 500 if (accessToken != null) 'access_token': accessToken, 392 501 if (refreshToken != null) 'refresh_token': refreshToken, 502 + if (dpopPublicKey != null) 'dpop_public_key': dpopPublicKey, 393 503 if (dpopPrivateKey != null) 'dpop_private_key': dpopPrivateKey, 504 + if (dpopNonce != null) 'dpop_nonce': dpopNonce, 394 505 if (expiresAt != null) 'expires_at': expiresAt, 395 506 if (createdAt != null) 'created_at': createdAt, 396 507 if (updatedAt != null) 'updated_at': updatedAt, ··· 402 513 Value<String>? did, 403 514 Value<String>? handle, 404 515 Value<String?>? displayName, 516 + Value<String?>? service, 405 517 Value<String>? accessToken, 406 518 Value<String?>? refreshToken, 519 + Value<String?>? dpopPublicKey, 407 520 Value<String?>? dpopPrivateKey, 521 + Value<String?>? dpopNonce, 408 522 Value<DateTime?>? expiresAt, 409 523 Value<DateTime>? createdAt, 410 524 Value<DateTime>? updatedAt, ··· 414 528 did: did ?? this.did, 415 529 handle: handle ?? this.handle, 416 530 displayName: displayName ?? this.displayName, 531 + service: service ?? this.service, 417 532 accessToken: accessToken ?? this.accessToken, 418 533 refreshToken: refreshToken ?? this.refreshToken, 534 + dpopPublicKey: dpopPublicKey ?? this.dpopPublicKey, 419 535 dpopPrivateKey: dpopPrivateKey ?? this.dpopPrivateKey, 536 + dpopNonce: dpopNonce ?? this.dpopNonce, 420 537 expiresAt: expiresAt ?? this.expiresAt, 421 538 createdAt: createdAt ?? this.createdAt, 422 539 updatedAt: updatedAt ?? this.updatedAt, ··· 436 553 if (displayName.present) { 437 554 map['display_name'] = Variable<String>(displayName.value); 438 555 } 556 + if (service.present) { 557 + map['service'] = Variable<String>(service.value); 558 + } 439 559 if (accessToken.present) { 440 560 map['access_token'] = Variable<String>(accessToken.value); 441 561 } 442 562 if (refreshToken.present) { 443 563 map['refresh_token'] = Variable<String>(refreshToken.value); 444 564 } 565 + if (dpopPublicKey.present) { 566 + map['dpop_public_key'] = Variable<String>(dpopPublicKey.value); 567 + } 445 568 if (dpopPrivateKey.present) { 446 569 map['dpop_private_key'] = Variable<String>(dpopPrivateKey.value); 570 + } 571 + if (dpopNonce.present) { 572 + map['dpop_nonce'] = Variable<String>(dpopNonce.value); 447 573 } 448 574 if (expiresAt.present) { 449 575 map['expires_at'] = Variable<DateTime>(expiresAt.value); ··· 466 592 ..write('did: $did, ') 467 593 ..write('handle: $handle, ') 468 594 ..write('displayName: $displayName, ') 595 + ..write('service: $service, ') 469 596 ..write('accessToken: $accessToken, ') 470 597 ..write('refreshToken: $refreshToken, ') 598 + ..write('dpopPublicKey: $dpopPublicKey, ') 471 599 ..write('dpopPrivateKey: $dpopPrivateKey, ') 600 + ..write('dpopNonce: $dpopNonce, ') 472 601 ..write('expiresAt: $expiresAt, ') 473 602 ..write('createdAt: $createdAt, ') 474 603 ..write('updatedAt: $updatedAt, ') ··· 478 607 } 479 608 } 480 609 610 + class $CachedProfilesTable extends CachedProfiles with TableInfo<$CachedProfilesTable, CachedProfile> { 611 + @override 612 + final GeneratedDatabase attachedDatabase; 613 + final String? _alias; 614 + $CachedProfilesTable(this.attachedDatabase, [this._alias]); 615 + static const VerificationMeta _didMeta = const VerificationMeta('did'); 616 + @override 617 + late final GeneratedColumn<String> did = GeneratedColumn<String>( 618 + 'did', 619 + aliasedName, 620 + false, 621 + type: DriftSqlType.string, 622 + requiredDuringInsert: true, 623 + ); 624 + static const VerificationMeta _handleMeta = const VerificationMeta('handle'); 625 + @override 626 + late final GeneratedColumn<String> handle = GeneratedColumn<String>( 627 + 'handle', 628 + aliasedName, 629 + false, 630 + type: DriftSqlType.string, 631 + requiredDuringInsert: true, 632 + ); 633 + static const VerificationMeta _payloadMeta = const VerificationMeta('payload'); 634 + @override 635 + late final GeneratedColumn<String> payload = GeneratedColumn<String>( 636 + 'payload', 637 + aliasedName, 638 + false, 639 + type: DriftSqlType.string, 640 + requiredDuringInsert: true, 641 + ); 642 + static const VerificationMeta _fetchedAtMeta = const VerificationMeta('fetchedAt'); 643 + @override 644 + late final GeneratedColumn<DateTime> fetchedAt = GeneratedColumn<DateTime>( 645 + 'fetched_at', 646 + aliasedName, 647 + false, 648 + type: DriftSqlType.dateTime, 649 + requiredDuringInsert: false, 650 + defaultValue: currentDateAndTime, 651 + ); 652 + @override 653 + List<GeneratedColumn> get $columns => [did, handle, payload, fetchedAt]; 654 + @override 655 + String get aliasedName => _alias ?? actualTableName; 656 + @override 657 + String get actualTableName => $name; 658 + static const String $name = 'cached_profiles'; 659 + @override 660 + VerificationContext validateIntegrity(Insertable<CachedProfile> instance, {bool isInserting = false}) { 661 + final context = VerificationContext(); 662 + final data = instance.toColumns(true); 663 + if (data.containsKey('did')) { 664 + context.handle(_didMeta, did.isAcceptableOrUnknown(data['did']!, _didMeta)); 665 + } else if (isInserting) { 666 + context.missing(_didMeta); 667 + } 668 + if (data.containsKey('handle')) { 669 + context.handle(_handleMeta, handle.isAcceptableOrUnknown(data['handle']!, _handleMeta)); 670 + } else if (isInserting) { 671 + context.missing(_handleMeta); 672 + } 673 + if (data.containsKey('payload')) { 674 + context.handle(_payloadMeta, payload.isAcceptableOrUnknown(data['payload']!, _payloadMeta)); 675 + } else if (isInserting) { 676 + context.missing(_payloadMeta); 677 + } 678 + if (data.containsKey('fetched_at')) { 679 + context.handle(_fetchedAtMeta, fetchedAt.isAcceptableOrUnknown(data['fetched_at']!, _fetchedAtMeta)); 680 + } 681 + return context; 682 + } 683 + 684 + @override 685 + Set<GeneratedColumn> get $primaryKey => {did}; 686 + @override 687 + CachedProfile map(Map<String, dynamic> data, {String? tablePrefix}) { 688 + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 689 + return CachedProfile( 690 + did: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}did'])!, 691 + handle: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}handle'])!, 692 + payload: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}payload'])!, 693 + fetchedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}fetched_at'])!, 694 + ); 695 + } 696 + 697 + @override 698 + $CachedProfilesTable createAlias(String alias) { 699 + return $CachedProfilesTable(attachedDatabase, alias); 700 + } 701 + } 702 + 703 + class CachedProfile extends DataClass implements Insertable<CachedProfile> { 704 + final String did; 705 + final String handle; 706 + final String payload; 707 + final DateTime fetchedAt; 708 + const CachedProfile({required this.did, required this.handle, required this.payload, required this.fetchedAt}); 709 + @override 710 + Map<String, Expression> toColumns(bool nullToAbsent) { 711 + final map = <String, Expression>{}; 712 + map['did'] = Variable<String>(did); 713 + map['handle'] = Variable<String>(handle); 714 + map['payload'] = Variable<String>(payload); 715 + map['fetched_at'] = Variable<DateTime>(fetchedAt); 716 + return map; 717 + } 718 + 719 + CachedProfilesCompanion toCompanion(bool nullToAbsent) { 720 + return CachedProfilesCompanion( 721 + did: Value(did), 722 + handle: Value(handle), 723 + payload: Value(payload), 724 + fetchedAt: Value(fetchedAt), 725 + ); 726 + } 727 + 728 + factory CachedProfile.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 729 + serializer ??= driftRuntimeOptions.defaultSerializer; 730 + return CachedProfile( 731 + did: serializer.fromJson<String>(json['did']), 732 + handle: serializer.fromJson<String>(json['handle']), 733 + payload: serializer.fromJson<String>(json['payload']), 734 + fetchedAt: serializer.fromJson<DateTime>(json['fetchedAt']), 735 + ); 736 + } 737 + @override 738 + Map<String, dynamic> toJson({ValueSerializer? serializer}) { 739 + serializer ??= driftRuntimeOptions.defaultSerializer; 740 + return <String, dynamic>{ 741 + 'did': serializer.toJson<String>(did), 742 + 'handle': serializer.toJson<String>(handle), 743 + 'payload': serializer.toJson<String>(payload), 744 + 'fetchedAt': serializer.toJson<DateTime>(fetchedAt), 745 + }; 746 + } 747 + 748 + CachedProfile copyWith({String? did, String? handle, String? payload, DateTime? fetchedAt}) => CachedProfile( 749 + did: did ?? this.did, 750 + handle: handle ?? this.handle, 751 + payload: payload ?? this.payload, 752 + fetchedAt: fetchedAt ?? this.fetchedAt, 753 + ); 754 + CachedProfile copyWithCompanion(CachedProfilesCompanion data) { 755 + return CachedProfile( 756 + did: data.did.present ? data.did.value : this.did, 757 + handle: data.handle.present ? data.handle.value : this.handle, 758 + payload: data.payload.present ? data.payload.value : this.payload, 759 + fetchedAt: data.fetchedAt.present ? data.fetchedAt.value : this.fetchedAt, 760 + ); 761 + } 762 + 763 + @override 764 + String toString() { 765 + return (StringBuffer('CachedProfile(') 766 + ..write('did: $did, ') 767 + ..write('handle: $handle, ') 768 + ..write('payload: $payload, ') 769 + ..write('fetchedAt: $fetchedAt') 770 + ..write(')')) 771 + .toString(); 772 + } 773 + 774 + @override 775 + int get hashCode => Object.hash(did, handle, payload, fetchedAt); 776 + @override 777 + bool operator ==(Object other) => 778 + identical(this, other) || 779 + (other is CachedProfile && 780 + other.did == this.did && 781 + other.handle == this.handle && 782 + other.payload == this.payload && 783 + other.fetchedAt == this.fetchedAt); 784 + } 785 + 786 + class CachedProfilesCompanion extends UpdateCompanion<CachedProfile> { 787 + final Value<String> did; 788 + final Value<String> handle; 789 + final Value<String> payload; 790 + final Value<DateTime> fetchedAt; 791 + final Value<int> rowid; 792 + const CachedProfilesCompanion({ 793 + this.did = const Value.absent(), 794 + this.handle = const Value.absent(), 795 + this.payload = const Value.absent(), 796 + this.fetchedAt = const Value.absent(), 797 + this.rowid = const Value.absent(), 798 + }); 799 + CachedProfilesCompanion.insert({ 800 + required String did, 801 + required String handle, 802 + required String payload, 803 + this.fetchedAt = const Value.absent(), 804 + this.rowid = const Value.absent(), 805 + }) : did = Value(did), 806 + handle = Value(handle), 807 + payload = Value(payload); 808 + static Insertable<CachedProfile> custom({ 809 + Expression<String>? did, 810 + Expression<String>? handle, 811 + Expression<String>? payload, 812 + Expression<DateTime>? fetchedAt, 813 + Expression<int>? rowid, 814 + }) { 815 + return RawValuesInsertable({ 816 + if (did != null) 'did': did, 817 + if (handle != null) 'handle': handle, 818 + if (payload != null) 'payload': payload, 819 + if (fetchedAt != null) 'fetched_at': fetchedAt, 820 + if (rowid != null) 'rowid': rowid, 821 + }); 822 + } 823 + 824 + CachedProfilesCompanion copyWith({ 825 + Value<String>? did, 826 + Value<String>? handle, 827 + Value<String>? payload, 828 + Value<DateTime>? fetchedAt, 829 + Value<int>? rowid, 830 + }) { 831 + return CachedProfilesCompanion( 832 + did: did ?? this.did, 833 + handle: handle ?? this.handle, 834 + payload: payload ?? this.payload, 835 + fetchedAt: fetchedAt ?? this.fetchedAt, 836 + rowid: rowid ?? this.rowid, 837 + ); 838 + } 839 + 840 + @override 841 + Map<String, Expression> toColumns(bool nullToAbsent) { 842 + final map = <String, Expression>{}; 843 + if (did.present) { 844 + map['did'] = Variable<String>(did.value); 845 + } 846 + if (handle.present) { 847 + map['handle'] = Variable<String>(handle.value); 848 + } 849 + if (payload.present) { 850 + map['payload'] = Variable<String>(payload.value); 851 + } 852 + if (fetchedAt.present) { 853 + map['fetched_at'] = Variable<DateTime>(fetchedAt.value); 854 + } 855 + if (rowid.present) { 856 + map['rowid'] = Variable<int>(rowid.value); 857 + } 858 + return map; 859 + } 860 + 861 + @override 862 + String toString() { 863 + return (StringBuffer('CachedProfilesCompanion(') 864 + ..write('did: $did, ') 865 + ..write('handle: $handle, ') 866 + ..write('payload: $payload, ') 867 + ..write('fetchedAt: $fetchedAt, ') 868 + ..write('rowid: $rowid') 869 + ..write(')')) 870 + .toString(); 871 + } 872 + } 873 + 874 + class $CachedPostsTable extends CachedPosts with TableInfo<$CachedPostsTable, CachedPost> { 875 + @override 876 + final GeneratedDatabase attachedDatabase; 877 + final String? _alias; 878 + $CachedPostsTable(this.attachedDatabase, [this._alias]); 879 + static const VerificationMeta _uriMeta = const VerificationMeta('uri'); 880 + @override 881 + late final GeneratedColumn<String> uri = GeneratedColumn<String>( 882 + 'uri', 883 + aliasedName, 884 + false, 885 + type: DriftSqlType.string, 886 + requiredDuringInsert: true, 887 + ); 888 + static const VerificationMeta _authorDidMeta = const VerificationMeta('authorDid'); 889 + @override 890 + late final GeneratedColumn<String> authorDid = GeneratedColumn<String>( 891 + 'author_did', 892 + aliasedName, 893 + false, 894 + type: DriftSqlType.string, 895 + requiredDuringInsert: true, 896 + ); 897 + static const VerificationMeta _payloadMeta = const VerificationMeta('payload'); 898 + @override 899 + late final GeneratedColumn<String> payload = GeneratedColumn<String>( 900 + 'payload', 901 + aliasedName, 902 + false, 903 + type: DriftSqlType.string, 904 + requiredDuringInsert: true, 905 + ); 906 + static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); 907 + @override 908 + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( 909 + 'created_at', 910 + aliasedName, 911 + true, 912 + type: DriftSqlType.dateTime, 913 + requiredDuringInsert: false, 914 + ); 915 + static const VerificationMeta _fetchedAtMeta = const VerificationMeta('fetchedAt'); 916 + @override 917 + late final GeneratedColumn<DateTime> fetchedAt = GeneratedColumn<DateTime>( 918 + 'fetched_at', 919 + aliasedName, 920 + false, 921 + type: DriftSqlType.dateTime, 922 + requiredDuringInsert: false, 923 + defaultValue: currentDateAndTime, 924 + ); 925 + @override 926 + List<GeneratedColumn> get $columns => [uri, authorDid, payload, createdAt, fetchedAt]; 927 + @override 928 + String get aliasedName => _alias ?? actualTableName; 929 + @override 930 + String get actualTableName => $name; 931 + static const String $name = 'cached_posts'; 932 + @override 933 + VerificationContext validateIntegrity(Insertable<CachedPost> instance, {bool isInserting = false}) { 934 + final context = VerificationContext(); 935 + final data = instance.toColumns(true); 936 + if (data.containsKey('uri')) { 937 + context.handle(_uriMeta, uri.isAcceptableOrUnknown(data['uri']!, _uriMeta)); 938 + } else if (isInserting) { 939 + context.missing(_uriMeta); 940 + } 941 + if (data.containsKey('author_did')) { 942 + context.handle(_authorDidMeta, authorDid.isAcceptableOrUnknown(data['author_did']!, _authorDidMeta)); 943 + } else if (isInserting) { 944 + context.missing(_authorDidMeta); 945 + } 946 + if (data.containsKey('payload')) { 947 + context.handle(_payloadMeta, payload.isAcceptableOrUnknown(data['payload']!, _payloadMeta)); 948 + } else if (isInserting) { 949 + context.missing(_payloadMeta); 950 + } 951 + if (data.containsKey('created_at')) { 952 + context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); 953 + } 954 + if (data.containsKey('fetched_at')) { 955 + context.handle(_fetchedAtMeta, fetchedAt.isAcceptableOrUnknown(data['fetched_at']!, _fetchedAtMeta)); 956 + } 957 + return context; 958 + } 959 + 960 + @override 961 + Set<GeneratedColumn> get $primaryKey => {uri}; 962 + @override 963 + CachedPost map(Map<String, dynamic> data, {String? tablePrefix}) { 964 + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 965 + return CachedPost( 966 + uri: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}uri'])!, 967 + authorDid: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}author_did'])!, 968 + payload: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}payload'])!, 969 + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at']), 970 + fetchedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}fetched_at'])!, 971 + ); 972 + } 973 + 974 + @override 975 + $CachedPostsTable createAlias(String alias) { 976 + return $CachedPostsTable(attachedDatabase, alias); 977 + } 978 + } 979 + 980 + class CachedPost extends DataClass implements Insertable<CachedPost> { 981 + final String uri; 982 + final String authorDid; 983 + final String payload; 984 + final DateTime? createdAt; 985 + final DateTime fetchedAt; 986 + const CachedPost({ 987 + required this.uri, 988 + required this.authorDid, 989 + required this.payload, 990 + this.createdAt, 991 + required this.fetchedAt, 992 + }); 993 + @override 994 + Map<String, Expression> toColumns(bool nullToAbsent) { 995 + final map = <String, Expression>{}; 996 + map['uri'] = Variable<String>(uri); 997 + map['author_did'] = Variable<String>(authorDid); 998 + map['payload'] = Variable<String>(payload); 999 + if (!nullToAbsent || createdAt != null) { 1000 + map['created_at'] = Variable<DateTime>(createdAt); 1001 + } 1002 + map['fetched_at'] = Variable<DateTime>(fetchedAt); 1003 + return map; 1004 + } 1005 + 1006 + CachedPostsCompanion toCompanion(bool nullToAbsent) { 1007 + return CachedPostsCompanion( 1008 + uri: Value(uri), 1009 + authorDid: Value(authorDid), 1010 + payload: Value(payload), 1011 + createdAt: createdAt == null && nullToAbsent ? const Value.absent() : Value(createdAt), 1012 + fetchedAt: Value(fetchedAt), 1013 + ); 1014 + } 1015 + 1016 + factory CachedPost.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 1017 + serializer ??= driftRuntimeOptions.defaultSerializer; 1018 + return CachedPost( 1019 + uri: serializer.fromJson<String>(json['uri']), 1020 + authorDid: serializer.fromJson<String>(json['authorDid']), 1021 + payload: serializer.fromJson<String>(json['payload']), 1022 + createdAt: serializer.fromJson<DateTime?>(json['createdAt']), 1023 + fetchedAt: serializer.fromJson<DateTime>(json['fetchedAt']), 1024 + ); 1025 + } 1026 + @override 1027 + Map<String, dynamic> toJson({ValueSerializer? serializer}) { 1028 + serializer ??= driftRuntimeOptions.defaultSerializer; 1029 + return <String, dynamic>{ 1030 + 'uri': serializer.toJson<String>(uri), 1031 + 'authorDid': serializer.toJson<String>(authorDid), 1032 + 'payload': serializer.toJson<String>(payload), 1033 + 'createdAt': serializer.toJson<DateTime?>(createdAt), 1034 + 'fetchedAt': serializer.toJson<DateTime>(fetchedAt), 1035 + }; 1036 + } 1037 + 1038 + CachedPost copyWith({ 1039 + String? uri, 1040 + String? authorDid, 1041 + String? payload, 1042 + Value<DateTime?> createdAt = const Value.absent(), 1043 + DateTime? fetchedAt, 1044 + }) => CachedPost( 1045 + uri: uri ?? this.uri, 1046 + authorDid: authorDid ?? this.authorDid, 1047 + payload: payload ?? this.payload, 1048 + createdAt: createdAt.present ? createdAt.value : this.createdAt, 1049 + fetchedAt: fetchedAt ?? this.fetchedAt, 1050 + ); 1051 + CachedPost copyWithCompanion(CachedPostsCompanion data) { 1052 + return CachedPost( 1053 + uri: data.uri.present ? data.uri.value : this.uri, 1054 + authorDid: data.authorDid.present ? data.authorDid.value : this.authorDid, 1055 + payload: data.payload.present ? data.payload.value : this.payload, 1056 + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, 1057 + fetchedAt: data.fetchedAt.present ? data.fetchedAt.value : this.fetchedAt, 1058 + ); 1059 + } 1060 + 1061 + @override 1062 + String toString() { 1063 + return (StringBuffer('CachedPost(') 1064 + ..write('uri: $uri, ') 1065 + ..write('authorDid: $authorDid, ') 1066 + ..write('payload: $payload, ') 1067 + ..write('createdAt: $createdAt, ') 1068 + ..write('fetchedAt: $fetchedAt') 1069 + ..write(')')) 1070 + .toString(); 1071 + } 1072 + 1073 + @override 1074 + int get hashCode => Object.hash(uri, authorDid, payload, createdAt, fetchedAt); 1075 + @override 1076 + bool operator ==(Object other) => 1077 + identical(this, other) || 1078 + (other is CachedPost && 1079 + other.uri == this.uri && 1080 + other.authorDid == this.authorDid && 1081 + other.payload == this.payload && 1082 + other.createdAt == this.createdAt && 1083 + other.fetchedAt == this.fetchedAt); 1084 + } 1085 + 1086 + class CachedPostsCompanion extends UpdateCompanion<CachedPost> { 1087 + final Value<String> uri; 1088 + final Value<String> authorDid; 1089 + final Value<String> payload; 1090 + final Value<DateTime?> createdAt; 1091 + final Value<DateTime> fetchedAt; 1092 + final Value<int> rowid; 1093 + const CachedPostsCompanion({ 1094 + this.uri = const Value.absent(), 1095 + this.authorDid = const Value.absent(), 1096 + this.payload = const Value.absent(), 1097 + this.createdAt = const Value.absent(), 1098 + this.fetchedAt = const Value.absent(), 1099 + this.rowid = const Value.absent(), 1100 + }); 1101 + CachedPostsCompanion.insert({ 1102 + required String uri, 1103 + required String authorDid, 1104 + required String payload, 1105 + this.createdAt = const Value.absent(), 1106 + this.fetchedAt = const Value.absent(), 1107 + this.rowid = const Value.absent(), 1108 + }) : uri = Value(uri), 1109 + authorDid = Value(authorDid), 1110 + payload = Value(payload); 1111 + static Insertable<CachedPost> custom({ 1112 + Expression<String>? uri, 1113 + Expression<String>? authorDid, 1114 + Expression<String>? payload, 1115 + Expression<DateTime>? createdAt, 1116 + Expression<DateTime>? fetchedAt, 1117 + Expression<int>? rowid, 1118 + }) { 1119 + return RawValuesInsertable({ 1120 + if (uri != null) 'uri': uri, 1121 + if (authorDid != null) 'author_did': authorDid, 1122 + if (payload != null) 'payload': payload, 1123 + if (createdAt != null) 'created_at': createdAt, 1124 + if (fetchedAt != null) 'fetched_at': fetchedAt, 1125 + if (rowid != null) 'rowid': rowid, 1126 + }); 1127 + } 1128 + 1129 + CachedPostsCompanion copyWith({ 1130 + Value<String>? uri, 1131 + Value<String>? authorDid, 1132 + Value<String>? payload, 1133 + Value<DateTime?>? createdAt, 1134 + Value<DateTime>? fetchedAt, 1135 + Value<int>? rowid, 1136 + }) { 1137 + return CachedPostsCompanion( 1138 + uri: uri ?? this.uri, 1139 + authorDid: authorDid ?? this.authorDid, 1140 + payload: payload ?? this.payload, 1141 + createdAt: createdAt ?? this.createdAt, 1142 + fetchedAt: fetchedAt ?? this.fetchedAt, 1143 + rowid: rowid ?? this.rowid, 1144 + ); 1145 + } 1146 + 1147 + @override 1148 + Map<String, Expression> toColumns(bool nullToAbsent) { 1149 + final map = <String, Expression>{}; 1150 + if (uri.present) { 1151 + map['uri'] = Variable<String>(uri.value); 1152 + } 1153 + if (authorDid.present) { 1154 + map['author_did'] = Variable<String>(authorDid.value); 1155 + } 1156 + if (payload.present) { 1157 + map['payload'] = Variable<String>(payload.value); 1158 + } 1159 + if (createdAt.present) { 1160 + map['created_at'] = Variable<DateTime>(createdAt.value); 1161 + } 1162 + if (fetchedAt.present) { 1163 + map['fetched_at'] = Variable<DateTime>(fetchedAt.value); 1164 + } 1165 + if (rowid.present) { 1166 + map['rowid'] = Variable<int>(rowid.value); 1167 + } 1168 + return map; 1169 + } 1170 + 1171 + @override 1172 + String toString() { 1173 + return (StringBuffer('CachedPostsCompanion(') 1174 + ..write('uri: $uri, ') 1175 + ..write('authorDid: $authorDid, ') 1176 + ..write('payload: $payload, ') 1177 + ..write('createdAt: $createdAt, ') 1178 + ..write('fetchedAt: $fetchedAt, ') 1179 + ..write('rowid: $rowid') 1180 + ..write(')')) 1181 + .toString(); 1182 + } 1183 + } 1184 + 481 1185 class $SettingsTable extends Settings with TableInfo<$SettingsTable, SettingsEntry> { 482 1186 @override 483 1187 final GeneratedDatabase attachedDatabase; ··· 703 1407 _$AppDatabase(QueryExecutor e) : super(e); 704 1408 $AppDatabaseManager get managers => $AppDatabaseManager(this); 705 1409 late final $AccountsTable accounts = $AccountsTable(this); 1410 + late final $CachedProfilesTable cachedProfiles = $CachedProfilesTable(this); 1411 + late final $CachedPostsTable cachedPosts = $CachedPostsTable(this); 706 1412 late final $SettingsTable settings = $SettingsTable(this); 707 1413 @override 708 1414 Iterable<TableInfo<Table, Object?>> get allTables => allSchemaEntities.whereType<TableInfo<Table, Object?>>(); 709 1415 @override 710 - List<DatabaseSchemaEntity> get allSchemaEntities => [accounts, settings]; 1416 + List<DatabaseSchemaEntity> get allSchemaEntities => [accounts, cachedProfiles, cachedPosts, settings]; 711 1417 } 712 1418 713 1419 typedef $$AccountsTableCreateCompanionBuilder = ··· 715 1421 required String did, 716 1422 required String handle, 717 1423 Value<String?> displayName, 1424 + Value<String?> service, 718 1425 required String accessToken, 719 1426 Value<String?> refreshToken, 1427 + Value<String?> dpopPublicKey, 720 1428 Value<String?> dpopPrivateKey, 1429 + Value<String?> dpopNonce, 721 1430 Value<DateTime?> expiresAt, 722 1431 Value<DateTime> createdAt, 723 1432 Value<DateTime> updatedAt, ··· 728 1437 Value<String> did, 729 1438 Value<String> handle, 730 1439 Value<String?> displayName, 1440 + Value<String?> service, 731 1441 Value<String> accessToken, 732 1442 Value<String?> refreshToken, 1443 + Value<String?> dpopPublicKey, 733 1444 Value<String?> dpopPrivateKey, 1445 + Value<String?> dpopNonce, 734 1446 Value<DateTime?> expiresAt, 735 1447 Value<DateTime> createdAt, 736 1448 Value<DateTime> updatedAt, ··· 753 1465 ColumnFilters<String> get displayName => 754 1466 $composableBuilder(column: $table.displayName, builder: (column) => ColumnFilters(column)); 755 1467 1468 + ColumnFilters<String> get service => 1469 + $composableBuilder(column: $table.service, builder: (column) => ColumnFilters(column)); 1470 + 756 1471 ColumnFilters<String> get accessToken => 757 1472 $composableBuilder(column: $table.accessToken, builder: (column) => ColumnFilters(column)); 758 1473 759 1474 ColumnFilters<String> get refreshToken => 760 1475 $composableBuilder(column: $table.refreshToken, builder: (column) => ColumnFilters(column)); 1476 + 1477 + ColumnFilters<String> get dpopPublicKey => 1478 + $composableBuilder(column: $table.dpopPublicKey, builder: (column) => ColumnFilters(column)); 761 1479 762 1480 ColumnFilters<String> get dpopPrivateKey => 763 1481 $composableBuilder(column: $table.dpopPrivateKey, builder: (column) => ColumnFilters(column)); 1482 + 1483 + ColumnFilters<String> get dpopNonce => 1484 + $composableBuilder(column: $table.dpopNonce, builder: (column) => ColumnFilters(column)); 764 1485 765 1486 ColumnFilters<DateTime> get expiresAt => 766 1487 $composableBuilder(column: $table.expiresAt, builder: (column) => ColumnFilters(column)); ··· 789 1510 ColumnOrderings<String> get displayName => 790 1511 $composableBuilder(column: $table.displayName, builder: (column) => ColumnOrderings(column)); 791 1512 1513 + ColumnOrderings<String> get service => 1514 + $composableBuilder(column: $table.service, builder: (column) => ColumnOrderings(column)); 1515 + 792 1516 ColumnOrderings<String> get accessToken => 793 1517 $composableBuilder(column: $table.accessToken, builder: (column) => ColumnOrderings(column)); 794 1518 795 1519 ColumnOrderings<String> get refreshToken => 796 1520 $composableBuilder(column: $table.refreshToken, builder: (column) => ColumnOrderings(column)); 797 1521 1522 + ColumnOrderings<String> get dpopPublicKey => 1523 + $composableBuilder(column: $table.dpopPublicKey, builder: (column) => ColumnOrderings(column)); 1524 + 798 1525 ColumnOrderings<String> get dpopPrivateKey => 799 1526 $composableBuilder(column: $table.dpopPrivateKey, builder: (column) => ColumnOrderings(column)); 1527 + 1528 + ColumnOrderings<String> get dpopNonce => 1529 + $composableBuilder(column: $table.dpopNonce, builder: (column) => ColumnOrderings(column)); 800 1530 801 1531 ColumnOrderings<DateTime> get expiresAt => 802 1532 $composableBuilder(column: $table.expiresAt, builder: (column) => ColumnOrderings(column)); ··· 823 1553 GeneratedColumn<String> get displayName => 824 1554 $composableBuilder(column: $table.displayName, builder: (column) => column); 825 1555 1556 + GeneratedColumn<String> get service => $composableBuilder(column: $table.service, builder: (column) => column); 1557 + 826 1558 GeneratedColumn<String> get accessToken => 827 1559 $composableBuilder(column: $table.accessToken, builder: (column) => column); 828 1560 829 1561 GeneratedColumn<String> get refreshToken => 830 1562 $composableBuilder(column: $table.refreshToken, builder: (column) => column); 1563 + 1564 + GeneratedColumn<String> get dpopPublicKey => 1565 + $composableBuilder(column: $table.dpopPublicKey, builder: (column) => column); 831 1566 832 1567 GeneratedColumn<String> get dpopPrivateKey => 833 1568 $composableBuilder(column: $table.dpopPrivateKey, builder: (column) => column); 834 1569 1570 + GeneratedColumn<String> get dpopNonce => $composableBuilder(column: $table.dpopNonce, builder: (column) => column); 1571 + 835 1572 GeneratedColumn<DateTime> get expiresAt => $composableBuilder(column: $table.expiresAt, builder: (column) => column); 836 1573 837 1574 GeneratedColumn<DateTime> get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); ··· 867 1604 Value<String> did = const Value.absent(), 868 1605 Value<String> handle = const Value.absent(), 869 1606 Value<String?> displayName = const Value.absent(), 1607 + Value<String?> service = const Value.absent(), 870 1608 Value<String> accessToken = const Value.absent(), 871 1609 Value<String?> refreshToken = const Value.absent(), 1610 + Value<String?> dpopPublicKey = const Value.absent(), 872 1611 Value<String?> dpopPrivateKey = const Value.absent(), 1612 + Value<String?> dpopNonce = const Value.absent(), 873 1613 Value<DateTime?> expiresAt = const Value.absent(), 874 1614 Value<DateTime> createdAt = const Value.absent(), 875 1615 Value<DateTime> updatedAt = const Value.absent(), ··· 878 1618 did: did, 879 1619 handle: handle, 880 1620 displayName: displayName, 1621 + service: service, 881 1622 accessToken: accessToken, 882 1623 refreshToken: refreshToken, 1624 + dpopPublicKey: dpopPublicKey, 883 1625 dpopPrivateKey: dpopPrivateKey, 1626 + dpopNonce: dpopNonce, 884 1627 expiresAt: expiresAt, 885 1628 createdAt: createdAt, 886 1629 updatedAt: updatedAt, ··· 891 1634 required String did, 892 1635 required String handle, 893 1636 Value<String?> displayName = const Value.absent(), 1637 + Value<String?> service = const Value.absent(), 894 1638 required String accessToken, 895 1639 Value<String?> refreshToken = const Value.absent(), 1640 + Value<String?> dpopPublicKey = const Value.absent(), 896 1641 Value<String?> dpopPrivateKey = const Value.absent(), 1642 + Value<String?> dpopNonce = const Value.absent(), 897 1643 Value<DateTime?> expiresAt = const Value.absent(), 898 1644 Value<DateTime> createdAt = const Value.absent(), 899 1645 Value<DateTime> updatedAt = const Value.absent(), ··· 902 1648 did: did, 903 1649 handle: handle, 904 1650 displayName: displayName, 1651 + service: service, 905 1652 accessToken: accessToken, 906 1653 refreshToken: refreshToken, 1654 + dpopPublicKey: dpopPublicKey, 907 1655 dpopPrivateKey: dpopPrivateKey, 1656 + dpopNonce: dpopNonce, 908 1657 expiresAt: expiresAt, 909 1658 createdAt: createdAt, 910 1659 updatedAt: updatedAt, ··· 930 1679 Account, 931 1680 PrefetchHooks Function() 932 1681 >; 1682 + typedef $$CachedProfilesTableCreateCompanionBuilder = 1683 + CachedProfilesCompanion Function({ 1684 + required String did, 1685 + required String handle, 1686 + required String payload, 1687 + Value<DateTime> fetchedAt, 1688 + Value<int> rowid, 1689 + }); 1690 + typedef $$CachedProfilesTableUpdateCompanionBuilder = 1691 + CachedProfilesCompanion Function({ 1692 + Value<String> did, 1693 + Value<String> handle, 1694 + Value<String> payload, 1695 + Value<DateTime> fetchedAt, 1696 + Value<int> rowid, 1697 + }); 1698 + 1699 + class $$CachedProfilesTableFilterComposer extends Composer<_$AppDatabase, $CachedProfilesTable> { 1700 + $$CachedProfilesTableFilterComposer({ 1701 + required super.$db, 1702 + required super.$table, 1703 + super.joinBuilder, 1704 + super.$addJoinBuilderToRootComposer, 1705 + super.$removeJoinBuilderFromRootComposer, 1706 + }); 1707 + ColumnFilters<String> get did => $composableBuilder(column: $table.did, builder: (column) => ColumnFilters(column)); 1708 + 1709 + ColumnFilters<String> get handle => 1710 + $composableBuilder(column: $table.handle, builder: (column) => ColumnFilters(column)); 1711 + 1712 + ColumnFilters<String> get payload => 1713 + $composableBuilder(column: $table.payload, builder: (column) => ColumnFilters(column)); 1714 + 1715 + ColumnFilters<DateTime> get fetchedAt => 1716 + $composableBuilder(column: $table.fetchedAt, builder: (column) => ColumnFilters(column)); 1717 + } 1718 + 1719 + class $$CachedProfilesTableOrderingComposer extends Composer<_$AppDatabase, $CachedProfilesTable> { 1720 + $$CachedProfilesTableOrderingComposer({ 1721 + required super.$db, 1722 + required super.$table, 1723 + super.joinBuilder, 1724 + super.$addJoinBuilderToRootComposer, 1725 + super.$removeJoinBuilderFromRootComposer, 1726 + }); 1727 + ColumnOrderings<String> get did => 1728 + $composableBuilder(column: $table.did, builder: (column) => ColumnOrderings(column)); 1729 + 1730 + ColumnOrderings<String> get handle => 1731 + $composableBuilder(column: $table.handle, builder: (column) => ColumnOrderings(column)); 1732 + 1733 + ColumnOrderings<String> get payload => 1734 + $composableBuilder(column: $table.payload, builder: (column) => ColumnOrderings(column)); 1735 + 1736 + ColumnOrderings<DateTime> get fetchedAt => 1737 + $composableBuilder(column: $table.fetchedAt, builder: (column) => ColumnOrderings(column)); 1738 + } 1739 + 1740 + class $$CachedProfilesTableAnnotationComposer extends Composer<_$AppDatabase, $CachedProfilesTable> { 1741 + $$CachedProfilesTableAnnotationComposer({ 1742 + required super.$db, 1743 + required super.$table, 1744 + super.joinBuilder, 1745 + super.$addJoinBuilderToRootComposer, 1746 + super.$removeJoinBuilderFromRootComposer, 1747 + }); 1748 + GeneratedColumn<String> get did => $composableBuilder(column: $table.did, builder: (column) => column); 1749 + 1750 + GeneratedColumn<String> get handle => $composableBuilder(column: $table.handle, builder: (column) => column); 1751 + 1752 + GeneratedColumn<String> get payload => $composableBuilder(column: $table.payload, builder: (column) => column); 1753 + 1754 + GeneratedColumn<DateTime> get fetchedAt => $composableBuilder(column: $table.fetchedAt, builder: (column) => column); 1755 + } 1756 + 1757 + class $$CachedProfilesTableTableManager 1758 + extends 1759 + RootTableManager< 1760 + _$AppDatabase, 1761 + $CachedProfilesTable, 1762 + CachedProfile, 1763 + $$CachedProfilesTableFilterComposer, 1764 + $$CachedProfilesTableOrderingComposer, 1765 + $$CachedProfilesTableAnnotationComposer, 1766 + $$CachedProfilesTableCreateCompanionBuilder, 1767 + $$CachedProfilesTableUpdateCompanionBuilder, 1768 + (CachedProfile, BaseReferences<_$AppDatabase, $CachedProfilesTable, CachedProfile>), 1769 + CachedProfile, 1770 + PrefetchHooks Function() 1771 + > { 1772 + $$CachedProfilesTableTableManager(_$AppDatabase db, $CachedProfilesTable table) 1773 + : super( 1774 + TableManagerState( 1775 + db: db, 1776 + table: table, 1777 + createFilteringComposer: () => $$CachedProfilesTableFilterComposer($db: db, $table: table), 1778 + createOrderingComposer: () => $$CachedProfilesTableOrderingComposer($db: db, $table: table), 1779 + createComputedFieldComposer: () => $$CachedProfilesTableAnnotationComposer($db: db, $table: table), 1780 + updateCompanionCallback: 1781 + ({ 1782 + Value<String> did = const Value.absent(), 1783 + Value<String> handle = const Value.absent(), 1784 + Value<String> payload = const Value.absent(), 1785 + Value<DateTime> fetchedAt = const Value.absent(), 1786 + Value<int> rowid = const Value.absent(), 1787 + }) => CachedProfilesCompanion( 1788 + did: did, 1789 + handle: handle, 1790 + payload: payload, 1791 + fetchedAt: fetchedAt, 1792 + rowid: rowid, 1793 + ), 1794 + createCompanionCallback: 1795 + ({ 1796 + required String did, 1797 + required String handle, 1798 + required String payload, 1799 + Value<DateTime> fetchedAt = const Value.absent(), 1800 + Value<int> rowid = const Value.absent(), 1801 + }) => CachedProfilesCompanion.insert( 1802 + did: did, 1803 + handle: handle, 1804 + payload: payload, 1805 + fetchedAt: fetchedAt, 1806 + rowid: rowid, 1807 + ), 1808 + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 1809 + prefetchHooksCallback: null, 1810 + ), 1811 + ); 1812 + } 1813 + 1814 + typedef $$CachedProfilesTableProcessedTableManager = 1815 + ProcessedTableManager< 1816 + _$AppDatabase, 1817 + $CachedProfilesTable, 1818 + CachedProfile, 1819 + $$CachedProfilesTableFilterComposer, 1820 + $$CachedProfilesTableOrderingComposer, 1821 + $$CachedProfilesTableAnnotationComposer, 1822 + $$CachedProfilesTableCreateCompanionBuilder, 1823 + $$CachedProfilesTableUpdateCompanionBuilder, 1824 + (CachedProfile, BaseReferences<_$AppDatabase, $CachedProfilesTable, CachedProfile>), 1825 + CachedProfile, 1826 + PrefetchHooks Function() 1827 + >; 1828 + typedef $$CachedPostsTableCreateCompanionBuilder = 1829 + CachedPostsCompanion Function({ 1830 + required String uri, 1831 + required String authorDid, 1832 + required String payload, 1833 + Value<DateTime?> createdAt, 1834 + Value<DateTime> fetchedAt, 1835 + Value<int> rowid, 1836 + }); 1837 + typedef $$CachedPostsTableUpdateCompanionBuilder = 1838 + CachedPostsCompanion Function({ 1839 + Value<String> uri, 1840 + Value<String> authorDid, 1841 + Value<String> payload, 1842 + Value<DateTime?> createdAt, 1843 + Value<DateTime> fetchedAt, 1844 + Value<int> rowid, 1845 + }); 1846 + 1847 + class $$CachedPostsTableFilterComposer extends Composer<_$AppDatabase, $CachedPostsTable> { 1848 + $$CachedPostsTableFilterComposer({ 1849 + required super.$db, 1850 + required super.$table, 1851 + super.joinBuilder, 1852 + super.$addJoinBuilderToRootComposer, 1853 + super.$removeJoinBuilderFromRootComposer, 1854 + }); 1855 + ColumnFilters<String> get uri => $composableBuilder(column: $table.uri, builder: (column) => ColumnFilters(column)); 1856 + 1857 + ColumnFilters<String> get authorDid => 1858 + $composableBuilder(column: $table.authorDid, builder: (column) => ColumnFilters(column)); 1859 + 1860 + ColumnFilters<String> get payload => 1861 + $composableBuilder(column: $table.payload, builder: (column) => ColumnFilters(column)); 1862 + 1863 + ColumnFilters<DateTime> get createdAt => 1864 + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); 1865 + 1866 + ColumnFilters<DateTime> get fetchedAt => 1867 + $composableBuilder(column: $table.fetchedAt, builder: (column) => ColumnFilters(column)); 1868 + } 1869 + 1870 + class $$CachedPostsTableOrderingComposer extends Composer<_$AppDatabase, $CachedPostsTable> { 1871 + $$CachedPostsTableOrderingComposer({ 1872 + required super.$db, 1873 + required super.$table, 1874 + super.joinBuilder, 1875 + super.$addJoinBuilderToRootComposer, 1876 + super.$removeJoinBuilderFromRootComposer, 1877 + }); 1878 + ColumnOrderings<String> get uri => 1879 + $composableBuilder(column: $table.uri, builder: (column) => ColumnOrderings(column)); 1880 + 1881 + ColumnOrderings<String> get authorDid => 1882 + $composableBuilder(column: $table.authorDid, builder: (column) => ColumnOrderings(column)); 1883 + 1884 + ColumnOrderings<String> get payload => 1885 + $composableBuilder(column: $table.payload, builder: (column) => ColumnOrderings(column)); 1886 + 1887 + ColumnOrderings<DateTime> get createdAt => 1888 + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnOrderings(column)); 1889 + 1890 + ColumnOrderings<DateTime> get fetchedAt => 1891 + $composableBuilder(column: $table.fetchedAt, builder: (column) => ColumnOrderings(column)); 1892 + } 1893 + 1894 + class $$CachedPostsTableAnnotationComposer extends Composer<_$AppDatabase, $CachedPostsTable> { 1895 + $$CachedPostsTableAnnotationComposer({ 1896 + required super.$db, 1897 + required super.$table, 1898 + super.joinBuilder, 1899 + super.$addJoinBuilderToRootComposer, 1900 + super.$removeJoinBuilderFromRootComposer, 1901 + }); 1902 + GeneratedColumn<String> get uri => $composableBuilder(column: $table.uri, builder: (column) => column); 1903 + 1904 + GeneratedColumn<String> get authorDid => $composableBuilder(column: $table.authorDid, builder: (column) => column); 1905 + 1906 + GeneratedColumn<String> get payload => $composableBuilder(column: $table.payload, builder: (column) => column); 1907 + 1908 + GeneratedColumn<DateTime> get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); 1909 + 1910 + GeneratedColumn<DateTime> get fetchedAt => $composableBuilder(column: $table.fetchedAt, builder: (column) => column); 1911 + } 1912 + 1913 + class $$CachedPostsTableTableManager 1914 + extends 1915 + RootTableManager< 1916 + _$AppDatabase, 1917 + $CachedPostsTable, 1918 + CachedPost, 1919 + $$CachedPostsTableFilterComposer, 1920 + $$CachedPostsTableOrderingComposer, 1921 + $$CachedPostsTableAnnotationComposer, 1922 + $$CachedPostsTableCreateCompanionBuilder, 1923 + $$CachedPostsTableUpdateCompanionBuilder, 1924 + (CachedPost, BaseReferences<_$AppDatabase, $CachedPostsTable, CachedPost>), 1925 + CachedPost, 1926 + PrefetchHooks Function() 1927 + > { 1928 + $$CachedPostsTableTableManager(_$AppDatabase db, $CachedPostsTable table) 1929 + : super( 1930 + TableManagerState( 1931 + db: db, 1932 + table: table, 1933 + createFilteringComposer: () => $$CachedPostsTableFilterComposer($db: db, $table: table), 1934 + createOrderingComposer: () => $$CachedPostsTableOrderingComposer($db: db, $table: table), 1935 + createComputedFieldComposer: () => $$CachedPostsTableAnnotationComposer($db: db, $table: table), 1936 + updateCompanionCallback: 1937 + ({ 1938 + Value<String> uri = const Value.absent(), 1939 + Value<String> authorDid = const Value.absent(), 1940 + Value<String> payload = const Value.absent(), 1941 + Value<DateTime?> createdAt = const Value.absent(), 1942 + Value<DateTime> fetchedAt = const Value.absent(), 1943 + Value<int> rowid = const Value.absent(), 1944 + }) => CachedPostsCompanion( 1945 + uri: uri, 1946 + authorDid: authorDid, 1947 + payload: payload, 1948 + createdAt: createdAt, 1949 + fetchedAt: fetchedAt, 1950 + rowid: rowid, 1951 + ), 1952 + createCompanionCallback: 1953 + ({ 1954 + required String uri, 1955 + required String authorDid, 1956 + required String payload, 1957 + Value<DateTime?> createdAt = const Value.absent(), 1958 + Value<DateTime> fetchedAt = const Value.absent(), 1959 + Value<int> rowid = const Value.absent(), 1960 + }) => CachedPostsCompanion.insert( 1961 + uri: uri, 1962 + authorDid: authorDid, 1963 + payload: payload, 1964 + createdAt: createdAt, 1965 + fetchedAt: fetchedAt, 1966 + rowid: rowid, 1967 + ), 1968 + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 1969 + prefetchHooksCallback: null, 1970 + ), 1971 + ); 1972 + } 1973 + 1974 + typedef $$CachedPostsTableProcessedTableManager = 1975 + ProcessedTableManager< 1976 + _$AppDatabase, 1977 + $CachedPostsTable, 1978 + CachedPost, 1979 + $$CachedPostsTableFilterComposer, 1980 + $$CachedPostsTableOrderingComposer, 1981 + $$CachedPostsTableAnnotationComposer, 1982 + $$CachedPostsTableCreateCompanionBuilder, 1983 + $$CachedPostsTableUpdateCompanionBuilder, 1984 + (CachedPost, BaseReferences<_$AppDatabase, $CachedPostsTable, CachedPost>), 1985 + CachedPost, 1986 + PrefetchHooks Function() 1987 + >; 933 1988 typedef $$SettingsTableCreateCompanionBuilder = 934 1989 SettingsCompanion Function({ 935 1990 required String key, ··· 1052 2107 final _$AppDatabase _db; 1053 2108 $AppDatabaseManager(this._db); 1054 2109 $$AccountsTableTableManager get accounts => $$AccountsTableTableManager(_db, _db.accounts); 2110 + $$CachedProfilesTableTableManager get cachedProfiles => $$CachedProfilesTableTableManager(_db, _db.cachedProfiles); 2111 + $$CachedPostsTableTableManager get cachedPosts => $$CachedPostsTableTableManager(_db, _db.cachedPosts); 1055 2112 $$SettingsTableTableManager get settings => $$SettingsTableTableManager(_db, _db.settings); 1056 2113 }
+26
lib/core/database/tables.dart
··· 5 5 TextColumn get did => text()(); 6 6 TextColumn get handle => text()(); 7 7 TextColumn get displayName => text().nullable()(); 8 + TextColumn get service => text().nullable()(); 8 9 TextColumn get accessToken => text()(); 9 10 TextColumn get refreshToken => text().nullable()(); 11 + TextColumn get dpopPublicKey => text().nullable()(); 10 12 TextColumn get dpopPrivateKey => text().nullable()(); 13 + TextColumn get dpopNonce => text().nullable()(); 11 14 DateTimeColumn get expiresAt => dateTime().nullable()(); 12 15 DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); 13 16 DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); 14 17 15 18 @override 16 19 Set<Column> get primaryKey => {did}; 20 + } 21 + 22 + @DataClassName('CachedProfile') 23 + class CachedProfiles extends Table { 24 + TextColumn get did => text()(); 25 + TextColumn get handle => text()(); 26 + TextColumn get payload => text()(); 27 + DateTimeColumn get fetchedAt => dateTime().withDefault(currentDateAndTime)(); 28 + 29 + @override 30 + Set<Column> get primaryKey => {did}; 31 + } 32 + 33 + @DataClassName('CachedPost') 34 + class CachedPosts extends Table { 35 + TextColumn get uri => text()(); 36 + TextColumn get authorDid => text()(); 37 + TextColumn get payload => text()(); 38 + DateTimeColumn get createdAt => dateTime().nullable()(); 39 + DateTimeColumn get fetchedAt => dateTime().withDefault(currentDateAndTime)(); 40 + 41 + @override 42 + Set<Column> get primaryKey => {uri}; 17 43 } 18 44 19 45 @DataClassName('SettingsEntry')
+2
lib/core/router/app_router.dart
··· 5 5 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 6 6 import 'package:lazurite/features/auth/presentation/home_screen.dart'; 7 7 import 'package:lazurite/features/auth/presentation/login_screen.dart'; 8 + import 'package:lazurite/features/profile/presentation/profile_screen.dart'; 8 9 import 'package:lazurite/features/settings/presentation/settings_screen.dart'; 9 10 10 11 class AppRouter { ··· 30 31 routes: [ 31 32 GoRoute(path: '/', builder: (context, state) => const HomeScreen()), 32 33 GoRoute(path: '/login', builder: (context, state) => const LoginScreen()), 34 + GoRoute(path: '/profile', builder: (context, state) => const ProfileScreen()), 33 35 GoRoute(path: '/settings', builder: (context, state) => const SettingsScreen()), 34 36 ], 35 37 );
+36 -45
lib/features/auth/bloc/auth_bloc.dart
··· 7 7 part 'auth_state.dart'; 8 8 9 9 class AuthBloc extends Bloc<AuthEvent, AuthState> { 10 - AuthBloc({required AuthRepository authRepository}) 10 + AuthBloc({required AuthRepository authRepository, AuthState initialState = const AuthState.unauthenticated()}) 11 11 : _authRepository = authRepository, 12 - super(const AuthState.unauthenticated()) { 13 - on<AuthEvent>(_onEvent); 12 + super(initialState) { 13 + on<LoginRequested>(_onLoginRequested); 14 + on<OAuthLoginRequested>(_onOAuthLoginRequested); 15 + on<LogoutRequested>(_onLogoutRequested); 16 + on<SessionRestored>(_onSessionRestored); 17 + on<CheckSessionRequested>(_onCheckSessionRequested); 14 18 } 19 + 15 20 final AuthRepository _authRepository; 16 21 17 - Future<void> _onEvent(AuthEvent event, Emitter<AuthState> emit) async { 18 - if (event is LoginRequested) { 19 - await _onLoginRequested(event, emit); 20 - } else if (event is OAuthLoginRequested) { 21 - await _onOAuthLoginRequested(event, emit); 22 - } else if (event is LogoutRequested) { 23 - await _onLogoutRequested(event, emit); 24 - } else if (event is SessionRestored) { 25 - await _onSessionRestored(event, emit); 26 - } else if (event is CheckSessionRequested) { 27 - await _onCheckSessionRequested(event, emit); 28 - } 29 - } 30 - 31 22 Future<void> _onLoginRequested(LoginRequested event, Emitter<AuthState> emit) async { 32 23 emit(const AuthState.authenticating()); 24 + 33 25 try { 34 26 final tokens = await _authRepository.loginWithAppPassword(event.handle, event.appPassword); 35 - if (tokens != null) { 36 - emit(AuthState.authenticated(tokens)); 37 - } else { 38 - emit(const AuthState.authError('Login failed')); 27 + 28 + if (tokens == null) { 29 + emit(const AuthState.authError('Login failed.')); 30 + return; 39 31 } 40 - } catch (e) { 41 - emit(const AuthState.authError('Login failed: \$e')); 32 + 33 + emit(AuthState.authenticated(tokens)); 34 + } catch (error) { 35 + emit(AuthState.authError('Login failed: $error')); 42 36 } 43 37 } 44 38 45 39 Future<void> _onOAuthLoginRequested(OAuthLoginRequested event, Emitter<AuthState> emit) async { 46 40 emit(const AuthState.authenticating()); 41 + 47 42 try { 48 43 final tokens = await _authRepository.loginWithOAuth(event.handle); 49 - if (tokens != null) { 50 - emit(AuthState.authenticated(tokens)); 51 - } else { 52 - emit(const AuthState.authError('OAuth login failed')); 44 + 45 + if (tokens == null) { 46 + emit(const AuthState.authError('OAuth login failed.')); 47 + return; 53 48 } 54 - } catch (e) { 55 - emit(const AuthState.authError('OAuth login failed: \$e')); 49 + 50 + emit(AuthState.authenticated(tokens)); 51 + } catch (error) { 52 + emit(AuthState.authError('OAuth login failed: $error')); 56 53 } 57 54 } 58 55 ··· 60 57 try { 61 58 await _authRepository.logout(); 62 59 emit(const AuthState.unauthenticated()); 63 - } catch (e) { 64 - emit(const AuthState.authError('Logout failed: \$e')); 60 + } catch (error) { 61 + emit(AuthState.authError('Logout failed: $error')); 65 62 } 66 63 } 67 64 ··· 70 67 } 71 68 72 69 Future<void> _onCheckSessionRequested(CheckSessionRequested event, Emitter<AuthState> emit) async { 70 + emit(const AuthState.authenticating()); 71 + 73 72 try { 74 - final tokens = await _authRepository.getStoredSession(); 75 - if (tokens != null) { 76 - if (tokens.isExpired && tokens.refreshToken != null) { 77 - final refreshed = await _authRepository.refreshSession(tokens.refreshToken!); 78 - if (refreshed != null) { 79 - emit(AuthState.authenticated(refreshed)); 80 - } else { 81 - emit(const AuthState.unauthenticated()); 82 - } 83 - } else { 84 - emit(AuthState.authenticated(tokens)); 85 - } 86 - } else { 73 + final tokens = await _authRepository.restoreSession(); 74 + if (tokens == null) { 87 75 emit(const AuthState.unauthenticated()); 76 + return; 88 77 } 89 - } catch (e) { 78 + 79 + emit(AuthState.authenticated(tokens)); 80 + } catch (_) { 90 81 emit(const AuthState.unauthenticated()); 91 82 } 92 83 }
+299 -77
lib/features/auth/data/auth_repository.dart
··· 1 1 import 'dart:async'; 2 - import 'dart:convert'; 3 2 import 'dart:io'; 4 - import 'dart:math'; 5 3 4 + import 'package:atproto/atproto.dart' as atp; 5 + import 'package:atproto_core/atproto_core.dart' as atcore; 6 6 import 'package:atproto_oauth/atproto_oauth.dart'; 7 - import 'package:bluesky/atproto.dart' as atp; 8 - import 'package:crypto/crypto.dart'; 7 + import 'package:bluesky/bluesky.dart'; 9 8 import 'package:drift/drift.dart'; 9 + import 'package:lazurite/core/database/app_database.dart'; 10 10 import 'package:lazurite/features/auth/data/models/auth_models.dart'; 11 11 import 'package:url_launcher/url_launcher.dart'; 12 12 13 - import '../../../../core/database/app_database.dart'; 14 - 15 13 class AuthRepository { 16 14 AuthRepository({required AppDatabase database}) : _database = database; 17 - static const String _clientId = 'https://lazurite.stormlightlabs.org/client-metadata.json'; 15 + 16 + static const String kClientId = 'https://lazurite.stormlightlabs.org/client-metadata.json'; 17 + static const String _fallbackService = 'bsky.social'; 18 18 19 19 final AppDatabase _database; 20 + 20 21 HttpServer? _callbackServer; 21 22 Completer<AuthTokens?>? _oauthCompleter; 23 + OAuthClient? _pendingOAuthClient; 24 + OAuthContext? _pendingOAuthContext; 25 + Uri? _pendingRedirectUri; 26 + String? _pendingHandle; 27 + String? _pendingService; 22 28 23 29 Future<AuthTokens?> getStoredSession() async { 24 30 final account = await _database.getActiveAccount(); 25 - if (account == null) return null; 31 + if (account == null) { 32 + return null; 33 + } 26 34 27 35 return AuthTokens( 28 36 accessToken: account.accessToken, ··· 31 39 did: account.did, 32 40 handle: account.handle, 33 41 displayName: account.displayName, 42 + service: account.service, 43 + dpopNonce: account.dpopNonce, 44 + dpopPublicKey: account.dpopPublicKey, 45 + dpopPrivateKey: account.dpopPrivateKey, 46 + authMethod: account.dpopPrivateKey != null && account.dpopPublicKey != null 47 + ? AuthMethod.oauth 48 + : AuthMethod.appPassword, 34 49 ); 35 50 } 36 51 52 + Future<AuthTokens?> restoreSession() async { 53 + final storedSession = await getStoredSession(); 54 + if (storedSession == null) { 55 + return null; 56 + } 57 + 58 + if (!storedSession.isExpired) { 59 + return storedSession; 60 + } 61 + 62 + if (storedSession.refreshToken == null) { 63 + await clearSession(); 64 + return null; 65 + } 66 + 67 + try { 68 + return await refreshSession(storedSession); 69 + } catch (_) { 70 + await clearSession(); 71 + return null; 72 + } 73 + } 74 + 37 75 Future<void> saveSession(AuthTokens tokens) async { 38 76 await _database.insertAccount( 39 77 AccountsCompanion( 40 78 did: Value(tokens.did), 41 79 handle: Value(tokens.handle), 42 80 displayName: tokens.displayName != null ? Value(tokens.displayName) : const Value.absent(), 81 + service: tokens.service != null ? Value(tokens.service) : const Value.absent(), 43 82 accessToken: Value(tokens.accessToken), 44 83 refreshToken: tokens.refreshToken != null ? Value(tokens.refreshToken) : const Value.absent(), 45 - expiresAt: tokens.expiresAt != null ? Value(tokens.expiresAt!) : const Value.absent(), 84 + dpopPublicKey: tokens.dpopPublicKey != null ? Value(tokens.dpopPublicKey) : const Value.absent(), 85 + dpopPrivateKey: tokens.dpopPrivateKey != null ? Value(tokens.dpopPrivateKey) : const Value.absent(), 86 + dpopNonce: tokens.dpopNonce != null ? Value(tokens.dpopNonce) : const Value.absent(), 87 + expiresAt: tokens.expiresAt != null ? Value(tokens.expiresAt) : const Value.absent(), 88 + updatedAt: Value(DateTime.now()), 46 89 ), 47 90 ); 48 91 } ··· 54 97 Future<AuthTokens?> loginWithOAuth(String handle) async { 55 98 try { 56 99 _oauthCompleter = Completer<AuthTokens?>(); 100 + _pendingHandle = handle.trim(); 101 + _pendingService = await _resolveServiceForIdentifier(_pendingHandle!); 57 102 58 - await _startCallbackServer(); 103 + final metadata = await getClientMetadata(kClientId); 104 + final redirectUri = Uri.parse(metadata.redirectUris.first); 105 + final oauthClient = OAuthClient(metadata, service: _pendingService!); 106 + final (authorizationUrl, context) = await oauthClient.authorize(_pendingHandle); 59 107 60 - final metadata = await getClientMetadata(_clientId); 61 - final oauthClient = OAuthClient(metadata); 108 + _pendingOAuthClient = oauthClient; 109 + _pendingOAuthContext = context; 110 + _pendingRedirectUri = redirectUri; 62 111 63 - final result = await oauthClient.authorize(handle); 64 - final authorizationUrl = result.$1; 65 - 112 + await _startCallbackServer(redirectUri); 66 113 await _launchUrl(authorizationUrl); 67 114 68 - final tokens = await _oauthCompleter!.future; 69 - return tokens; 70 - } catch (e) { 115 + return await _oauthCompleter!.future; 116 + } catch (error) { 71 117 await _stopCallbackServer(); 72 - rethrow; 118 + _resetPendingOAuthState(); 119 + throw Exception('Failed to login with OAuth: $error'); 73 120 } 74 121 } 75 122 76 123 Future<AuthTokens?> loginWithAppPassword(String handle, String appPassword) async { 77 124 try { 78 - final session = await atp.createSession(identifier: handle, password: appPassword); 125 + final service = await _resolveServiceForIdentifier(handle); 126 + final session = await atp.createSession(identifier: handle, password: appPassword, service: service); 79 127 80 128 final tokens = AuthTokens( 81 129 accessToken: session.data.accessJwt, 82 130 refreshToken: session.data.refreshJwt, 131 + expiresAt: session.data.accessTokenJwt.exp, 83 132 did: session.data.did, 84 133 handle: session.data.handle, 85 134 displayName: null, 135 + service: service, 136 + authMethod: AuthMethod.appPassword, 86 137 ); 87 138 88 139 await saveSession(tokens); 89 140 return tokens; 90 - } catch (e) { 91 - throw Exception('Failed to login with app password: $e'); 141 + } catch (error) { 142 + throw Exception('Failed to login with app password: $error'); 92 143 } 93 144 } 94 145 95 - Future<AuthTokens?> refreshSession(String refreshToken) async { 146 + Future<AuthTokens?> refreshSession(AuthTokens currentSession) async { 147 + if (currentSession.refreshToken == null) { 148 + throw Exception('No refresh token available for session refresh'); 149 + } 150 + 151 + if (currentSession.usesOAuth) { 152 + final publicKey = currentSession.dpopPublicKey; 153 + final privateKey = currentSession.dpopPrivateKey; 154 + if (publicKey == null || privateKey == null) { 155 + throw Exception('Stored OAuth session is missing DPoP keys'); 156 + } 157 + 158 + try { 159 + final metadata = await getClientMetadata(kClientId); 160 + final oauthClient = OAuthClient(metadata, service: currentSession.service ?? _fallbackService); 161 + final restoredSession = atcore.restoreOAuthSession( 162 + accessToken: currentSession.accessToken, 163 + refreshToken: currentSession.refreshToken!, 164 + dPoPNonce: currentSession.dpopNonce, 165 + publicKey: publicKey, 166 + privateKey: privateKey, 167 + ); 168 + 169 + final refreshedSession = await oauthClient.refresh(restoredSession); 170 + final refreshedTokens = await _buildOAuthTokens( 171 + refreshedSession, 172 + fallbackHandle: currentSession.handle, 173 + service: currentSession.service ?? _fallbackService, 174 + ); 175 + 176 + await saveSession(refreshedTokens); 177 + return refreshedTokens; 178 + } catch (error) { 179 + await clearSession(); 180 + throw Exception('Failed to refresh OAuth session: $error'); 181 + } 182 + } 183 + 96 184 try { 97 - final refreshed = await atp.refreshSession(refreshJwt: refreshToken); 185 + final refreshed = await atp.refreshSession( 186 + refreshJwt: currentSession.refreshToken!, 187 + service: currentSession.service, 188 + ); 98 189 99 190 final tokens = AuthTokens( 100 191 accessToken: refreshed.data.accessJwt, 101 192 refreshToken: refreshed.data.refreshJwt, 193 + expiresAt: refreshed.data.accessTokenJwt.exp, 102 194 did: refreshed.data.did, 103 195 handle: refreshed.data.handle, 104 - displayName: null, 196 + displayName: currentSession.displayName, 197 + service: currentSession.service, 198 + authMethod: AuthMethod.appPassword, 105 199 ); 106 200 107 201 await saveSession(tokens); 108 202 return tokens; 109 - } catch (e) { 203 + } catch (error) { 110 204 await clearSession(); 111 - throw Exception('Failed to refresh session: $e'); 205 + throw Exception('Failed to refresh session: $error'); 112 206 } 113 207 } 114 208 115 209 Future<void> logout() async { 116 - await clearSession(); 210 + final storedSession = await getStoredSession(); 211 + 212 + try { 213 + if (storedSession?.refreshToken != null && storedSession?.usesOAuth == false) { 214 + await atp.deleteSession(refreshJwt: storedSession!.refreshToken!, service: storedSession.service); 215 + } 216 + } finally { 217 + await clearSession(); 218 + } 117 219 } 118 220 119 - Future<void> _startCallbackServer() async { 120 - _callbackServer = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); 221 + Future<void> _startCallbackServer(Uri redirectUri) async { 222 + final requestedPort = redirectUri.hasPort ? redirectUri.port : 80; 121 223 122 - _callbackServer!.listen((request) async { 123 - final uri = request.requestedUri; 224 + _callbackServer = await HttpServer.bind(InternetAddress.loopbackIPv4, requestedPort); 225 + 226 + unawaited( 227 + _callbackServer!.forEach((request) async { 228 + final uri = request.requestedUri; 124 229 125 - if (uri.path == '/callback') { 126 - final code = uri.queryParameters['code']; 127 - final error = uri.queryParameters['error']; 230 + if (uri.path != redirectUri.path) { 231 + request.response.statusCode = HttpStatus.notFound; 232 + await request.response.close(); 233 + return; 234 + } 128 235 129 236 request.response 130 - ..statusCode = 200 237 + ..statusCode = HttpStatus.ok 131 238 ..headers.contentType = ContentType.html 132 - ..write(''' 133 - <!DOCTYPE html> 134 - <html> 135 - <head><title>Authentication Complete</title></head> 136 - <body> 137 - <h1>Authentication Complete</h1> 138 - <p>You can close this window and return to the app.</p> 139 - </body> 140 - </html> 141 - '''); 142 - 239 + ..write(_callbackPageHtml); 143 240 await request.response.close(); 241 + 242 + final callbackUrl = uri.replace( 243 + scheme: redirectUri.scheme, 244 + host: redirectUri.host, 245 + port: _pendingRedirectUri?.hasPort == true ? _pendingRedirectUri!.port : null, 246 + ); 247 + 144 248 await _stopCallbackServer(); 145 249 146 - if (error != null) { 147 - _oauthCompleter?.completeError(Exception('OAuth error: $error')); 148 - } else if (code != null) { 149 - try { 150 - final tokens = await _exchangeCodeForTokens(code); 151 - await saveSession(tokens); 152 - _oauthCompleter?.complete(tokens); 153 - } catch (e) { 154 - _oauthCompleter?.completeError(e); 155 - } 156 - } else { 157 - _oauthCompleter?.completeError(Exception('No authorization code received')); 250 + try { 251 + final tokens = await _handleOAuthCallback(callbackUrl.toString()); 252 + _oauthCompleter?.complete(tokens); 253 + } catch (error) { 254 + _oauthCompleter?.completeError(error); 255 + } finally { 256 + _resetPendingOAuthState(); 158 257 } 159 - } else { 160 - request.response.statusCode = 404; 161 - await request.response.close(); 162 - } 163 - }); 258 + }), 259 + ); 260 + } 261 + 262 + Future<AuthTokens> _handleOAuthCallback(String callbackUrl) async { 263 + final oauthClient = _pendingOAuthClient; 264 + final oauthContext = _pendingOAuthContext; 265 + final fallbackHandle = _pendingHandle; 266 + final service = _pendingService; 267 + 268 + if (oauthClient == null || oauthContext == null || fallbackHandle == null || service == null) { 269 + throw StateError('OAuth callback received without an active auth flow'); 270 + } 271 + 272 + final oauthSession = await oauthClient.callback(callbackUrl, oauthContext); 273 + final tokens = await _buildOAuthTokens(oauthSession, fallbackHandle: fallbackHandle, service: service); 274 + await saveSession(tokens); 275 + return tokens; 276 + } 277 + 278 + Future<AuthTokens> _buildOAuthTokens( 279 + OAuthSession session, { 280 + required String fallbackHandle, 281 + required String service, 282 + }) async { 283 + var resolvedHandle = fallbackHandle; 284 + String? displayName; 285 + 286 + try { 287 + final authSession = await atp.ATProto.fromOAuthSession(session, service: service).server.getSession(); 288 + resolvedHandle = authSession.data.handle; 289 + } catch (_) { 290 + // Fall back to the login hint if the server session lookup fails. 291 + } 292 + 293 + try { 294 + final profile = await Bluesky.fromOAuthSession(session, service: service).actor.getProfile(actor: session.sub); 295 + displayName = profile.data.displayName; 296 + } catch (_) { 297 + // Display name is optional and should not block session persistence. 298 + } 299 + 300 + return AuthTokens( 301 + accessToken: session.accessToken, 302 + refreshToken: session.refreshToken, 303 + expiresAt: session.expiresAt, 304 + did: session.sub, 305 + handle: resolvedHandle, 306 + displayName: displayName, 307 + service: service, 308 + dpopNonce: session.$dPoPNonce, 309 + dpopPublicKey: session.$publicKey, 310 + dpopPrivateKey: session.$privateKey, 311 + authMethod: AuthMethod.oauth, 312 + ); 164 313 } 165 314 166 315 Future<void> _stopCallbackServer() async { 167 - await _callbackServer?.close(); 316 + await _callbackServer?.close(force: true); 168 317 _callbackServer = null; 169 318 } 170 319 171 - Future<AuthTokens> _exchangeCodeForTokens(String code) async { 172 - throw UnimplementedError('OAuth token exchange not yet implemented'); 320 + Future<String> _resolveServiceForIdentifier(String identifier) async { 321 + final client = atp.ATProto.anonymous(service: _fallbackService); 322 + 323 + final did = identifier.startsWith('did:') 324 + ? identifier 325 + : (await client.identity.resolveHandle(handle: identifier)).data.did; 326 + 327 + final didDoc = (await client.identity.resolveDid(did: did)).data.didDoc; 328 + return _extractServiceEndpoint(didDoc) ?? _fallbackService; 329 + } 330 + 331 + String? _extractServiceEndpoint(Map<String, dynamic> didDoc) { 332 + final services = didDoc['service']; 333 + if (services is! List) { 334 + return null; 335 + } 336 + 337 + for (final service in services) { 338 + if (service is! Map<String, dynamic>) { 339 + continue; 340 + } 341 + 342 + if (service['id'] == '#atproto_pds' && 343 + service['type'] == 'AtprotoPersonalDataServer' && 344 + service['serviceEndpoint'] is String) { 345 + final endpoint = Uri.tryParse(service['serviceEndpoint'] as String); 346 + if (endpoint != null && endpoint.host.isNotEmpty) { 347 + return endpoint.host; 348 + } 349 + } 350 + } 351 + 352 + return null; 173 353 } 174 354 175 355 Future<void> _launchUrl(Uri url) async { ··· 178 358 } 179 359 } 180 360 361 + void _resetPendingOAuthState() { 362 + _pendingOAuthClient = null; 363 + _pendingOAuthContext = null; 364 + _pendingRedirectUri = null; 365 + _pendingHandle = null; 366 + _pendingService = null; 367 + } 368 + 181 369 int get callbackPort => _callbackServer?.port ?? 0; 182 370 } 183 371 184 - String generateCodeVerifier() { 185 - final random = Random.secure(); 186 - final values = List<int>.generate(32, (_) => random.nextInt(256)); 187 - return base64Url.encode(values).replaceAll('=', ''); 188 - } 372 + const String _callbackPageHtml = ''' 373 + <!DOCTYPE html> 374 + <html lang="en"> 375 + <head> 376 + <meta charset="UTF-8" /> 377 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 378 + <title>Lazurite Authentication Complete</title> 379 + <style> 380 + body { 381 + align-items: center; 382 + background: #161616; 383 + color: #f2f4f8; 384 + display: flex; 385 + font-family: system-ui, sans-serif; 386 + justify-content: center; 387 + margin: 0; 388 + min-height: 100vh; 389 + padding: 24px; 390 + text-align: center; 391 + } 189 392 190 - String generateCodeChallenge(String codeVerifier) { 191 - final bytes = utf8.encode(codeVerifier); 192 - final digest = sha256.convert(bytes); 193 - return base64Url.encode(digest.bytes).replaceAll('=', ''); 194 - } 393 + main { 394 + max-width: 420px; 395 + } 396 + 397 + h1 { 398 + font-size: 28px; 399 + margin-bottom: 12px; 400 + } 401 + 402 + p { 403 + color: #dde1e6; 404 + line-height: 1.5; 405 + margin: 0; 406 + } 407 + </style> 408 + </head> 409 + <body> 410 + <main> 411 + <h1>Authentication Complete</h1> 412 + <p>You can close this window and return to Lazurite.</p> 413 + </main> 414 + </body> 415 + </html> 416 + ''';
+37 -1
lib/features/auth/data/models/auth_models.dart
··· 1 1 import 'package:equatable/equatable.dart'; 2 2 3 + enum AuthMethod { appPassword, oauth } 4 + 3 5 class AuthTokens extends Equatable { 4 6 const AuthTokens({ 5 7 required this.accessToken, ··· 8 10 required this.did, 9 11 required this.handle, 10 12 this.displayName, 13 + this.service, 14 + this.dpopNonce, 15 + this.dpopPublicKey, 16 + this.dpopPrivateKey, 17 + this.authMethod = AuthMethod.appPassword, 11 18 }); 12 19 final String accessToken; 13 20 final String? refreshToken; ··· 15 22 final String did; 16 23 final String handle; 17 24 final String? displayName; 25 + final String? service; 26 + final String? dpopNonce; 27 + final String? dpopPublicKey; 28 + final String? dpopPrivateKey; 29 + final AuthMethod authMethod; 18 30 19 31 AuthTokens copyWith({ 20 32 String? accessToken, ··· 23 35 String? did, 24 36 String? handle, 25 37 String? displayName, 38 + String? service, 39 + String? dpopNonce, 40 + String? dpopPublicKey, 41 + String? dpopPrivateKey, 42 + AuthMethod? authMethod, 26 43 }) { 27 44 return AuthTokens( 28 45 accessToken: accessToken ?? this.accessToken, ··· 31 48 did: did ?? this.did, 32 49 handle: handle ?? this.handle, 33 50 displayName: displayName ?? this.displayName, 51 + service: service ?? this.service, 52 + dpopNonce: dpopNonce ?? this.dpopNonce, 53 + dpopPublicKey: dpopPublicKey ?? this.dpopPublicKey, 54 + dpopPrivateKey: dpopPrivateKey ?? this.dpopPrivateKey, 55 + authMethod: authMethod ?? this.authMethod, 34 56 ); 35 57 } 58 + 59 + bool get usesOAuth => authMethod == AuthMethod.oauth; 36 60 37 61 bool get isExpired { 38 62 if (expiresAt == null) return false; ··· 40 64 } 41 65 42 66 @override 43 - List<Object?> get props => [accessToken, refreshToken, expiresAt, did, handle, displayName]; 67 + List<Object?> get props => [ 68 + accessToken, 69 + refreshToken, 70 + expiresAt, 71 + did, 72 + handle, 73 + displayName, 74 + service, 75 + dpopNonce, 76 + dpopPublicKey, 77 + dpopPrivateKey, 78 + authMethod, 79 + ]; 44 80 } 45 81 46 82 class User extends Equatable {
+21 -12
lib/features/auth/presentation/home_screen.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 3 import 'package:go_router/go_router.dart'; 4 - 5 - import '../../../features/auth/bloc/auth_bloc.dart'; 4 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 6 5 7 6 class HomeScreen extends StatelessWidget { 8 7 const HomeScreen({super.key}); ··· 13 12 appBar: AppBar( 14 13 title: const Text('Lazurite'), 15 14 actions: [ 16 - IconButton(icon: const Icon(Icons.settings), onPressed: () => context.push('/settings')), 15 + IconButton(icon: const Icon(Icons.person_outline), onPressed: () => context.push('/profile')), 16 + IconButton(icon: const Icon(Icons.settings_outlined), onPressed: () => context.push('/settings')), 17 17 IconButton( 18 18 icon: const Icon(Icons.logout), 19 19 onPressed: () { ··· 24 24 ), 25 25 body: BlocBuilder<AuthBloc, AuthState>( 26 26 builder: (context, state) { 27 - if (!state.isAuthenticated || state.tokens == null) { 27 + final tokens = state.tokens; 28 + if (!state.isAuthenticated || tokens == null) { 28 29 return const Center(child: Text('Not authenticated')); 29 30 } 30 31 31 32 return Center( 32 - child: Column( 33 - mainAxisAlignment: MainAxisAlignment.center, 34 - children: [ 35 - Text('Welcome!', style: Theme.of(context).textTheme.headlineMedium), 36 - const SizedBox(height: 16), 37 - const Text('Handle: @\${state.tokens!.handle}'), 38 - const Text('DID: \${state.tokens!.did}'), 39 - ], 33 + child: Padding( 34 + padding: const EdgeInsets.all(24), 35 + child: Column( 36 + mainAxisAlignment: MainAxisAlignment.center, 37 + children: [ 38 + Text( 39 + tokens.displayName ?? 'Welcome', 40 + style: Theme.of(context).textTheme.headlineMedium, 41 + textAlign: TextAlign.center, 42 + ), 43 + const SizedBox(height: 12), 44 + Text('@${tokens.handle}'), 45 + const SizedBox(height: 8), 46 + SelectableText(tokens.did, textAlign: TextAlign.center), 47 + ], 48 + ), 40 49 ), 41 50 ); 42 51 },
+194 -120
lib/features/auth/presentation/login_screen.dart
··· 1 1 import 'package:flutter/foundation.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 - 5 - import '../../../features/auth/bloc/auth_bloc.dart'; 4 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 6 5 7 6 class LoginScreen extends StatefulWidget { 8 7 const LoginScreen({super.key}); ··· 25 24 } 26 25 27 26 void _onOAuthLogin() { 28 - if (_formKey.currentState?.validate() ?? false) { 29 - final handle = _handleController.text.trim(); 30 - context.read<AuthBloc>().add(OAuthLoginRequested(handle: handle)); 27 + if (!_isHandleValid()) { 28 + return; 31 29 } 30 + 31 + context.read<AuthBloc>().add(OAuthLoginRequested(handle: _handleController.text.trim())); 32 32 } 33 33 34 34 void _onAppPasswordLogin() { 35 - if (_formKey.currentState?.validate() ?? false) { 36 - final handle = _handleController.text.trim(); 37 - final appPassword = _appPasswordController.text.trim(); 38 - context.read<AuthBloc>().add(LoginRequested(handle: handle, appPassword: appPassword)); 35 + if (!(_formKey.currentState?.validate() ?? false)) { 36 + return; 39 37 } 38 + 39 + context.read<AuthBloc>().add( 40 + LoginRequested(handle: _handleController.text.trim(), appPassword: _appPasswordController.text.trim()), 41 + ); 42 + } 43 + 44 + bool _isHandleValid() { 45 + return _formKey.currentState?.validate() ?? false; 40 46 } 41 47 42 48 @override 43 49 Widget build(BuildContext context) { 50 + final theme = Theme.of(context); 51 + final colorScheme = theme.colorScheme; 52 + 44 53 return Scaffold( 45 - body: BlocListener<AuthBloc, AuthState>( 46 - listener: (context, state) { 47 - if (state.hasError && state.errorMessage != null) { 48 - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.errorMessage!))); 49 - } 50 - }, 51 - child: Center( 52 - child: SingleChildScrollView( 53 - padding: const EdgeInsets.all(24.0), 54 - child: Form( 55 - key: _formKey, 56 - child: Column( 57 - mainAxisAlignment: MainAxisAlignment.center, 58 - crossAxisAlignment: CrossAxisAlignment.stretch, 59 - children: [ 60 - const Icon(Icons.cloud, size: 80, color: Colors.blue), 61 - const SizedBox(height: 24), 62 - Text('Lazurite', style: Theme.of(context).textTheme.headlineLarge, textAlign: TextAlign.center), 63 - const SizedBox(height: 8), 64 - Text('Sign in to BlueSky', style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center), 65 - const SizedBox(height: 32), 66 - TextFormField( 67 - controller: _handleController, 68 - decoration: const InputDecoration( 69 - labelText: 'Handle', 70 - hintText: 'your-handle.bsky.social', 71 - prefixIcon: Icon(Icons.person), 72 - border: OutlineInputBorder(), 73 - ), 74 - validator: (value) { 75 - if (value == null || value.isEmpty) { 76 - return 'Please enter your handle'; 77 - } 78 - return null; 79 - }, 80 - textInputAction: TextInputAction.next, 81 - autocorrect: false, 82 - ), 83 - const SizedBox(height: 24), 84 - BlocBuilder<AuthBloc, AuthState>( 85 - builder: (context, state) { 86 - return ElevatedButton.icon( 87 - onPressed: state.isLoading ? null : _onOAuthLogin, 88 - icon: state.isLoading 89 - ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) 90 - : const Icon(Icons.login), 91 - label: Text(state.isLoading ? 'Signing in...' : 'Sign in with BlueSky'), 92 - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), 93 - ); 94 - }, 95 - ), 96 - if (kDebugMode) ...[ 97 - const SizedBox(height: 16), 98 - TextButton( 99 - onPressed: () { 100 - setState(() { 101 - _showDebugForm = !_showDebugForm; 102 - }); 103 - }, 104 - child: Text(_showDebugForm ? 'Hide Debug Login' : 'Show Debug Login (App Password)'), 105 - ), 106 - if (_showDebugForm) ...[ 107 - const SizedBox(height: 16), 108 - const Divider(), 109 - const SizedBox(height: 16), 110 - Text( 111 - 'Debug Login (App Password)', 112 - style: Theme.of(context).textTheme.titleMedium, 113 - textAlign: TextAlign.center, 114 - ), 115 - const SizedBox(height: 16), 116 - TextFormField( 117 - controller: _appPasswordController, 118 - decoration: const InputDecoration( 119 - labelText: 'App Password', 120 - hintText: 'xxxx-xxxx-xxxx-xxxx', 121 - prefixIcon: Icon(Icons.lock), 122 - border: OutlineInputBorder(), 54 + body: DecoratedBox( 55 + decoration: BoxDecoration( 56 + gradient: LinearGradient( 57 + begin: Alignment.topCenter, 58 + end: Alignment.bottomCenter, 59 + colors: [colorScheme.surface, colorScheme.surfaceContainerLowest], 60 + ), 61 + ), 62 + child: SafeArea( 63 + child: BlocListener<AuthBloc, AuthState>( 64 + listener: (context, state) { 65 + if (state.hasError && state.errorMessage != null) { 66 + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.errorMessage!))); 67 + } 68 + }, 69 + child: Center( 70 + child: SingleChildScrollView( 71 + padding: const EdgeInsets.all(24), 72 + child: ConstrainedBox( 73 + constraints: const BoxConstraints(maxWidth: 440), 74 + child: Form( 75 + key: _formKey, 76 + child: Column( 77 + crossAxisAlignment: CrossAxisAlignment.stretch, 78 + children: [ 79 + const SizedBox(height: 16), 80 + _LogoCard(colorScheme: colorScheme), 81 + const SizedBox(height: 24), 82 + Text( 83 + 'Lazurite', 84 + textAlign: TextAlign.center, 85 + style: theme.textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w700), 123 86 ), 124 - obscureText: true, 125 - validator: (value) { 126 - if (_showDebugForm && (value == null || value.isEmpty)) { 127 - return 'Please enter your app password'; 128 - } 129 - return null; 130 - }, 131 - textInputAction: TextInputAction.done, 132 - onFieldSubmitted: (_) => _onAppPasswordLogin(), 133 - ), 134 - const SizedBox(height: 16), 135 - BlocBuilder<AuthBloc, AuthState>( 136 - builder: (context, state) { 137 - return ElevatedButton.icon( 138 - onPressed: state.isLoading ? null : _onAppPasswordLogin, 139 - icon: state.isLoading 140 - ? const SizedBox( 141 - width: 20, 142 - height: 20, 143 - child: CircularProgressIndicator(strokeWidth: 2), 144 - ) 145 - : const Icon(Icons.bug_report), 146 - label: Text(state.isLoading ? 'Signing in...' : 'Debug Sign In'), 147 - style: ElevatedButton.styleFrom( 148 - backgroundColor: Colors.orange, 149 - foregroundColor: Colors.white, 150 - padding: const EdgeInsets.symmetric(vertical: 16), 87 + const SizedBox(height: 8), 88 + Text( 89 + 'Connect with BlueSky', 90 + textAlign: TextAlign.center, 91 + style: theme.textTheme.bodyLarge?.copyWith(color: colorScheme.onSurfaceVariant), 92 + ), 93 + const SizedBox(height: 32), 94 + TextFormField( 95 + controller: _handleController, 96 + decoration: const InputDecoration( 97 + labelText: 'Handle or DID', 98 + hintText: 'username.bsky.social or did:plc:...', 99 + prefixIcon: Icon(Icons.person_outline), 100 + border: OutlineInputBorder(), 101 + ), 102 + textInputAction: TextInputAction.next, 103 + autocorrect: false, 104 + validator: (value) { 105 + if (value == null || value.trim().isEmpty) { 106 + return 'Enter your BlueSky handle or DID'; 107 + } 108 + return null; 109 + }, 110 + ), 111 + const SizedBox(height: 20), 112 + BlocBuilder<AuthBloc, AuthState>( 113 + builder: (context, state) { 114 + return FilledButton.icon( 115 + onPressed: state.isLoading ? null : _onOAuthLogin, 116 + icon: state.isLoading 117 + ? const SizedBox( 118 + width: 18, 119 + height: 18, 120 + child: CircularProgressIndicator(strokeWidth: 2), 121 + ) 122 + : const Icon(Icons.language), 123 + label: Text(state.isLoading ? 'Starting sign in...' : 'Continue with BlueSky OAuth'), 124 + style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 18)), 125 + ); 126 + }, 127 + ), 128 + if (kDebugMode) ...[ 129 + const SizedBox(height: 24), 130 + Row( 131 + children: [ 132 + const Expanded(child: Divider()), 133 + Padding( 134 + padding: const EdgeInsets.symmetric(horizontal: 12), 135 + child: Text('Debug', style: theme.textTheme.labelMedium), 136 + ), 137 + const Expanded(child: Divider()), 138 + ], 139 + ), 140 + const SizedBox(height: 16), 141 + Card.outlined( 142 + child: Padding( 143 + padding: const EdgeInsets.all(16), 144 + child: Column( 145 + crossAxisAlignment: CrossAxisAlignment.stretch, 146 + children: [ 147 + Row( 148 + children: [ 149 + const Icon(Icons.bug_report_outlined), 150 + const SizedBox(width: 8), 151 + Text('App Password Login', style: theme.textTheme.titleMedium), 152 + const Spacer(), 153 + TextButton( 154 + onPressed: () { 155 + setState(() { 156 + _showDebugForm = !_showDebugForm; 157 + }); 158 + }, 159 + child: Text(_showDebugForm ? 'Hide' : 'Show'), 160 + ), 161 + ], 162 + ), 163 + if (_showDebugForm) ...[ 164 + const SizedBox(height: 12), 165 + TextFormField( 166 + controller: _appPasswordController, 167 + decoration: const InputDecoration( 168 + labelText: 'App Password', 169 + hintText: 'xxxx-xxxx-xxxx-xxxx', 170 + prefixIcon: Icon(Icons.lock_outline), 171 + border: OutlineInputBorder(), 172 + ), 173 + obscureText: true, 174 + validator: (value) { 175 + if (_showDebugForm && (value == null || value.trim().isEmpty)) { 176 + return 'Enter your app password'; 177 + } 178 + return null; 179 + }, 180 + onFieldSubmitted: (_) => _onAppPasswordLogin(), 181 + ), 182 + const SizedBox(height: 12), 183 + BlocBuilder<AuthBloc, AuthState>( 184 + builder: (context, state) { 185 + return OutlinedButton.icon( 186 + onPressed: state.isLoading ? null : _onAppPasswordLogin, 187 + icon: const Icon(Icons.login), 188 + label: const Text('Sign In'), 189 + ); 190 + }, 191 + ), 192 + const SizedBox(height: 8), 193 + Text( 194 + 'Generate app passwords from BlueSky Settings -> App Passwords.', 195 + style: theme.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 196 + textAlign: TextAlign.center, 197 + ), 198 + ], 199 + ], 200 + ), 151 201 ), 152 - ); 153 - }, 154 - ), 155 - ], 156 - ], 157 - ], 202 + ), 203 + ], 204 + ], 205 + ), 206 + ), 207 + ), 158 208 ), 159 209 ), 160 210 ), ··· 163 213 ); 164 214 } 165 215 } 216 + 217 + class _LogoCard extends StatelessWidget { 218 + const _LogoCard({required this.colorScheme}); 219 + 220 + final ColorScheme colorScheme; 221 + 222 + @override 223 + Widget build(BuildContext context) { 224 + return Center( 225 + child: Container( 226 + width: 88, 227 + height: 88, 228 + decoration: BoxDecoration( 229 + borderRadius: BorderRadius.circular(24), 230 + gradient: LinearGradient(colors: [colorScheme.primary, colorScheme.secondary]), 231 + boxShadow: [ 232 + BoxShadow(color: colorScheme.primary.withValues(alpha: 0.24), blurRadius: 28, offset: const Offset(0, 12)), 233 + ], 234 + ), 235 + child: const Icon(Icons.layers_outlined, color: Colors.white, size: 42), 236 + ), 237 + ); 238 + } 239 + }
+37
lib/features/profile/presentation/profile_screen.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:flutter_bloc/flutter_bloc.dart'; 3 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 4 + 5 + class ProfileScreen extends StatelessWidget { 6 + const ProfileScreen({super.key}); 7 + 8 + @override 9 + Widget build(BuildContext context) { 10 + final state = context.watch<AuthBloc>().state; 11 + final tokens = state.tokens; 12 + 13 + return Scaffold( 14 + appBar: AppBar(title: const Text('Profile')), 15 + body: tokens == null 16 + ? const Center(child: Text('No active account')) 17 + : Padding( 18 + padding: const EdgeInsets.all(24), 19 + child: Column( 20 + crossAxisAlignment: CrossAxisAlignment.start, 21 + children: [ 22 + Text(tokens.displayName ?? tokens.handle, style: Theme.of(context).textTheme.headlineMedium), 23 + const SizedBox(height: 8), 24 + Text('@${tokens.handle}', style: Theme.of(context).textTheme.titleMedium), 25 + const SizedBox(height: 12), 26 + SelectableText(tokens.did, style: Theme.of(context).textTheme.bodyMedium), 27 + const SizedBox(height: 24), 28 + Text( 29 + 'Phase 1 milestone 0 requires the profile route and feature structure. Full profile rendering is scheduled in milestone 2.', 30 + style: Theme.of(context).textTheme.bodyMedium, 31 + ), 32 + ], 33 + ), 34 + ), 35 + ); 36 + } 37 + }
+12 -9
lib/features/settings/presentation/settings_screen.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 3 import 'package:go_router/go_router.dart'; 4 - 5 - import '../../../features/auth/bloc/auth_bloc.dart'; 4 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 6 5 7 6 class SettingsScreen extends StatelessWidget { 8 7 const SettingsScreen({super.key}); ··· 15 14 children: [ 16 15 BlocBuilder<AuthBloc, AuthState>( 17 16 builder: (context, state) { 18 - if (state.isAuthenticated && state.tokens != null) { 19 - return ListTile( 20 - leading: const Icon(Icons.person), 21 - title: const Text('@\${state.tokens!.handle}'), 22 - subtitle: Text(state.tokens!.displayName ?? 'No display name'), 23 - ); 17 + final tokens = state.tokens; 18 + if (!state.isAuthenticated || tokens == null) { 19 + return const SizedBox.shrink(); 24 20 } 25 - return const SizedBox.shrink(); 21 + 22 + return ListTile( 23 + leading: const CircleAvatar(child: Icon(Icons.person)), 24 + title: Text(tokens.displayName ?? tokens.handle), 25 + subtitle: Text('@${tokens.handle}'), 26 + trailing: const Icon(Icons.chevron_right), 27 + onTap: () => context.push('/profile'), 28 + ); 26 29 }, 27 30 ), 28 31 const Divider(),
+13 -9
lib/main.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 + import 'package:lazurite/core/database/app_database.dart'; 4 + import 'package:lazurite/core/router/app_router.dart'; 5 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 6 + import 'package:lazurite/features/auth/data/auth_repository.dart'; 3 7 4 - import 'core/database/app_database.dart'; 5 - import 'core/router/app_router.dart'; 6 - import 'features/auth/bloc/auth_bloc.dart'; 7 - import 'features/auth/data/auth_repository.dart'; 8 - 9 - void main() async { 8 + Future<void> main() async { 10 9 WidgetsFlutterBinding.ensureInitialized(); 11 10 12 11 final database = AppDatabase(); 13 12 final authRepository = AuthRepository(database: database); 14 - final authBloc = AuthBloc(authRepository: authRepository); 15 - 16 - authBloc.add(const CheckSessionRequested()); 13 + final restoredSession = await authRepository.restoreSession(); 14 + final authBloc = AuthBloc( 15 + authRepository: authRepository, 16 + initialState: restoredSession != null 17 + ? AuthState.authenticated(restoredSession) 18 + : const AuthState.unauthenticated(), 19 + ); 17 20 18 21 runApp(LazuriteApp(authBloc: authBloc)); 19 22 } 20 23 21 24 class LazuriteApp extends StatelessWidget { 22 25 const LazuriteApp({super.key, required this.authBloc}); 26 + 23 27 final AuthBloc authBloc; 24 28 25 29 @override
+2 -26
pubspec.lock
··· 42 42 source: hosted 43 43 version: "1.0.0" 44 44 atproto: 45 - dependency: transitive 45 + dependency: "direct main" 46 46 description: 47 47 name: atproto 48 48 sha256: "115944b8e4859bdaa0fe0d5162725ddf80269f14e79a33ab7ee05db972932ef9" ··· 50 50 source: hosted 51 51 version: "1.4.1" 52 52 atproto_core: 53 - dependency: transitive 53 + dependency: "direct main" 54 54 description: 55 55 name: atproto_core 56 56 sha256: "76b8c6237cf0c446c2f056cd76723eb5d7993ecaf7f7cc417b1b6bf26d8f24ce" ··· 821 821 url: "https://pub.dev" 822 822 source: hosted 823 823 version: "1.10.2" 824 - sqflite_common: 825 - dependency: transitive 826 - description: 827 - name: sqflite_common 828 - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" 829 - url: "https://pub.dev" 830 - source: hosted 831 - version: "2.5.6" 832 - sqflite_common_ffi: 833 - dependency: "direct dev" 834 - description: 835 - name: sqflite_common_ffi 836 - sha256: "8d7b8749a516cbf6e9057f9b480b716ad14fc4f3d3873ca6938919cc626d9025" 837 - url: "https://pub.dev" 838 - source: hosted 839 - version: "2.3.7+1" 840 824 sqlite3: 841 825 dependency: transitive 842 826 description: ··· 893 877 url: "https://pub.dev" 894 878 source: hosted 895 879 version: "1.4.1" 896 - synchronized: 897 - dependency: transitive 898 - description: 899 - name: synchronized 900 - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 901 - url: "https://pub.dev" 902 - source: hosted 903 - version: "3.4.0" 904 880 term_glyph: 905 881 dependency: transitive 906 882 description:
+3 -2
pubspec.yaml
··· 1 1 name: lazurite 2 - description: "A new Flutter project." 2 + description: A material BlueSky client. 3 3 publish_to: "none" 4 4 version: 1.0.0+1 5 5 ··· 10 10 flutter: 11 11 sdk: flutter 12 12 cupertino_icons: ^1.0.8 13 + atproto: ^1.4.1 14 + atproto_core: ^1.2.0 13 15 bluesky: ^1.4.1 14 16 bluesky_text: ^1.1.1 15 17 atproto_oauth: ^0.2.0 ··· 35 37 json_serializable: ^6.8.0 36 38 bloc_test: ^10.0.0 37 39 mocktail: ^1.0.4 38 - sqflite_common_ffi: ^2.3.5 39 40 40 41 flutter: 41 42 uses-material-design: true
+1
scripts/.python-version
··· 1 + 3.10
+1
scripts/README.md
··· 1 + # ATProto/BlueSky API Scripts
+6
scripts/main.py
··· 1 + def main(): 2 + print("Hello from scripts!") 3 + 4 + 5 + if __name__ == "__main__": 6 + main()
+7
scripts/pyproject.toml
··· 1 + [project] 2 + name = "scripts" 3 + version = "0.1.0" 4 + description = "Add your description here" 5 + readme = "README.md" 6 + requires-python = ">=3.10" 7 + dependencies = []
+29 -7
test/core/database/app_database_test.dart
··· 1 + import 'package:drift/native.dart'; 1 2 import 'package:flutter_test/flutter_test.dart'; 2 3 import 'package:lazurite/core/database/app_database.dart'; 3 - import 'package:sqflite_common_ffi/sqflite_ffi.dart'; 4 4 5 5 void main() { 6 6 late AppDatabase database; 7 7 8 - setUpAll(() { 9 - sqfliteFfiInit(); 10 - databaseFactory = databaseFactoryFfi; 11 - }); 12 - 13 8 setUp(() async { 14 - database = AppDatabase(); 9 + database = AppDatabase(executor: NativeDatabase.memory()); 15 10 }); 16 11 17 12 tearDown(() async { ··· 125 120 'did:plc:abc123', 126 121 accessToken: 'new_token', 127 122 refreshToken: 'new_refresh', 123 + dpopNonce: 'nonce-1', 128 124 ); 129 125 130 126 expect(updated, isTrue); ··· 132 128 final retrieved = await database.getAccount('did:plc:abc123'); 133 129 expect(retrieved!.accessToken, equals('new_token')); 134 130 expect(retrieved.refreshToken, equals('new_refresh')); 131 + expect(retrieved.dpopNonce, equals('nonce-1')); 132 + }); 133 + }); 134 + 135 + group('Cache operations', () { 136 + test('should cache a profile payload', () async { 137 + await database.cacheProfile( 138 + did: 'did:plc:abc123', 139 + handle: 'user.bsky.social', 140 + payload: '{"did":"did:plc:abc123"}', 141 + ); 142 + 143 + final cached = await database.select(database.cachedProfiles).getSingle(); 144 + expect(cached.did, equals('did:plc:abc123')); 145 + expect(cached.handle, equals('user.bsky.social')); 146 + }); 147 + 148 + test('should cache a post payload', () async { 149 + await database.cachePost( 150 + uri: 'at://did:plc:abc123/app.bsky.feed.post/123', 151 + authorDid: 'did:plc:abc123', 152 + payload: '{"uri":"at://did:plc:abc123/app.bsky.feed.post/123"}', 153 + ); 154 + 155 + final cached = await database.select(database.cachedPosts).getSingle(); 156 + expect(cached.authorDid, equals('did:plc:abc123')); 135 157 }); 136 158 }); 137 159
+5 -5
test/features/auth/bloc/auth_bloc_test.dart
··· 1 1 import 'package:bloc_test/bloc_test.dart'; 2 2 import 'package:flutter_test/flutter_test.dart'; 3 - import 'package:mocktail/mocktail.dart'; 4 3 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 5 4 import 'package:lazurite/features/auth/data/auth_repository.dart'; 6 5 import 'package:lazurite/features/auth/data/models/auth_models.dart'; 6 + import 'package:mocktail/mocktail.dart'; 7 7 8 8 class MockAuthRepository extends Mock implements AuthRepository {} 9 9 ··· 87 87 'emits [unauthenticated] when CheckSessionRequested is added and no session exists', 88 88 build: () => AuthBloc(authRepository: mockAuthRepository), 89 89 setUp: () { 90 - when(() => mockAuthRepository.getStoredSession()).thenAnswer((_) async => null); 90 + when(() => mockAuthRepository.restoreSession()).thenAnswer((_) async => null); 91 91 }, 92 92 act: (bloc) => bloc.add(const CheckSessionRequested()), 93 - expect: () => [const AuthState.unauthenticated()], 93 + expect: () => [const AuthState.authenticating(), const AuthState.unauthenticated()], 94 94 ); 95 95 96 96 blocTest<AuthBloc, AuthState>( 97 97 'emits [authenticated] when CheckSessionRequested is added and valid session exists', 98 98 build: () => AuthBloc(authRepository: mockAuthRepository), 99 99 setUp: () { 100 - when(() => mockAuthRepository.getStoredSession()).thenAnswer((_) async => tokens); 100 + when(() => mockAuthRepository.restoreSession()).thenAnswer((_) async => tokens); 101 101 }, 102 102 act: (bloc) => bloc.add(const CheckSessionRequested()), 103 - expect: () => [const AuthState.authenticated(tokens)], 103 + expect: () => [const AuthState.authenticating(), const AuthState.authenticated(tokens)], 104 104 ); 105 105 }); 106 106 }
+38 -1
test/features/auth/data/auth_repository_test.dart
··· 1 1 import 'package:flutter_test/flutter_test.dart'; 2 - import 'package:mocktail/mocktail.dart'; 3 2 import 'package:lazurite/core/database/app_database.dart'; 4 3 import 'package:lazurite/features/auth/data/auth_repository.dart'; 5 4 import 'package:lazurite/features/auth/data/models/auth_models.dart'; 5 + import 'package:mocktail/mocktail.dart'; 6 6 7 7 class MockAppDatabase extends Mock implements AppDatabase {} 8 8 ··· 36 36 final account = Account( 37 37 did: 'did:plc:abc123', 38 38 handle: 'user.bsky.social', 39 + service: 'bsky.social', 39 40 accessToken: 'access_token', 40 41 refreshToken: 'refresh_token', 42 + dpopPublicKey: null, 43 + dpopPrivateKey: null, 44 + dpopNonce: null, 41 45 displayName: 'User Name', 46 + expiresAt: null, 42 47 createdAt: DateTime.now(), 43 48 updatedAt: DateTime.now(), 44 49 ); ··· 53 58 expect(result.accessToken, equals('access_token')); 54 59 expect(result.refreshToken, equals('refresh_token')); 55 60 expect(result.displayName, equals('User Name')); 61 + expect(result.service, equals('bsky.social')); 62 + expect(result.authMethod, AuthMethod.appPassword); 56 63 }); 57 64 }); 58 65 ··· 64 71 did: 'did:plc:abc123', 65 72 handle: 'user.bsky.social', 66 73 displayName: 'User Name', 74 + service: 'bsky.social', 67 75 ); 68 76 69 77 when(() => mockDatabase.insertAccount(any())).thenAnswer((_) async => 1); ··· 74 82 }); 75 83 }); 76 84 85 + group('restoreSession', () { 86 + test('should return stored session when it is still valid', () async { 87 + final futureExpiry = DateTime.now().add(const Duration(hours: 1)); 88 + final account = Account( 89 + did: 'did:plc:abc123', 90 + handle: 'user.bsky.social', 91 + service: 'bsky.social', 92 + accessToken: 'access_token', 93 + refreshToken: 'refresh_token', 94 + dpopPublicKey: null, 95 + dpopPrivateKey: null, 96 + dpopNonce: null, 97 + displayName: 'User Name', 98 + expiresAt: futureExpiry, 99 + createdAt: DateTime.now(), 100 + updatedAt: DateTime.now(), 101 + ); 102 + 103 + when(() => mockDatabase.getActiveAccount()).thenAnswer((_) async => account); 104 + 105 + final restored = await authRepository.restoreSession(); 106 + 107 + expect(restored, isNotNull); 108 + expect(restored!.handle, equals('user.bsky.social')); 109 + }); 110 + }); 111 + 77 112 group('clearSession', () { 78 113 test('should delete all accounts', () async { 79 114 when(() => mockDatabase.deleteAllAccounts()).thenAnswer((_) async => 1); ··· 86 121 87 122 group('logout', () { 88 123 test('should clear session', () async { 124 + when(() => mockDatabase.getActiveAccount()).thenAnswer((_) async => null); 89 125 when(() => mockDatabase.deleteAllAccounts()).thenAnswer((_) async => 1); 90 126 91 127 await authRepository.logout(); 92 128 129 + verify(() => mockDatabase.getActiveAccount()).called(1); 93 130 verify(() => mockDatabase.deleteAllAccounts()).called(1); 94 131 }); 95 132 });
+16
test/features/auth/data/models/auth_models_test.dart
··· 10 10 did: 'did:plc:abc123', 11 11 handle: 'user.bsky.social', 12 12 displayName: 'User Name', 13 + service: 'bsky.social', 13 14 ); 14 15 15 16 expect(tokens.accessToken, equals('access_token')); ··· 17 18 expect(tokens.did, equals('did:plc:abc123')); 18 19 expect(tokens.handle, equals('user.bsky.social')); 19 20 expect(tokens.displayName, equals('User Name')); 21 + expect(tokens.service, equals('bsky.social')); 20 22 }); 21 23 22 24 test('should create AuthTokens without optional fields', () { ··· 37 39 expect(newTokens.accessToken, equals('new_token')); 38 40 expect(newTokens.did, equals('did:plc:abc123')); 39 41 expect(newTokens.displayName, equals('New Name')); 42 + }); 43 + 44 + test('should identify oauth-backed sessions', () { 45 + const tokens = AuthTokens( 46 + accessToken: 'access_token', 47 + refreshToken: 'refresh_token', 48 + did: 'did:plc:abc123', 49 + handle: 'user.bsky.social', 50 + dpopPublicKey: 'public', 51 + dpopPrivateKey: 'private', 52 + authMethod: AuthMethod.oauth, 53 + ); 54 + 55 + expect(tokens.usesOAuth, isTrue); 40 56 }); 41 57 42 58 test('should check if tokens are expired', () {