Files
buzz/mobile/lib/shared/hosted/hosted_environment.dart
klopez4212 f3a7bab098 Clarify mobile community onboarding and saved identity selection (#8729)
## Summary

Make community onboarding easier to scan: shorten hosted hostname
fallbacks, clarify Add Community instructions, and replace raw saved-key
buttons with identity cards. Use white pages, grey cards, ghost
secondary actions, standard page transitions, and a “Getting ready”
screen during joining. Native profile setup keeps a horizontal modal
transition. Identity cards load signed name/avatar metadata from saved
communities without switching accounts; unavailable profiles use a
neutral fallback. Cards show only the avatar, a 16px medium-weight name,
and muted npub; community lists are omitted. Retiring profile setup
closes its avatar editor too.

### Related issue

Follow-up to #8687 and #8599; no duplicate PR found.

### Testing

- Formatting and analysis passed. Full suite with hosted-auth defines:
4,019 passed, 5 skipped; includes profile-fetch and cancellation
regressions.
- Prior iOS device preview and native profile tests passed; hosted CI
native Swift checks passed on the preceding head. Latest Dart fixes are
not yet reinstalled on the phone.
- Simulator walkthrough and owner-tested iPhone preview use sample
identities and simulated admission.

Sample-data previews; the identity snapshot shows the
unavailable-profile fallback rendered by Flutter. Media is attached here
only.

| Community picker | Add Community | Saved identity |
| --- | --- | --- |
| ![Shortened community
name](https://github.com/user-attachments/assets/cf0af6aa-3a3d-4ffe-b469-e26f9bb92c30)
| ![White Add Community
page](https://github.com/user-attachments/assets/b2f71c77-1aa0-465a-b302-7eb5f1e92819)
| ![Saved identity card and ghost import
action](https://github.com/user-attachments/assets/47f20f34-f047-4ce5-befa-f56834096e95)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-30 19:00:42 +00:00

101 lines
3.5 KiB
Dart

/// Trusted build configuration, never supplied by a deep link or relay.
abstract final class HostedEnvironment {
/// Account UI and startup work are opt-in, independently of the backend URL.
static bool get enabled => authDomains.isNotEmpty;
static final Uri origin = _origin();
static const _communityAccounts = {
'communities.buzz.xyz': 'https://app.builderlab.xyz',
'communities.public.builderlabstaging.xyz':
'https://app.builderlabstaging.xyz',
};
/// Shortens hostname fallbacks for display without changing relay identity.
static String communityDisplayName(String name, String relayUrl) {
final relay = Uri.tryParse(relayUrl);
if (relay == null ||
name != relay.host ||
accountOriginFor(relay) == null) {
return name;
}
for (final domain in _communityAccounts.keys) {
final suffix = '.$domain';
if (name.endsWith(suffix)) {
return name.substring(0, name.length - suffix.length);
}
}
return name;
}
/// Creation and admission use the same trusted build-specific host mapping.
static String get communitySuffix => _communityAccounts.entries
.singleWhere((entry) => Uri.parse(entry.value) == origin)
.key;
/// Comma-separated DNS domains requiring Builderlab authentication.
/// Empty builds keep Nostr-only entry available, including hosted QR pairing.
static final List<String> authDomains = List.unmodifiable(
const String.fromEnvironment('BUZZ_AUTH_DOMAINS')
.split(',')
.map(
(domain) =>
domain.trim().toLowerCase().replaceFirst(RegExp(r'\.+$'), ''),
)
.where((domain) => domain.isNotEmpty)
.map((domain) {
if (!RegExp(
r'^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$',
).hasMatch(domain)) {
throw StateError(
'BUZZ_AUTH_DOMAINS must contain DNS names, not URLs or wildcards',
);
}
return domain;
}),
);
/// Matches a configured domain and its subdomains, never a string suffix.
static bool requiresAuth(String host) {
final normalized = host.toLowerCase().replaceFirst(RegExp(r'\.+$'), '');
return authDomains.any(
(domain) => normalized == domain || normalized.endsWith('.$domain'),
);
}
/// Explicit community-to-account mapping. Never infer an account endpoint
/// from arbitrary relay metadata or send this build's session across realms.
static Uri? accountOriginFor(Uri relay) {
if (!['wss', 'https'].contains(relay.scheme) ||
relay.userInfo.isNotEmpty ||
relay.hasQuery ||
relay.hasFragment ||
(relay.path.isNotEmpty && relay.path != '/') ||
(relay.hasPort && relay.port != 443)) {
return null;
}
final host = relay.host.toLowerCase();
for (final entry in _communityAccounts.entries) {
if (host.endsWith('.${entry.key}')) return Uri.parse(entry.value);
}
return null;
}
/// Whether this build can use its account session for this relay.
static bool supportsRelay(Uri relay) => accountOriginFor(relay) == origin;
static Uri _origin() {
const configured = String.fromEnvironment(
'BUZZ_BUILDERLAB_URL',
defaultValue: 'https://app.builderlab.xyz',
);
final uri = Uri.parse(configured);
if (!{
'https://app.builderlab.xyz',
'https://app.builderlabstaging.xyz',
}.contains(configured)) {
throw StateError('Unsupported hosted account environment');
}
return uri;
}
}