Skip to content

Commit d8d4f50

Browse files
authored
Consolidate connection creation into one experience (#427)
1 parent b5fc791 commit d8d4f50

File tree

82 files changed

+5223
-3493
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+5223
-3493
lines changed

services/backend-api/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,6 @@ deploy*.sh
4141
e2e/auth.json
4242
e2e/config.json
4343
e2e/test-results/
44+
e2e/.paddle-state.json
45+
e2e/.tunnel-pid
4446
playwright-report/

services/backend-api/client/src/components/DiscordMessageDisplay/DiscordMessageDisplay.test.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ vi.mock("../DiscordView", () => ({
1616
{messages?.map(
1717
(
1818
msg: { content?: string; embeds?: Array<{ title?: string; description?: string }> },
19-
i: number,
19+
i: number
2020
) => (
2121
<div key={i} data-testid="legacy-message">
2222
{msg.content && <span data-testid="message-content">{msg.content}</span>}
@@ -29,7 +29,7 @@ vi.mock("../DiscordView", () => ({
2929
</div>
3030
))}
3131
</div>
32-
),
32+
)
3333
)}
3434
</div>
3535
)),
@@ -232,7 +232,7 @@ describe("DiscordMessageDisplay", () => {
232232
// MediaGallery renders a grid with image containers
233233
// In test environment, images may show fallback UI since URLs don't load
234234
const { container } = renderWithChakra(
235-
<DiscordMessageDisplay messages={[mockV2WithMediaGallery]} />,
235+
<DiscordMessageDisplay messages={[mockV2WithMediaGallery]} />
236236
);
237237

238238
// Verify the component renders (doesn't throw)
@@ -320,12 +320,21 @@ describe("DiscordMessageDisplay", () => {
320320
expect(avatar).toBeInTheDocument();
321321
});
322322

323-
it("renders APP badge", () => {
323+
it("renders APP badge with checkmark by default", () => {
324324
renderWithChakra(<DiscordMessageDisplay messages={[mockV2Message]} />);
325325

326326
expect(screen.getByText("✓ APP")).toBeInTheDocument();
327327
});
328328

329+
it("renders APP badge without checkmark when showVerifiedInAppBadge is false", () => {
330+
renderWithChakra(
331+
<DiscordMessageDisplay messages={[mockV2Message]} showVerifiedInAppBadge={false} />
332+
);
333+
334+
expect(screen.getByText("APP")).toBeInTheDocument();
335+
expect(screen.queryByText("✓ APP")).not.toBeInTheDocument();
336+
});
337+
329338
it("renders timestamp", () => {
330339
renderWithChakra(<DiscordMessageDisplay messages={[mockV2Message]} />);
331340

@@ -354,7 +363,7 @@ describe("DiscordMessageDisplay", () => {
354363

355364
it("matches snapshot for MediaGallery", () => {
356365
const { container } = renderWithChakra(
357-
<DiscordMessageDisplay messages={[mockV2WithMediaGallery]} />,
366+
<DiscordMessageDisplay messages={[mockV2WithMediaGallery]} />
358367
);
359368

360369
expect(container).toMatchSnapshot();

services/backend-api/client/src/components/DiscordMessageDisplay/index.tsx

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ const DISCORD_V2_COMPONENT_TYPE = {
3434
Container: 17,
3535
} as const;
3636

37-
const MONITORSS_AVATAR_URL =
38-
"https://cdn.discordapp.com/avatars/302050872383242240/1fb101f4b0fe104b6b8c53ec5e3d5af6.png";
39-
const MONITORSS_USERNAME = "MonitoRSS";
37+
export const DISCORD_DEFAULT_AVATAR_URL = "https://cdn.discordapp.com/embed/avatars/0.png";
38+
export const MONITORSS_USERNAME = "MonitoRSS";
4039

4140
const buttonColors: Record<string, { bg: string; color: string; border: string }> = {
4241
Primary: {
@@ -115,12 +114,15 @@ interface DiscordMessageDisplayProps {
115114
isLoading?: boolean;
116115
emptyMessage?: string;
117116
mentionResolvers?: MentionResolvers;
117+
username?: string;
118+
avatarUrl?: string;
119+
showVerifiedInAppBadge?: boolean;
118120
}
119121

120122
// Shared button rendering logic to avoid duplication
121123
const renderButtonElement = (
122124
btn: { style?: number; url?: string | null; disabled?: boolean; label?: string },
123-
key: string,
125+
key: string
124126
): React.ReactNode => {
125127
const styleName = styleNumToName[btn.style || 2] || "Secondary";
126128
const colors = buttonColors[styleName];
@@ -130,7 +132,7 @@ const renderButtonElement = (
130132
<Button
131133
key={key}
132134
as={isLinkButton ? "a" : undefined}
133-
href={isLinkButton ? (btn.url ?? undefined) : undefined}
135+
href={isLinkButton ? btn.url ?? undefined : undefined}
134136
target={isLinkButton ? "_blank" : undefined}
135137
rel={isLinkButton ? "noopener noreferrer" : undefined}
136138
size="sm"
@@ -159,7 +161,7 @@ const renderButtonElement = (
159161

160162
const renderApiAccessory = (
161163
accessory: DiscordApiComponent["accessory"],
162-
key: string,
164+
key: string
163165
): React.ReactNode => {
164166
if (!accessory) return null;
165167

@@ -231,7 +233,7 @@ const renderApiButton = (btn: DiscordApiComponent["accessory"], key: string): Re
231233
const renderApiComponent = (
232234
comp: DiscordApiComponent,
233235
index: number,
234-
mentionResolvers?: MentionResolvers,
236+
mentionResolvers?: MentionResolvers
235237
): React.ReactNode => {
236238
const { type } = comp;
237239
const parserState = mentionResolvers ? { mentionResolvers } : {};
@@ -302,7 +304,7 @@ const renderApiComponent = (
302304
const renderGalleryItem = (
303305
item: { media?: { url: string }; spoiler?: boolean; description?: string },
304306
i: number,
305-
height?: string,
307+
height?: string
306308
) => (
307309
<Box
308310
key={`gallery-item-${index}-${i}`}
@@ -569,7 +571,7 @@ const renderApiComponent = (
569571
)}
570572
<VStack align="stretch" spacing={2} pl={accentColor ? 2 : 0}>
571573
{containerComp.components?.map((child, i) =>
572-
renderApiComponent(child, i, mentionResolvers),
574+
renderApiComponent(child, i, mentionResolvers)
573575
)}
574576
</VStack>
575577
</Box>
@@ -585,6 +587,9 @@ export const DiscordMessageDisplay: React.FC<DiscordMessageDisplayProps> = ({
585587
isLoading,
586588
emptyMessage,
587589
mentionResolvers,
590+
username,
591+
avatarUrl,
592+
showVerifiedInAppBadge = true,
588593
}) => {
589594
const bgColor = useColorModeValue("#36393f", "#36393f");
590595
const textColor = useColorModeValue("#dcddde", "#dcddde");
@@ -606,16 +611,15 @@ export const DiscordMessageDisplay: React.FC<DiscordMessageDisplayProps> = ({
606611
<HStack align="flex-start" spacing={3}>
607612
<Avatar
608613
size="sm"
609-
src={MONITORSS_AVATAR_URL}
610-
name={MONITORSS_USERNAME}
614+
src={avatarUrl || DISCORD_DEFAULT_AVATAR_URL}
611615
borderRadius="50%"
612616
w={10}
613617
h={10}
614618
/>
615619
<Stack spacing={1} flex={1} maxW="calc(100% - 40px - 0.75rem)">
616620
<HStack spacing={2} align="center">
617621
<Text fontSize="sm" fontWeight="semibold" color="white">
618-
{MONITORSS_USERNAME}
622+
{username || MONITORSS_USERNAME}
619623
</Text>
620624
<Box
621625
fontSize="xs"
@@ -627,7 +631,7 @@ export const DiscordMessageDisplay: React.FC<DiscordMessageDisplayProps> = ({
627631
fontWeight="bold"
628632
lineHeight="1"
629633
>
630-
APP
634+
{showVerifiedInAppBadge ? "✓ " : ""}APP
631635
</Box>
632636
<Text fontSize="xs" color="#a3a6aa" ml={1}>
633637
Today at 12:04 PM
@@ -697,16 +701,15 @@ export const DiscordMessageDisplay: React.FC<DiscordMessageDisplayProps> = ({
697701
<HStack align="flex-start" spacing={3}>
698702
<Avatar
699703
size="sm"
700-
src={MONITORSS_AVATAR_URL}
701-
name={MONITORSS_USERNAME}
704+
src={avatarUrl || DISCORD_DEFAULT_AVATAR_URL}
702705
borderRadius="50%"
703706
w={10}
704707
h={10}
705708
/>
706709
<Stack spacing={1} flex={1} maxW="calc(100% - 40px - 0.75rem)">
707710
<HStack spacing={2} align="center">
708711
<Text fontSize="sm" fontWeight="semibold" color="white">
709-
{MONITORSS_USERNAME}
712+
{username || MONITORSS_USERNAME}
710713
</Text>
711714
<Box
712715
fontSize="xs"
@@ -718,7 +721,7 @@ export const DiscordMessageDisplay: React.FC<DiscordMessageDisplayProps> = ({
718721
fontWeight="bold"
719722
lineHeight="1"
720723
>
721-
APP
724+
{showVerifiedInAppBadge ? "✓ " : ""}APP
722725
</Box>
723726
<Text fontSize="xs" color="#a3a6aa" ml={1}>
724727
Today at 12:04 PM
@@ -729,8 +732,8 @@ export const DiscordMessageDisplay: React.FC<DiscordMessageDisplayProps> = ({
729732
{legacyMessages.length > 0 && (
730733
<DiscordView
731734
darkTheme
732-
username={MONITORSS_USERNAME}
733-
avatar_url={MONITORSS_AVATAR_URL}
735+
username={username || MONITORSS_USERNAME}
736+
avatar_url={avatarUrl || DISCORD_DEFAULT_AVATAR_URL}
734737
messages={legacyMessages}
735738
excludeHeader
736739
mentionResolvers={mentionResolvers}

services/backend-api/client/src/components/PricingDialog/index.tsx

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { useNavigate } from "react-router-dom";
3737
import { InlineErrorAlert } from "../InlineErrorAlert";
3838
import { FAQ } from "../FAQ";
3939
import { ChangeSubscriptionDialog } from "../ChangeSubscriptionDialog";
40-
import { pages, ProductKey } from "../../constants";
40+
import { pages, ProductKey, TIER_CONFIGS } from "../../constants";
4141
import { EXTERNAL_PROPERTIES_MAX_ARTICLES } from "../../constants/externalPropertiesMaxArticles";
4242
import { captureException } from "@sentry/react";
4343
import { usePaddleContext } from "../../contexts/PaddleContext";
@@ -52,65 +52,6 @@ interface Props {
5252
onOpen: () => void;
5353
}
5454

55-
enum Feature {
56-
Feeds = "Feeds",
57-
ArticleLimit = "Article Limit",
58-
Webhooks = "Webhooks",
59-
CustomPlaceholders = "Custom Placeholders",
60-
RefreshRate = "Refresh Rate",
61-
ExternalProperties = "External Properties",
62-
}
63-
64-
interface TierConfig {
65-
productId: ProductKey;
66-
supportsAdditionalFeeds?: boolean;
67-
features: Array<{ name: string; description: string; enabled?: boolean }>;
68-
}
69-
70-
const TIER_CONFIGS: TierConfig[] = [
71-
{
72-
productId: ProductKey.Tier1,
73-
features: [
74-
{ name: Feature.Feeds, description: "Track 35 news feeds", enabled: true },
75-
{ name: Feature.ArticleLimit, description: "1000 articles daily per feed", enabled: true },
76-
{ name: Feature.Webhooks, description: "Custom name/avatar with webhooks", enabled: true },
77-
{ name: Feature.CustomPlaceholders, description: "Custom placeholders", enabled: true },
78-
{ name: Feature.RefreshRate, description: "2 minute refresh rate", enabled: true },
79-
],
80-
},
81-
{
82-
productId: ProductKey.Tier2,
83-
features: [
84-
{ name: Feature.Feeds, description: "Track 70 news feeds", enabled: true },
85-
{ name: Feature.ArticleLimit, description: "1000 articles daily per feed", enabled: true },
86-
{ name: Feature.Webhooks, description: "Custom name/avatar with webhooks", enabled: true },
87-
{ name: Feature.CustomPlaceholders, description: "Custom placeholders", enabled: true },
88-
{
89-
name: Feature.ExternalProperties,
90-
description: "External properties (scrape external links)*",
91-
enabled: true,
92-
},
93-
{ name: Feature.RefreshRate, description: "2 minute refresh rate", enabled: true },
94-
],
95-
},
96-
{
97-
productId: ProductKey.Tier3,
98-
supportsAdditionalFeeds: true,
99-
features: [
100-
{ name: Feature.Feeds, description: "Track 140 news feeds", enabled: true },
101-
{ name: Feature.ArticleLimit, description: "1000 articles daily per feed", enabled: true },
102-
{ name: Feature.Webhooks, description: "Custom name/avatar with webhooks", enabled: true },
103-
{ name: Feature.CustomPlaceholders, description: "Custom placeholders", enabled: true },
104-
{
105-
name: Feature.ExternalProperties,
106-
description: "External properties (scrape external links)*",
107-
enabled: true,
108-
},
109-
{ name: Feature.RefreshRate, description: "2 minute refresh rate", enabled: true },
110-
],
111-
},
112-
];
113-
11455
const getIdealPriceTextSize = (length: number) => {
11556
if (length < 10) return "6xl";
11657
if (length < 11) return "5xl";

services/backend-api/client/src/components/SubscriberBlockText/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,7 @@ export const SubscriberBlockText = ({ alternateText, onClick, feature, supporter
100100
<Box>
101101
<Text>
102102
{alternateText ||
103-
`You must be a supporter at a sufficient tier (${showTier}) to access this. Consider
104-
supporting MonitoRSS's free services and open-source development!`}
103+
`Upgrade to a paid plan to deliver articles with your own custom name and avatar — so your feed looks like a natural part of your server.`}
105104
</Text>
106105
{userMeData?.result.enableBilling && (
107106
<Button mt={4} onClick={onClickBecomeSupporter}>

services/backend-api/client/src/constants/productKey.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,95 @@ export const PRICE_IDS: ProductPriceIds = import.meta.env.PROD
7070
? PRODUCTION_PRICE_IDS
7171
: SANDBOX_PRICE_IDS;
7272

73+
export enum ProductFeature {
74+
Feeds = "Feeds",
75+
ArticleLimit = "Article Limit",
76+
Webhooks = "Webhooks",
77+
CustomPlaceholders = "Custom Placeholders",
78+
RefreshRate = "Refresh Rate",
79+
ExternalProperties = "External Properties",
80+
}
81+
82+
export interface TierFeature {
83+
name: string;
84+
description: string;
85+
enabled?: boolean;
86+
}
87+
88+
export interface TierConfig {
89+
productId: ProductKey;
90+
supportsAdditionalFeeds?: boolean;
91+
features: TierFeature[];
92+
}
93+
94+
export const TIER_CONFIGS: TierConfig[] = [
95+
{
96+
productId: ProductKey.Tier1,
97+
features: [
98+
{ name: ProductFeature.Feeds, description: "Track 35 news feeds", enabled: true },
99+
{
100+
name: ProductFeature.ArticleLimit,
101+
description: "1000 articles daily per feed",
102+
enabled: true,
103+
},
104+
{ name: ProductFeature.Webhooks, description: "Branded message delivery", enabled: true },
105+
{
106+
name: ProductFeature.CustomPlaceholders,
107+
description: "Custom placeholders",
108+
enabled: true,
109+
},
110+
{ name: ProductFeature.RefreshRate, description: "2 minute refresh rate", enabled: true },
111+
],
112+
},
113+
{
114+
productId: ProductKey.Tier2,
115+
features: [
116+
{ name: ProductFeature.Feeds, description: "Track 70 news feeds", enabled: true },
117+
{
118+
name: ProductFeature.ArticleLimit,
119+
description: "1000 articles daily per feed",
120+
enabled: true,
121+
},
122+
{ name: ProductFeature.Webhooks, description: "Branded message delivery", enabled: true },
123+
{
124+
name: ProductFeature.CustomPlaceholders,
125+
description: "Custom placeholders",
126+
enabled: true,
127+
},
128+
{
129+
name: ProductFeature.ExternalProperties,
130+
description: "External properties (scrape external links)*",
131+
enabled: true,
132+
},
133+
{ name: ProductFeature.RefreshRate, description: "2 minute refresh rate", enabled: true },
134+
],
135+
},
136+
{
137+
productId: ProductKey.Tier3,
138+
supportsAdditionalFeeds: true,
139+
features: [
140+
{ name: ProductFeature.Feeds, description: "Track 140 news feeds", enabled: true },
141+
{
142+
name: ProductFeature.ArticleLimit,
143+
description: "1000 articles daily per feed",
144+
enabled: true,
145+
},
146+
{ name: ProductFeature.Webhooks, description: "Branded message delivery", enabled: true },
147+
{
148+
name: ProductFeature.CustomPlaceholders,
149+
description: "Custom placeholders",
150+
enabled: true,
151+
},
152+
{
153+
name: ProductFeature.ExternalProperties,
154+
description: "External properties (scrape external links)*",
155+
enabled: true,
156+
},
157+
{ name: ProductFeature.RefreshRate, description: "2 minute refresh rate", enabled: true },
158+
],
159+
},
160+
];
161+
73162
export const findProductKeyByPriceId = (
74163
priceId: string,
75164
): Exclude<ProductKey, ProductKey.Free> | null => {

0 commit comments

Comments
 (0)