Skip to content

Commit 4a9534f

Browse files
committed
fix(web): tighten token recovery and retry CTA gating
1 parent 788ecce commit 4a9534f

3 files changed

Lines changed: 65 additions & 5 deletions

File tree

frontend/src/__tests__/App.test.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ describe('App', () => {
276276

277277
expect(screen.getByText('Preparing feed')).toBeInTheDocument();
278278
expect(
279-
screen.getByText('Creating the feed and loading its preview before showing the result.')
279+
screen.getByText('Creating the feed now. The result appears first, then preview loading continues.')
280280
).toBeInTheDocument();
281281
});
282282

@@ -497,6 +497,35 @@ describe('App', () => {
497497
expect(screen.queryByRole('button', { name: /Try .* instead/ })).not.toBeInTheDocument();
498498
});
499499

500+
it('does not treat non-token forbidden failures as token rejection or strategy-recovery UX', async () => {
501+
mockUseAccessToken.mockReturnValue({
502+
token: 'saved-token',
503+
hasToken: true,
504+
saveToken: mockSaveToken,
505+
clearToken: mockClearToken,
506+
isLoading: false,
507+
error: null,
508+
});
509+
mockConvertFeed.mockRejectedValueOnce(
510+
Object.assign(new Error('URL not allowed for this account'), {
511+
manualRetryStrategy: 'browserless',
512+
})
513+
);
514+
515+
render(<App />);
516+
517+
fireEvent.input(screen.getByLabelText('Page URL'), {
518+
target: { value: 'https://example.com/articles' },
519+
});
520+
fireEvent.click(screen.getByRole('button', { name: 'Generate feed URL' }));
521+
522+
await screen.findByText('URL not allowed for this account');
523+
expect(mockClearToken).not.toHaveBeenCalled();
524+
expect(screen.queryByText('Add access token')).not.toBeInTheDocument();
525+
expect(screen.queryByText('Access token was rejected. Paste a valid token to continue.')).not.toBeInTheDocument();
526+
expect(screen.queryByRole('button', { name: /Try .* instead/ })).not.toBeInTheDocument();
527+
});
528+
500529
it('shows the utility links in a user-focused order', () => {
501530
window.history.replaceState({}, '', 'http://localhost:3000/#result');
502531
render(<App />);

frontend/src/components/App.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,40 @@ export function App() {
102102

103103
const isAccessTokenError = (message: string) => {
104104
const normalized = message.toLowerCase();
105+
const mentionsAuthToken =
106+
normalized.includes('access token') ||
107+
normalized.includes('token') ||
108+
normalized.includes('authentication') ||
109+
normalized.includes('bearer');
110+
105111
return (
112+
normalized.includes('unauthorized') ||
113+
normalized.includes('invalid token') ||
114+
normalized.includes('token rejected') ||
115+
normalized.includes('authentication') ||
116+
(normalized.includes('forbidden') && mentionsAuthToken)
117+
);
118+
};
119+
120+
const isActionableStrategySwitch = (
121+
message: string,
122+
currentStrategy: string,
123+
retryStrategy: string
124+
) => {
125+
if (currentStrategy !== 'faraday' || retryStrategy !== 'browserless') return false;
126+
127+
const normalized = message.toLowerCase();
128+
return !(
106129
normalized.includes('unauthorized') ||
107130
normalized.includes('forbidden') ||
131+
normalized.includes('not allowed') ||
132+
normalized.includes('disabled') ||
108133
normalized.includes('access token') ||
109-
normalized.includes('authentication')
134+
normalized.includes('token') ||
135+
normalized.includes('authentication') ||
136+
normalized.includes('bad request') ||
137+
normalized.includes('url') ||
138+
normalized.includes('unsupported strategy')
110139
);
111140
};
112141

@@ -150,7 +179,9 @@ export function App() {
150179
} catch (submitError) {
151180
const message = submitError instanceof Error ? submitError.message : 'Unable to start feed generation.';
152181
const retryStrategy = (submitError as ConversionErrorWithMeta).manualRetryStrategy ?? '';
153-
setManualRetryStrategy(retryStrategy);
182+
setManualRetryStrategy(
183+
isActionableStrategySwitch(message, strategy, retryStrategy) ? retryStrategy : ''
184+
);
154185

155186
if (feedCreation.access_token_required && isAccessTokenError(message)) {
156187
clearToken();

frontend/src/components/AppPanels.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export function CreateFeedPanel({
198198
<input
199199
id="access-token"
200200
name="access-token"
201-
type="password"
201+
type="text"
202202
class="input input--mono input--minimal"
203203
aria-label="Access token"
204204
placeholder="Paste access token"
@@ -251,7 +251,7 @@ export function CreateFeedPanel({
251251
{isConverting && (
252252
<div class="ui-card ui-card--notice ui-card--padded notice" data-state="loading" role="status">
253253
<div class="notice__title">Preparing feed</div>
254-
<p>Creating the feed and loading its preview before showing the result.</p>
254+
<p>Creating the feed now. The result appears first, then preview loading continues.</p>
255255
</div>
256256
)}
257257

0 commit comments

Comments
 (0)