Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: simplified collect screen #691

Merged
merged 12 commits into from
Oct 3, 2023
8 changes: 4 additions & 4 deletions src/e2e-tests/top-up-create-stream.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,11 @@ describe('app', async () => {
});

it('expands the squeezing section', async () => {
await page.locator('label:has-text("Include funds from current cycle")').click();
await page.locator('label:has-text("Include unsettled stream earnings")').click();

await page
.locator(`data-testid=item-383620263794848526656662033323214000554911775452`)
.click();
await expect(
page.locator(`data-testid=item-383620263794848526656662033323214000554911775452`),
).toHaveAttribute('aria-selected', 'true');

await page.locator('button', { hasText: 'Collect TEST' }).click();
});
Expand Down
8 changes: 5 additions & 3 deletions src/lib/components/annotation-box/annotation-box.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
<slot />
</div>
</div>
<div class="flex-1 flex justify-end">
<slot name="actions" />
</div>
{#if $$slots.actions}
<div class="flex-1 flex justify-end">
<slot name="actions" />
</div>
{/if}
</div>

<style>
Expand Down
184 changes: 87 additions & 97 deletions src/lib/flows/collect-flow/collect-amounts.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,12 @@
import expect from '$lib/utils/expect';
import transact, { makeTransactPayload } from '$lib/components/stepper/utils/transact';
import type { StepComponentEvents } from '$lib/components/stepper/types';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import SafeAppDisclaimer from '$lib/components/safe-app-disclaimer/safe-app-disclaimer.svelte';
import type { AddressDriverAccount } from '$lib/stores/streams/types';

export let context: Writable<CollectFlowState>;

const restorer = $context.restorer;

$: cycle = $context.currentDripsCycle ?? unreachable();
$: currentCycleEnd = new Date(cycle.start.getTime() + cycle.durationMillis);

Expand Down Expand Up @@ -86,6 +84,15 @@
return acc;
}, []);

// Initially select all incoming squeeze senders by default, unless there's prior state.
onMount(() => {
if ($context.selectedSqueezeSenderItems?.length === 0 && !$context.squeezeEnabled) {
$context.selectedSqueezeSenderItems = incomingEstimatesBySender.map(
(e) => e.sender.accountId,
);
}
});

let currentCycleSenders: Items;
$: currentCycleSenders = Object.fromEntries(
mapFilterUndefined(incomingEstimatesBySender, (estimate) => {
Expand All @@ -110,11 +117,8 @@
}),
);

let squeezeEnabled = restorer.restore('squeezeEnabled');
let selectedSqueezeSenderItems: string[] = restorer.restore('selectedSqueezeSenderItems');

