mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(invites): add use-limited invite links (#3141)
## Summary - add database-backed v2 invite links with optional maximum-use limits and atomic final-slot redemption - preserve v1 invite compatibility while adding exhausted/expired/invalid client handling across desktop, web, and mobile - emit structured claim-outcome logs with community, invite ID, outcome, maximum uses, and post-claim count ## Verification - `cargo fmt --all -- --check` - `cargo test -p buzz-db` (85 passed, 134 Postgres-dependent ignored) - `cargo clippy -p buzz-db --all-targets -- -D warnings` - desktop `npm run typecheck` - push hook: desktop checks/tests, desktop Tauri tests, Rust tests, and branch-skew passed - Postgres integration tests were previously reviewed green at the pre-rebase tree; local rerun on this session was unavailable because Postgres/Docker were not running - mobile push-hook check could not start because Flutter is unavailable locally --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub1c4alndp82zyt9veaklm5d965quss79vlhk9awv7qu5erwhmf42qqlvc25c <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
This commit is contained in:
co-authored by
npub1c4alndp82zyt9veaklm5d965quss79vlhk9awv7qu5erwhmf42qqlvc25c
npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc
npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7
parent
98a7b13348
commit
d500c2d5cf
@@ -261,12 +261,17 @@ String _communityNameFromClaim(Map<String, dynamic> claim, String relayUrl) {
|
||||
}
|
||||
|
||||
bool _requiresFreshInvite(Object error) {
|
||||
return error.toString().contains('join_policy_required');
|
||||
final message = error.toString();
|
||||
return message.contains('join_policy_required') ||
|
||||
message.contains('invite_exhausted');
|
||||
}
|
||||
|
||||
String _friendlyInviteError(Object error) {
|
||||
final message = error.toString();
|
||||
if (message.contains('invite_expired')) return 'This invite has expired.';
|
||||
if (message.contains('invite_exhausted')) {
|
||||
return 'This invite has reached its use limit. Ask for a new invite.';
|
||||
}
|
||||
if (message.contains('invite_invalid')) return 'This invite is not valid.';
|
||||
if (message.contains('join_policy_required')) {
|
||||
return 'This invite approval has expired. Re-open the invite link to try again.';
|
||||
|
||||
@@ -218,6 +218,49 @@ void main() {
|
||||
expect(attempts, 1);
|
||||
});
|
||||
|
||||
test('invite_exhausted requires a fresh invite and cannot retry', () async {
|
||||
final keys = nostr.Keys.generate();
|
||||
var attempts = 0;
|
||||
final storage = CommunityStorage(secure: FakeSecureStorage());
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
communityStorageProvider.overrideWithValue(storage),
|
||||
inviteKeyGeneratorProvider.overrideWithValue(() => keys),
|
||||
inviteJoinHttpClientProvider.overrideWithValue(
|
||||
http_testing.MockClient((request) async {
|
||||
attempts++;
|
||||
return http.Response(
|
||||
jsonEncode({'error': 'invite_exhausted'}),
|
||||
403,
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container
|
||||
.read(inviteJoinProvider.notifier)
|
||||
.prepare(
|
||||
const InviteDeepLink(
|
||||
relayUrl: 'wss://relay.example.com',
|
||||
code: 'v2.exhausted-secret',
|
||||
),
|
||||
);
|
||||
await container.read(inviteJoinProvider.notifier).confirmJoin();
|
||||
|
||||
final state = container.read(inviteJoinProvider);
|
||||
expect(state.status, InviteJoinStatus.error);
|
||||
expect(state.requiresFreshInvite, isTrue);
|
||||
expect(
|
||||
state.errorMessage,
|
||||
'This invite has reached its use limit. Ask for a new invite.',
|
||||
);
|
||||
|
||||
await container.read(inviteJoinProvider.notifier).confirmJoin();
|
||||
expect(attempts, 1);
|
||||
});
|
||||
|
||||
test('failed claim can be retried and preserves policy receipt', () async {
|
||||
final keys = nostr.Keys.generate();
|
||||
var attempts = 0;
|
||||
|
||||
Reference in New Issue
Block a user