Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions components/layout/notification-bell.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Meta, StoryObj } from '@storybook/react'
import { NotificationBell } from './notification-bell'

const meta = {
title: 'Layout/NotificationBell',
component: NotificationBell,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof NotificationBell>

export default meta
type Story = StoryObj<typeof meta>

export const NoNotifications: Story = {
beforeEach: () => {
localStorage.removeItem('notifications')
},
decorators: [
(Story) => (
<div className="p-4">
<Story />
</div>
),
],
}

export const UnreadNotifications: Story = {
beforeEach: () => {
const notifications = [
{ id: '1', title: 'Stream Created', message: 'Your stream is active', read: false },
{ id: '2', title: 'Payment Received', message: '10 USDC received', read: true },
]
localStorage.setItem('notifications', JSON.stringify(notifications))
},
decorators: [
(Story) => (
<div className="p-4">
<Story />
</div>
),
],
}

export const LongNotificationList: Story = {
beforeEach: () => {
const notifications = Array.from({ length: 15 }, (_, i) => ({
id: `${i}`,
title: `Notification ${i + 1}`,
message: `This is notification number ${i + 1}`,
read: i > 5,
timestamp: new Date(Date.now() - i * 60000).toISOString(),
}))
localStorage.setItem('notifications', JSON.stringify(notifications))
},
decorators: [
(Story) => (
<div className="p-4">
<Story />
</div>
),
],
}
69 changes: 69 additions & 0 deletions components/settings/address-book-settings.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { Meta, StoryObj } from '@storybook/react'
import { AddressBookSettings } from './address-book-settings'

const meta = {
title: 'Settings/AddressBookSettings',
component: AddressBookSettings,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
} satisfies Meta<typeof AddressBookSettings>

export default meta
type Story = StoryObj<typeof meta>

export const EmptyBook: Story = {
beforeEach: () => {
localStorage.removeItem('address_book')
},
decorators: [
(Story) => (
<div className="max-w-2xl">
<Story />
</div>
),
],
}

export const BookWithEntries: Story = {
beforeEach: () => {
const addressBook = [
{
id: 'addr_1',
name: 'Alice Johnson',
address: 'GDZST3XVCDTUJ76ZAV2HA72KYUW4DKOWLQ4BVE32YQRGEAIFUQ2YCQY',
label: 'Primary Recipient',
createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
},
{
id: 'addr_2',
name: 'Bob Smith',
address: 'GBQT74NWN7KHZK5VPVL2YQHQIRLSVS3ATVUQRXE2BXVMQUQMVKTM5HSQ',
label: 'Company Account',
createdAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
},
{
id: 'addr_3',
name: 'Carol Lee',
address: 'GAOK5WSI7U3DKPWYULJEZGMFS5ZCJQPGQXE5MGRFVE52QOBVVBRKQW23',
label: 'Vault Account',
createdAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
},
{
id: 'addr_4',
name: 'David Chen',
address: 'GCSK5XT5ADFLBV7VJXVJTHZ3AKGFGAVDPVH3POYBNSPVWVVPNQVGJBQY',
createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
},
]
localStorage.setItem('address_book', JSON.stringify(addressBook))
},
decorators: [
(Story) => (
<div className="max-w-2xl">
<Story />
</div>
),
],
}
98 changes: 98 additions & 0 deletions components/webhooks/webhook-settings.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { Meta, StoryObj } from '@storybook/react'
import { WebhookSettings } from './webhook-settings'

const meta = {
title: 'Settings/WebhookSettings',
component: WebhookSettings,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
} satisfies Meta<typeof WebhookSettings>

export default meta
type Story = StoryObj<typeof meta>

export const EmptyState: Story = {
beforeEach: () => {
localStorage.removeItem('webhooks')
localStorage.removeItem('webhook_deliveries')
},
decorators: [
(Story) => (
<div className="max-w-2xl">
<Story />
</div>
),
],
}

export const RegisteredWebhook: Story = {
beforeEach: () => {
const webhooks = [
{
id: 'wh_1',
url: 'https://api.example.com/webhooks/flowstar',
events: ['stream.created', 'payment.received'],
active: true,
createdAt: new Date().toISOString(),
},
]
localStorage.setItem('webhooks', JSON.stringify(webhooks))
},
decorators: [
(Story) => (
<div className="max-w-2xl">
<Story />
</div>
),
],
}

export const DeliveryHistory: Story = {
beforeEach: () => {
const webhooks = [
{
id: 'wh_1',
url: 'https://api.example.com/webhooks/flowstar',
events: ['stream.created', 'payment.received'],
active: true,
createdAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
},
]
const deliveries = [
{
id: 'del_1',
webhookId: 'wh_1',
status: 'success',
statusCode: 200,
timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(),
},
{
id: 'del_2',
webhookId: 'wh_1',
status: 'failed',
statusCode: 500,
error: 'Internal Server Error',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
retries: 3,
},
{
id: 'del_3',
webhookId: 'wh_1',
status: 'success',
statusCode: 200,
timestamp: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(),
},
]
localStorage.setItem('webhooks', JSON.stringify(webhooks))
localStorage.setItem('webhook_deliveries', JSON.stringify(deliveries))
},
decorators: [
(Story) => (
<div className="max-w-2xl">
<Story />
</div>
),
],
}
4 changes: 4 additions & 0 deletions contracts/streaming/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,10 @@ impl StreamingContract {
///
/// Either party (sender or recipient) may call this. The stream must be
/// cancelled or fully drained before cleanup is allowed.
///
/// Note: cleanup_stream has no require_not_paused guard and will succeed
/// even when the contract is paused, since cleanup only removes storage
/// and does not modify stream state (issue #1051).
pub fn cleanup_stream(env: Env, caller: Address, stream_id: u64) -> Result<(), StreamError> {
caller.require_auth();

Expand Down
26 changes: 26 additions & 0 deletions contracts/streaming/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2167,6 +2167,10 @@ fn test_set_delegate_called_twice_overwrites_previous() {
}

#[test]
fn test_cleanup_stream_succeeds_while_paused() {
// cleanup_stream has no require_not_paused guard and will succeed even when
// the contract is paused, since cleanup only removes storage without
// modifying stream state (issue #1051).
fn test_update_stream_metadata_emits_event() {
// Verify that update_stream_metadata publishes StreamMetadataUpdatedEvent.
let t = TestEnv::setup();
Expand All @@ -2184,6 +2188,28 @@ fn test_update_stream_metadata_emits_event() {
);
let stream_id = client.create_stream(&t.sender, &params);

// Cancel the stream to make it eligible for cleanup
t.set_time(now + 500);
client.cancel(&stream_id);

// Pause the contract
client.pause();

// cleanup_stream should still succeed while paused (no require_not_paused guard)
let result = client.try_cleanup_stream(&t.sender, &stream_id);
assert_eq!(
result,
Ok(Ok(())),
"cleanup_stream should succeed while paused since it only removes storage"
);

// Verify the stream is actually cleaned up
assert!(
!client
.get_archived_sent_streams(&t.sender, &0, &100)
.contains(stream_id),
"stream should be removed from archive after cleanup"
);
// Update metadata and verify the event is published.
let metadata = StreamMetadata {
name: soroban_sdk::String::from_str(&t.env, "Payroll"),
Expand Down
Loading