Skip to content

Commit 6c1d15a

Browse files
committed
store: Keep login credentials in the database
Fixes: #13 Fixes: #34
1 parent 62c332f commit 6c1d15a

File tree

3 files changed

+52
-72
lines changed

3 files changed

+52
-72
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ app.*.map.json
4343
/android/app/profile
4444
/android/app/release
4545

46-
# Scaffolding hack
46+
# Old scaffolding hack
4747
lib/credential_fixture.dart

README.md

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -39,39 +39,6 @@ community. See [issue #15][].
3939
[issue #15]: https://github.com/zulip/zulip-flutter/issues/15
4040

4141

42-
### Server credentials
43-
44-
In this early prototype, we don't yet have a UI for logging into
45-
a Zulip server. Instead, you supply Zulip credentials at build time.
46-
47-
To do this, log into the Zulip web app for the test account
48-
you want to use, and gather two kinds of information:
49-
* [Download a `.zuliprc` file][download-zuliprc].
50-
This will contain a realm URL, email, and API key.
51-
* Find the account's user ID. You can do this by visiting your
52-
DMs with yourself, and looking at the URL;
53-
it's the number after `pm-with/` or `dm-with/`.
54-
55-
Then create a file `lib/credential_fixture.dart` in this worktree
56-
with the following form, and fill in the gathered information:
57-
```dart
58-
const String realmUrl = '…';
59-
const String email = '…';
60-
const String apiKey = '…';
61-
const int userId = /* … */ -1;
62-
```
63-
64-
Now build and run the app (see "Flutter help" above), and things
65-
should work.
66-
67-
Note this means the account's API key gets incorporated into the
68-
build output. Consider using a low-value test account, or else
69-
deleting the build output (`flutter clean`, and then delete the app
70-
from any mobile devices you ran it on) when done.
71-
72-
[download-zuliprc]: https://zulip.com/api/api-keys
73-
74-
7542
### Tests
7643

7744
You can run all our forms of tests with two commands:

lib/model/store.dart

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import 'dart:convert';
2+
import 'dart:io';
23

4+
import 'package:drift/native.dart';
35
import 'package:flutter/foundation.dart';
6+
import 'package:path/path.dart' as p;
7+
import 'package:path_provider/path_provider.dart';
48

59
import '../api/core.dart';
610
import '../api/model/events.dart';
711
import '../api/model/initial_snapshot.dart';
812
import '../api/model/model.dart';
913
import '../api/route/events.dart';
1014
import '../api/route/messages.dart';
11-
import '../credential_fixture.dart' as credentials;
1215
import 'database.dart';
1316
import 'message_list.dart';
1417

@@ -110,8 +113,6 @@ abstract class GlobalStore extends ChangeNotifier {
110113

111114
Account? getAccount(int id) => _accounts[id];
112115

113-
// TODO(#13): rewrite these setters/mutators with a database
114-
115116
/// Add an account to the store, returning its assigned account ID.
116117
Future<int> insertAccount(AccountsCompanion data) async {
117118
final account = await doInsertAccount(data);
@@ -207,58 +208,70 @@ class PerAccountStore extends ChangeNotifier {
207208
}
208209
}
209210

211+
/// A [GlobalStore] that uses a live server and live, persistent local database.
212+
///
213+
/// The underlying data store is an [AppDatabase] corresponding to a SQLite
214+
/// database file in the app's persistent storage on the device.
215+
///
216+
/// The per-account stores will be instances of [LivePerAccountStore],
217+
/// with data loaded through [LiveApiConnection].
210218
class LiveGlobalStore extends GlobalStore {
211-
LiveGlobalStore._({required super.accounts}) : super();
212-
213-
// For convenience, a number we won't use as an ID in the database table.
214-
static const fixtureAccountId = -1;
219+
LiveGlobalStore._({
220+
required AppDatabase db,
221+
required super.accounts,
222+
}) : _db = db;
215223

216224
// We keep the API simple and synchronous for the bulk of the app's code
217225
// by doing this loading up front before constructing a [GlobalStore].
218226
static Future<GlobalStore> load() async {
219-
final accounts = {fixtureAccountId: _fixtureAccount};
220-
return LiveGlobalStore._(accounts: accounts);
227+
final db = AppDatabase(NativeDatabase.createInBackground(await _dbFile()));
228+
final accounts = await db.select(db.accounts).get();
229+
return LiveGlobalStore._(
230+
db: db,
231+
accounts: Map.fromEntries(accounts.map((a) => MapEntry(a.id, a))),
232+
);
233+
}
234+
235+
/// The file path to use for the app database.
236+
static Future<File> _dbFile() async {
237+
// What directory should we use?
238+
// path_provider's getApplicationSupportDirectory:
239+
// on Android, -> Flutter's PathUtils.getFilesDir -> https://developer.android.com/reference/android/content/Context#getFilesDir()
240+
// -> empirically /data/data/com.zulip.flutter/files/
241+
// on iOS, -> "Library/Application Support" via https://developer.apple.com/documentation/foundation/nssearchpathdirectory/nsapplicationsupportdirectory
242+
// on Linux, -> "${XDG_DATA_HOME:-~/.local/share}/com.zulip.flutter/"
243+
// All seem reasonable.
244+
// path_provider's getApplicationDocumentsDirectory:
245+
// on Android, -> Flutter's PathUtils.getDataDirectory -> https://developer.android.com/reference/android/content/Context#getDir(java.lang.String,%20int)
246+
// with https://developer.android.com/reference/android/content/Context#MODE_PRIVATE
247+
// on iOS, "Document directory" via https://developer.apple.com/documentation/foundation/nssearchpathdirectory/nsdocumentdirectory
248+
// on Linux, -> `xdg-user-dir DOCUMENTS` -> e.g. ~/Documents
249+
// That Linux answer is definitely not a fit. Harder to tell about the rest.
250+
final dir = await getApplicationSupportDirectory();
251+
return File(p.join(dir.path, 'zulip.db'));
221252
}
222253

254+
final AppDatabase _db;
255+
223256
@override
224257
Future<PerAccountStore> loadPerAccount(Account account) {
225258
return LivePerAccountStore.load(account);
226259
}
227260

228-
int _nextAccountId = 1;
229-
230261
@override
231262
Future<Account> doInsertAccount(AccountsCompanion data) async {
232-
final accountId = _nextAccountId;
233-
_nextAccountId++;
234-
return Account(
235-
id: accountId,
236-
realmUrl: data.realmUrl.value,
237-
userId: data.userId.value,
238-
email: data.email.value,
239-
apiKey: data.apiKey.value,
240-
zulipFeatureLevel: data.zulipFeatureLevel.value,
241-
zulipVersion: data.zulipVersion.value,
242-
zulipMergeBase: data.zulipMergeBase.value,
243-
);
263+
final accountId = await _db.createAccount(data);
264+
// We can *basically* predict what the Account will contain
265+
// based on the AccountsCompanion and the account ID. But
266+
// if we did that and then there was some subtle case where we
267+
// didn't match the database's behavior, that'd be a nasty bug.
268+
// This isn't a hot path, so just make the extra query.
269+
return await (_db.select(_db.accounts) // TODO perhaps put this logic in AppDatabase
270+
..where((a) => a.id.equals(accountId))
271+
).getSingle();
244272
}
245273
}
246274

247-
/// A scaffolding hack for while prototyping.
248-
///
249-
/// See "Server credentials" in the project README for how to fill in the
250-
/// `credential_fixture.dart` file this requires.
251-
final Account _fixtureAccount = Account(
252-
id: LiveGlobalStore.fixtureAccountId,
253-
realmUrl: Uri.parse(credentials.realmUrl),
254-
email: credentials.email,
255-
apiKey: credentials.apiKey,
256-
userId: credentials.userId,
257-
zulipFeatureLevel: 169,
258-
zulipVersion: '6.0-1235-g061f1dc43b',
259-
zulipMergeBase: '6.0-1235-g061f1dc43b',
260-
);
261-
262275
/// A [PerAccountStore] which polls an event queue to stay up to date.
263276
class LivePerAccountStore extends PerAccountStore {
264277
LivePerAccountStore.fromInitialSnapshot({

0 commit comments

Comments
 (0)