$: totalSelectedSqueezeAmount = squeezeEnabled
? selectedSqueezeSenderItems.reduce<bigint>(
$: totalSelectedSqueezeAmount = $context.squeezeEnabled
? $context.selectedSqueezeSenderItems.reduce<bigint>(
(acc, sender) =>
acc +
(incomingEstimatesBySender.find((e) => e.sender.accountId === sender)?.amount ??
Expand Down Expand Up @@ -145,8 +149,8 @@
const { DRIPS, ADDRESS_DRIVER } = getNetworkConfig();

let squeezeArgs: Awaited<ReturnType<typeof getSqueezeArgs>> | undefined;
if (squeezeEnabled && selectedSqueezeSenderItems.length > 0) {
squeezeArgs = await getSqueezeArgs(selectedSqueezeSenderItems, tokenAddress);
if ($context.squeezeEnabled && $context.selectedSqueezeSenderItems.length > 0) {
squeezeArgs = await getSqueezeArgs($context.selectedSqueezeSenderItems, tokenAddress);
}

const collectFlow = await AddressDriverPresets.Presets.createCollectFlow({
Expand Down Expand Up @@ -201,125 +205,123 @@
context.update((c) => ({
...c,
amountCollected,
squeezeEnabled,
receipt,
}));

// The squeeze event should be indexed by now, so this should cause the dashboard to update
// in the background to reflect the newly reduced incoming balance.
if (squeezeEnabled) await balancesStore.updateSqueezeHistory(transactContext.accountId);
if ($context.squeezeEnabled) {
await balancesStore.updateSqueezeHistory(transactContext.accountId);
}
},
}),
);
}

$: restorer.saveAll({
squeezeEnabled,
selectedSqueezeSenderItems,
});
</script>

<StepLayout>
<EmojiAndToken emoji="👛" {tokenAddress} animateTokenOnMount={splittableAfterReceive !== 0n} />
<StepHeader headline={`Collect ${selectedToken.symbol}`} />
<div>
<p>
Tokens streamed to your account automatically become receivable on a weekly cycle. Your
receivable balance updates next on <span class="typo-text-bold"
>{formatDate(currentCycleEnd)}</span
Earnings settle once per week. The next settlement date is <span class="typo-text-bold"
>{formatDate(currentCycleEnd, 'onlyDay')}</span
>.
</p>
<a
class="typo-text-small"
target="_blank"
rel="noreferrer"
href="https://docs.drips.network/docs/streaming-and-splitting/manage-funds/collect-earnings"
>Learn more</a
>
</div>
<div class="squeeze-section">
<Toggleable label="Include funds from current cycle" bind:toggled={squeezeEnabled}>
<p>
Select which senders from the current cycle you would like to collect from. The network fee
for collecting increases with each selected sender.
</p>
<AnnotationBox type="warning">
The amounts shown below are estimated based on your system time so the value you collect may
slightly differ.
</AnnotationBox>
<div class="list-wrapper">
<ListSelect
items={currentCycleSenders}
multiselect
bind:selected={selectedSqueezeSenderItems}
searchable={false}
/>
</div>
</Toggleable>
</div>
{#if incomingEstimatesBySender.length > 0}
<div class="squeeze-section">
<Toggleable label="Include unsettled stream earnings" bind:toggled={$context.squeezeEnabled}>
<AnnotationBox type="warning">
The network fee for collecting increases with each selected sender. Unsettled earnings are
estimates, so you may collect less than expected.
</AnnotationBox>
<div class="list-wrapper">
<ListSelect
items={currentCycleSenders}
multiselect
bind:selected={$context.selectedSqueezeSenderItems}
searchable={false}
emptyStateText="You don't have any unsettled earnings from streams."
/>
</div>
</Toggleable>
</div>
{/if}
<FormField title="Review">
<LineItems
lineItems={mapFilterUndefined(
[
squeezeEnabled
balances.receivable > 0n
? {
title: `${selectedToken.symbol} from current cycle`,
subtitle: 'Earned from incoming streams',
title: `Streams`,
subtitle: $context.squeezeEnabled ? 'Including unsettled earnings' : undefined,
value:
'≈ ' +
formatTokenAmount(
makeAmount(totalSelectedSqueezeAmount ?? 0n),
selectedToken.decimals,
1n,
),
$context.squeezeEnabled && totalSelectedSqueezeAmount > 0n
? '≈ ' +
formatTokenAmount(
makeAmount(balances.receivable + (totalSelectedSqueezeAmount ?? 0n)),
selectedToken.decimals,
1n,
)
: formatTokenAmount(
makeAmount(balances.receivable),
selectedToken.decimals,
1n,
),
symbol: selectedToken.symbol,
}
: undefined,
{
title: `${selectedToken.symbol} from concluded cycles`,
subtitle: 'Earned from incoming streams',
value: formatTokenAmount(makeAmount(balances.receivable), selectedToken.decimals, 1n),
symbol: selectedToken.symbol,
disabled: balances.receivable === 0n,
},
balances.splittable > 0n
? {
title: `Splittable ${selectedToken.symbol}`,
subtitle: 'Earned from already-received streams or incoming splits & gives',
value:
'+' +
formatTokenAmount(makeAmount(balances.splittable), selectedToken.decimals, 1n),
title: 'Drip Lists and projects',
value: formatTokenAmount(
makeAmount(balances.splittable),
selectedToken.decimals,
1n,
),
symbol: selectedToken.symbol,
disabled: balances.splittable === 0n,
}
: undefined,
{
title: `Splitting ${getSplitPercent(1000000n - ownSplitsWeight, 'pretty')}`,
value:
(squeezeEnabled ? '≈ ' : '') +
formatTokenAmount(
makeAmount(collectableAfterSplit - splittableAfterReceive),
selectedToken.decimals,
1n,
),
disabled:
ownSplitsWeight === 1000000n || collectableAfterSplit - splittableAfterReceive === 0n,
symbol: selectedToken.symbol,
},
/*
It used to be possible to set splits for your own AddressDriver account in the Drips App.
Even though it's no longer possible to do so, maybe some old account still has splits set,
or maybe the user manually configured splits outside the app. For this reason, we display
the splitting percentage while collecting, but only if it's more than 0%.
*/
ownSplitsWeight < 1000000n
? {
title: `Splitting ${getSplitPercent(1000000n - ownSplitsWeight, 'pretty')}`,
value:
($context.squeezeEnabled ? '≈ ' : '') +
formatTokenAmount(
makeAmount(collectableAfterSplit - splittableAfterReceive),
selectedToken.decimals,
1n,
),
disabled:
ownSplitsWeight === 1000000n ||
collectableAfterSplit - splittableAfterReceive === 0n,
symbol: selectedToken.symbol,
}
: undefined,
balances.collectable !== 0n
? {
title: `Previously-split funds`,
value:
'+' +
formatTokenAmount(makeAmount(balances.collectable), selectedToken.decimals, 1n),
value: formatTokenAmount(
makeAmount(balances.collectable),
selectedToken.decimals,
1n,
),
symbol: selectedToken.symbol,
}
: undefined,
{
title: 'You collect',
subtitle: 'These funds will be sent to your wallet.',
value:
(squeezeEnabled ? '≈ ' : '') +
($context.squeezeEnabled && totalSelectedSqueezeAmount > 0n ? '≈ ' : '') +
formatTokenAmount(makeAmount(collectableAfterSplit), selectedToken.decimals, 1n),
symbol: selectedToken.symbol,
disabled: collectableAfterSplit === 0n,
Expand All @@ -346,18 +348,6 @@
text-align: left;
}

.squeeze-section p {
margin-bottom: 1rem;
}

a {
color: var(--color-foreground-level-6);
text-decoration: underline;
display: block;
margin-top: 0.5rem;
text-align: left;
}

.list-wrapper {
margin-top: 1rem;
border: 1px solid var(--color-foreground);
Expand Down
13 changes: 4 additions & 9 deletions src/lib/flows/collect-flow/collect-flow-state.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { newRestorer, type Restorer } from '$lib/utils/restorer';
import type { ContractReceipt } from 'ethers';
import type { SplitsEntry } from 'radicle-drips';
import { writable } from 'svelte/store';

type Restorable = {
squeezeEnabled: boolean;
selectedSqueezeSenderItems: string[];
};

export interface CollectFlowState {
tokenAddress?: string;
balances?: {
Expand All @@ -23,13 +17,14 @@ export interface CollectFlowState {
durationMillis: number;
};
amountCollected?: bigint;
squeezeEnabled?: boolean;
squeezeEnabled: boolean;
selectedSqueezeSenderItems: string[];
receipt?: ContractReceipt;
restorer: Restorer<Restorable>;
}

export default (tokenAddress?: string) =>
writable<CollectFlowState>({
tokenAddress,
restorer: newRestorer<Restorable>({ squeezeEnabled: false, selectedSqueezeSenderItems: [] }),
squeezeEnabled: false,
selectedSqueezeSenderItems: [],
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
onMount(() =>
dispatch('await', {
promise,
message: 'Fetching collectable amounts…',
message: 'Getting ready…',
}),
);
</script>
19 changes: 18 additions & 1 deletion src/lib/utils/format-date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ const DATE_FORMAT_CONVENTIONS = {
},
} as const;

function suffixNumber(n: number) {
if (n > 3 && n < 21) return 'th';
switch (n % 10) {
case 1:
return 'st';
case 2:
return 'nd';
case 3:
return 'rd';
default:
return 'th';
}
}

/**
* Format a date with a reusable, pre-configured date format convention.
* @param date The date to format.
Expand All @@ -33,5 +47,8 @@ export default function (
date: Date,
convention: keyof typeof DATE_FORMAT_CONVENTIONS = 'standard',
): string {
return Intl.DateTimeFormat('en-US', DATE_FORMAT_CONVENTIONS[convention]).format(date);
return (
Intl.DateTimeFormat('en-US', DATE_FORMAT_CONVENTIONS[convention]).format(date) +
(convention === 'onlyDay' ? suffixNumber(date.getDate()) : '')
);
}
4 changes: 2 additions & 2 deletions src/routes/app/(app)/projects/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@
<div class="earnings card">
<div class="content">
<div class="values">
<KeyValuePair key="Collectable now" highlight>
<KeyValuePair key="Settled earnings" highlight>
<AggregateFiatEstimate amounts={$splittableStore} />
</KeyValuePair>
<KeyValuePair key="Next payout">{formatDate(cycle.end, 'onlyDay')}</KeyValuePair>
<KeyValuePair key="Next settlement">{formatDate(cycle.end, 'onlyDay')}</KeyValuePair>
</div>
<div />
</div>
Expand Down