Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0895df2
feat(billing): add Razorpay subscription service and webhook handler
senutpal Jan 14, 2026
aefe23e
feat(billing): configure Razorpay environment and database schema
senutpal Jan 14, 2026
afe326d
feat(frontend): add billing dashboard page with Razorpay checkout
senutpal Jan 14, 2026
4b1120b
feat(gating): add feature gating infrastructure
senutpal Jan 14, 2026
cc95485
chore: update server and extension dependencies for billing
senutpal Jan 14, 2026
585fd01
chore: update shared exports and page content
senutpal Jan 14, 2026
2d6be1d
chore: update .gitignore for Razorpay config files
senutpal Jan 14, 2026
3a0a2d7
chore: update pnpm-lock.yaml with razorpay dependency
senutpal Jan 14, 2026
c4f90c8
feat(auth): add frontend authentication flow with Razorpay
senutpal Jan 14, 2026
962eb23
feat(header): add magnetic Sign In button
senutpal Jan 14, 2026
7b29a10
fix(auth): add client directive to auth context
senutpal Jan 14, 2026
517a1f8
fix(dashboard): convert to client component and add layout with metadata
senutpal Jan 14, 2026
d9bbd3c
refactor(env): use env vars for all hardcoded domains and URLs
senutpal Jan 14, 2026
eee24c6
fix(razorpay): fix timing attack vulnerability with crypto.timingSafe…
senutpal Jan 14, 2026
720bd6a
fix(billing): add rate limiting and save razorpaySubscriptionId on ve…
senutpal Jan 14, 2026
b781540
fix(webhooks): use userId from subscription notes for database lookup
senutpal Jan 14, 2026
8d79585
refactor(featureGate): import from shared package and add validation
senutpal Jan 14, 2026
578dd10
refactor(extension): deduplicate getWebAppUrl and import from shared
senutpal Jan 14, 2026
9276a54
fix(privacy): fix spelling RazorPay to Razorpay
senutpal Jan 14, 2026
4725f1a
fix(pricing): fix Team tier monthly price to 249
senutpal Jan 14, 2026
a0d5b8f
feat(billing): improve UX with toast notifications and useCallback
senutpal Jan 14, 2026
f0e8f59
infra(server): add fastify-raw-body for webhook signature verification
senutpal Jan 14, 2026
165382b
infra(prisma): suppress dotenv informational output with quiet mode
senutpal Jan 14, 2026
6dbb29f
deps(web): add sonner for toast notifications
senutpal Jan 14, 2026
e61b9a5
feat(config): add webAppUrl to DevRadarConfig
senutpal Jan 14, 2026
1a4fffc
fix(extension): resolve lint warnings in FeatureGatingService
senutpal Jan 14, 2026
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
51 changes: 15 additions & 36 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,36 +1,15 @@
# Node Environment
NODE_ENV=development

# Server Configuration
PORT=3000
HOST=localhost

# Database (PostgreSQL)
DATABASE_URL=postgresql://devradar:devradar@localhost:5432/devradar?schema=public

# Redis
REDIS_URL=redis://localhost:6379

# GitHub OAuth
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_CALLBACK_URL=http://localhost:3000/auth/callback
# GitHub Webhooks (for Boss Battles - achievements from GitHub events)
# Generate with: openssl rand -hex 32
GITHUB_WEBHOOK_SECRET=your_webhook_secret_for_github

# JWT
JWT_SECRET=your_super_secret_jwt_key_change_in_production
JWT_EXPIRES_IN=7d

# WebSocket
WS_PORT=3001

# Logging
LOG_LEVEL=debug

# Slack Integration (Phase 3 - Optional)
# Create a Slack App at https://api.slack.com/apps
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
SLACK_SIGNING_SECRET=
# ============================================
# DevRadar Environment Configuration
# ============================================

# Environment variables are now organized per application:
#
# - Server variables: apps/server/.env.example
# - Web variables: apps/web/.env.example
#
# Copy the appropriate .env.example to .env in each app directory
# and fill in your values before running locally.
#
# For production deployment:
# - Vercel: Add NEXT_PUBLIC_RAZORPAY_KEY_ID in Vercel dashboard
# - Koyeb: Add all vars from apps/server/.env.example in Koyeb dashboard
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ coverage/
docker-compose.override.yml
apps/server/src/generated/

rules/
rules/
docs/
opencode.jsonc
26 changes: 26 additions & 0 deletions apps/extension/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ============================================
# DevRadar VS Code Extension Environment Configuration
# ============================================
# Copy this file to .env and fill in your values

# -----------------------------------------------------------------------------
# DEPLOYMENT: Add these in VS Code Extension settings or .env file
# -----------------------------------------------------------------------------

# Development Server URLs (for local development)
DEV_SERVER_URL=http://localhost:3000
DEV_WS_URL=ws://localhost:3000/ws

# Production Server URLs (for Koyeb deployment)
# Replace with your Koyeb app URL after deployment
PROD_SERVER_URL=https://your-koyeb-app.koyeb.app
PROD_WS_URL=wss://your-koyeb-app.koyeb.app/ws

# Web Application URL (for links from extension to web app)
# Update this to your Vercel deployment URL in production
NEXT_PUBLIC_WEB_APP_URL=http://localhost:3000

# -----------------------------------------------------------------------------
# PRODUCTION VALUES (update these when deploying)
# -----------------------------------------------------------------------------
# NEXT_PUBLIC_WEB_APP_URL=https://devradar.io
Comment on lines +19 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider renaming NEXT_PUBLIC_WEB_APP_URL for clarity.

The NEXT_PUBLIC_ prefix is a Next.js convention for exposing environment variables to the browser. In a VS Code extension context, this naming is misleading. Consider using a more generic name like WEB_APP_URL or DEVRADAR_WEB_URL.

♻️ Suggested rename
 # Web Application URL (for links from extension to web app)
 # Update this to your Vercel deployment URL in production
-NEXT_PUBLIC_WEB_APP_URL=http://localhost:3000
+WEB_APP_URL=http://localhost:3000

 # -----------------------------------------------------------------------------
 # PRODUCTION VALUES (update these when deploying)
 # -----------------------------------------------------------------------------
-# NEXT_PUBLIC_WEB_APP_URL=https://devradar.io
+# WEB_APP_URL=https://devradar.io
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Web Application URL (for links from extension to web app)
# Update this to your Vercel deployment URL in production
NEXT_PUBLIC_WEB_APP_URL=http://localhost:3000
# -----------------------------------------------------------------------------
# PRODUCTION VALUES (update these when deploying)
# -----------------------------------------------------------------------------
# NEXT_PUBLIC_WEB_APP_URL=https://devradar.io
# Web Application URL (for links from extension to web app)
# Update this to your Vercel deployment URL in production
WEB_APP_URL=http://localhost:3000
# -----------------------------------------------------------------------------
# PRODUCTION VALUES (update these when deploying)
# -----------------------------------------------------------------------------
# WEB_APP_URL=https://devradar.io
🤖 Prompt for AI Agents
In `@apps/extension/.env.example` around lines 19 - 26, The env var name
NEXT_PUBLIC_WEB_APP_URL is misleading for an extension (Next.js browser prefix);
rename it to a generic name (e.g., WEB_APP_URL or DEVRADAR_WEB_URL) in
apps/extension/.env.example and update all code references that read
process.env.NEXT_PUBLIC_WEB_APP_URL (search for NEXT_PUBLIC_WEB_APP_URL,
NEXT_PUBLIC_WEB_APP_URL=, and usages in extension code) to the new symbol so
runtime lookups continue to work, and adjust any README/docs or deployment
config that mention the old name.

48 changes: 47 additions & 1 deletion apps/extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as vscode from 'vscode';

import { ActivityTracker } from './services/activityTracker';
import { AuthService } from './services/authService';
import { FeatureGatingService } from './services/featureGatingService';
import { FriendRequestService } from './services/friendRequestService';
import { WebSocketClient } from './services/wsClient';
import { ConfigManager } from './utils/configManager';
Expand Down Expand Up @@ -48,6 +49,8 @@ class DevRadarExtension implements vscode.Disposable {
private readonly statsProvider: StatsProvider;
private readonly leaderboardProvider: LeaderboardProvider;
private statsRefreshInterval: NodeJS.Timeout | null = null;
// Phase 5: Feature Gating
private readonly featureGatingService: FeatureGatingService;

constructor(context: vscode.ExtensionContext) {
this.logger = new Logger('DevRadar');
Expand Down Expand Up @@ -77,6 +80,12 @@ class DevRadarExtension implements vscode.Disposable {
// Phase 2: Gamification views
this.statsProvider = new StatsProvider(this.logger);
this.leaderboardProvider = new LeaderboardProvider(this.logger);
// Phase 5: Feature Gating
this.featureGatingService = new FeatureGatingService(
this.authService,
this.logger,
this.configManager
);
/* Track disposables */
this.disposables.push(
this.authService,
Expand All @@ -89,7 +98,8 @@ class DevRadarExtension implements vscode.Disposable {
this.statusBar,
this.configManager,
this.statsProvider,
this.leaderboardProvider
this.leaderboardProvider,
this.featureGatingService
);
}

Expand Down Expand Up @@ -194,6 +204,14 @@ class DevRadarExtension implements vscode.Disposable {
void vscode.commands.executeCommand('devradar.friendRequests.focus');
},
},
{
id: 'devradar.enableGhostMode',
handler: () => this.handleEnableGhostMode(),
},
{
id: 'devradar.openBilling',
handler: () => this.handleOpenBilling(),
},
];

for (const command of commands) {
Expand Down Expand Up @@ -761,6 +779,34 @@ class DevRadarExtension implements vscode.Disposable {
}
}

private async handleEnableGhostMode(): Promise<void> {
// Check if user has access to ghost mode feature
const hasAccess = await this.featureGatingService.promptUpgrade('ghostMode');
if (!hasAccess) {
return;
}

// Toggle ghost mode
const currentMode = this.configManager.get('privacyMode');
await this.configManager.update('privacyMode', !currentMode);

const message = !currentMode
? 'DevRadar: Ghost mode enabled - you are now invisible to others'
: 'DevRadar: Ghost mode disabled - your activity is now visible';

void vscode.window.showInformationMessage(message);
this.activityTracker.sendStatusUpdate();
}
Comment on lines +782 to +799
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "privacyMode|ghostMode" --type=ts -C2 apps/extension/

Repository: senutpal/devRadar

Length of output: 5655


Use a separate config key for ghostMode or refactor the feature architecture.

The handleEnableGhostMode() handler gates access on the ghostMode feature (PRO tier: "Go completely invisible") but toggles the privacyMode config setting (FREE tier: "Hide activity details"). This creates a semantic mismatch:

  • Both FREE and PRO users toggle the same privacyMode config
  • ghostMode is only checked for feature gating, never for actual behavior
  • The two features have different descriptions and tier levels but share a single config key

Either create a separate ghostMode config key or refactor so PRO users enable a distinct behavior.

🤖 Prompt for AI Agents
In `@apps/extension/src/extension.ts` around lines 778 - 795, The handler
handleEnableGhostMode currently gates on
featureGatingService.promptUpgrade('ghostMode') but toggles
configManager.get/update('privacyMode'), causing a semantic mismatch; create and
use a distinct config key (e.g., 'ghostMode') instead of 'privacyMode' in this
handler: replace configManager.get('privacyMode') and update('privacyMode', ...)
with configManager.get('ghostMode') and update('ghostMode', !currentGhost),
update the user message to reference ghost mode, and audit any other code paths
that read 'privacyMode' to ensure PRO-only ghost behavior is implemented via the
new 'ghostMode' flag (or, if you choose the refactor route, centralize
privacy/ghost behavior behind a feature-aware accessor that returns the
effective mode based on tier and config).


private async handleOpenBilling(): Promise<void> {
const webAppUrl = this.featureGatingService.getWebAppUrl();
const tier = this.featureGatingService.getCurrentTier();
const billingUrl = `${webAppUrl}/dashboard/billing?current=${tier}`;

await vscode.env.openExternal(vscode.Uri.parse(billingUrl));
this.logger.info('Opened billing page', { currentTier: tier });
}

dispose(): void {
this.logger.info('Disposing DevRadar extension...');

Expand Down
143 changes: 143 additions & 0 deletions apps/extension/src/services/featureGatingService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Feature Gating Service
*
* Client-side feature access control for the VS Code extension.
* Checks user tier and prompts for upgrade when accessing gated features.
*/

import {
type Feature,
type SubscriptionTier,
SUBSCRIPTION_FEATURES,
FEATURE_DESCRIPTIONS,
} from '@devradar/shared';
import * as vscode from 'vscode';

import type { AuthService } from './authService';
import type { ConfigManager } from '../utils/configManager';
import type { Logger } from '../utils/logger';

const FEATURE_TIER_MAP: Record<Feature, SubscriptionTier> = {
presence: 'FREE',
friends: 'FREE',
globalLeaderboard: 'FREE',
friendsLeaderboard: 'FREE',
streaks: 'FREE',
achievements: 'FREE',
poke: 'FREE',
privacyMode: 'FREE',
unlimitedFriends: 'PRO',
ghostMode: 'PRO',
customStatus: 'PRO',
history30d: 'PRO',
themes: 'PRO',
customEmoji: 'PRO',
prioritySupport: 'PRO',
conflictRadar: 'TEAM',
teamCreation: 'TEAM',
teamAnalytics: 'TEAM',
slackIntegration: 'TEAM',
privateLeaderboards: 'TEAM',
adminControls: 'TEAM',
ssoSaml: 'TEAM',
dedicatedSupport: 'TEAM',
} as const;
Comment on lines +20 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider deriving FEATURE_TIER_MAP from SUBSCRIPTION_FEATURES to prevent drift.

This map duplicates tier information already present in SUBSCRIPTION_FEATURES. If features or tiers change in the shared package, this map could become stale.

♻️ Optional: Derive from SUBSCRIPTION_FEATURES
import {
  type Feature,
  type SubscriptionTier,
  SUBSCRIPTION_FEATURES,
  FEATURE_DESCRIPTIONS,
} from '@devradar/shared';

const TIER_PRIORITY: SubscriptionTier[] = ['FREE', 'PRO', 'TEAM'];

const FEATURE_TIER_MAP: Record<Feature, SubscriptionTier> = Object.fromEntries(
  TIER_PRIORITY.flatMap((tier) =>
    SUBSCRIPTION_FEATURES[tier].map((feature) => [feature, tier] as const)
  ).reduce((acc, [feature, tier]) => {
    // Keep the lowest tier (first occurrence)
    if (!acc.has(feature)) acc.set(feature, tier);
    return acc;
  }, new Map<Feature, SubscriptionTier>())
) as Record<Feature, SubscriptionTier>;
🤖 Prompt for AI Agents
In `@apps/extension/src/services/featureGatingService.ts` around lines 20 - 44,
FEATURE_TIER_MAP duplicates tier data from SUBSCRIPTION_FEATURES and can drift;
replace the hardcoded FEATURE_TIER_MAP with a derived map built from
SUBSCRIPTION_FEATURES (imported from `@devradar/shared`) by iterating tiers in
priority order (e.g., TIER_PRIORITY = ['FREE','PRO','TEAM']) and mapping each
feature to its first/lowest-tier occurrence so the resulting
Record<Feature,SubscriptionTier> (used wherever FEATURE_TIER_MAP is referenced)
stays in sync with SUBSCRIPTION_FEATURES.


/** Manages feature access control and upgrade prompts. */
export class FeatureGatingService implements vscode.Disposable {
constructor(
private readonly authService: AuthService,
private readonly logger: Logger,
private readonly configManager: ConfigManager
) {}

/**
* Checks if the current user has access to a feature.
* @param feature - The feature to check access for
* @returns true if the user has access
*/
hasAccess(feature: Feature): boolean {
const user = this.authService.getUser();
if (!user) {
return false;
}

// Defensive: ensure tier is valid, default to FREE if not
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime value might differ from type
const tier = (user.tier ?? 'FREE') as SubscriptionTier;
return SUBSCRIPTION_FEATURES[tier].includes(feature);
}
Comment on lines +59 to +69
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add validation for unexpected tier values.

The defensive ?? handles null/undefined, but if user.tier is an unexpected string (e.g., from API version mismatch), SUBSCRIPTION_FEATURES[tier] would be undefined and .includes() would throw a TypeError.

♻️ Suggested defensive approach
   hasAccess(feature: Feature): boolean {
     const user = this.authService.getUser();
     if (!user) {
       return false;
     }

-    // Defensive: ensure tier is valid, default to FREE if not
-    // eslint-disable-next-line `@typescript-eslint/no-unnecessary-condition` -- runtime value might differ from type
-    const tier = (user.tier ?? 'FREE') as SubscriptionTier;
-    return SUBSCRIPTION_FEATURES[tier].includes(feature);
+    // Defensive: ensure tier is valid and exists in SUBSCRIPTION_FEATURES
+    const tier = user.tier as SubscriptionTier | undefined;
+    const features = SUBSCRIPTION_FEATURES[tier ?? 'FREE'];
+    return features?.includes(feature) ?? false;
   }
🤖 Prompt for AI Agents
In `@apps/extension/src/services/featureGatingService.ts` around lines 59 - 69, In
hasAccess, guard against unexpected user.tier strings before indexing
SUBSCRIPTION_FEATURES: retrieve user via this.authService.getUser(), normalize
tierCandidate = (user.tier ?? 'FREE') as string, check if tierCandidate is a key
in SUBSCRIPTION_FEATURES (e.g.,
Object.prototype.hasOwnProperty.call(SUBSCRIPTION_FEATURES, tierCandidate)); if
not, fall back to 'FREE' (or return false), then cast to SubscriptionTier and
safely call SUBSCRIPTION_FEATURES[tier].includes(feature); update code to remove
the eslint disable and ensure the runtime check prevents
SUBSCRIPTION_FEATURES[tier] from being undefined.


/**
* Gets the minimum tier required for a feature.
* @param feature - The feature to check
* @returns The minimum tier required
*/
getRequiredTier(feature: Feature): SubscriptionTier {
return FEATURE_TIER_MAP[feature];
}

/**
* Gets the user's current tier.
* @returns The user's tier or 'FREE' if not authenticated
*/
getCurrentTier(): SubscriptionTier {
const user = this.authService.getUser();
return (user?.tier ?? 'FREE') as SubscriptionTier;
}

/**
* Prompts the user to upgrade if they don't have access to a feature.
* Opens the billing page in the browser with upgrade parameters.
*
* @param feature - The feature requiring upgrade
* @returns true if the user has access, false if they need to upgrade
*/
async promptUpgrade(feature: Feature): Promise<boolean> {
if (this.hasAccess(feature)) {
return true;
}

const requiredTier = this.getRequiredTier(feature);
const featureDescription = FEATURE_DESCRIPTIONS[feature];

const action = await vscode.window.showWarningMessage(
`DevRadar: "${featureDescription}" requires ${requiredTier} tier.`,
'Upgrade Now',
'Maybe Later'
);

if (action === 'Upgrade Now') {
const webAppUrl = this.getWebAppUrl();
const upgradeUrl = `${webAppUrl}/dashboard/billing?upgrade=${requiredTier}&feature=${feature}`;

await vscode.env.openExternal(vscode.Uri.parse(upgradeUrl));
this.logger.info('Opened upgrade page', { feature, requiredTier });
}

return false;
}
Comment on lines +96 to +119
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider URL-encoding query parameters.

While current Feature and SubscriptionTier values are safe alphanumeric strings, URL-encoding the parameters is best practice and guards against future feature names with special characters.

♻️ Optional fix
     if (action === 'Upgrade Now') {
       const webAppUrl = this.getWebAppUrl();
-      const upgradeUrl = `${webAppUrl}/dashboard/billing?upgrade=${requiredTier}&feature=${feature}`;
+      const upgradeUrl = `${webAppUrl}/dashboard/billing?upgrade=${encodeURIComponent(requiredTier)}&feature=${encodeURIComponent(feature)}`;

       await vscode.env.openExternal(vscode.Uri.parse(upgradeUrl));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async promptUpgrade(feature: Feature): Promise<boolean> {
if (this.hasAccess(feature)) {
return true;
}
const requiredTier = this.getRequiredTier(feature);
const featureDescription = FEATURE_DESCRIPTIONS[feature];
const action = await vscode.window.showWarningMessage(
`DevRadar: "${featureDescription}" requires ${requiredTier} tier.`,
'Upgrade Now',
'Maybe Later'
);
if (action === 'Upgrade Now') {
const webAppUrl = this.getWebAppUrl();
const upgradeUrl = `${webAppUrl}/dashboard/billing?upgrade=${requiredTier}&feature=${feature}`;
await vscode.env.openExternal(vscode.Uri.parse(upgradeUrl));
this.logger.info('Opened upgrade page', { feature, requiredTier });
}
return false;
}
async promptUpgrade(feature: Feature): Promise<boolean> {
if (this.hasAccess(feature)) {
return true;
}
const requiredTier = this.getRequiredTier(feature);
const featureDescription = FEATURE_DESCRIPTIONS[feature];
const action = await vscode.window.showWarningMessage(
`DevRadar: "${featureDescription}" requires ${requiredTier} tier.`,
'Upgrade Now',
'Maybe Later'
);
if (action === 'Upgrade Now') {
const webAppUrl = this.getWebAppUrl();
const upgradeUrl = `${webAppUrl}/dashboard/billing?upgrade=${encodeURIComponent(requiredTier)}&feature=${encodeURIComponent(feature)}`;
await vscode.env.openExternal(vscode.Uri.parse(upgradeUrl));
this.logger.info('Opened upgrade page', { feature, requiredTier });
}
return false;
}
🤖 Prompt for AI Agents
In `@apps/extension/src/services/featureGatingService.ts` around lines 96 - 119,
In promptUpgrade, when building upgradeUrl use URL-encoding for the query
values: replace the raw interpolation of feature and requiredTier with encoded
values (e.g., via encodeURIComponent or URLSearchParams) so upgradeUrl is
constructed from getWebAppUrl() plus a safely encoded query like
?upgrade=<encodedRequiredTier>&feature=<encodedFeature>; update references to
feature and requiredTier in that construction and keep the rest of the method
(openExternal, logger.info) unchanged.


/**
* Gets the upgrade URL for a specific tier.
* @param tier - The target tier
* @returns The full upgrade URL
*/
getUpgradeUrl(tier: SubscriptionTier): string {
const webAppUrl = this.getWebAppUrl();
return `${webAppUrl}/dashboard/billing?upgrade=${tier}`;
}

/**
* Gets the web app URL from config or uses default.
* @returns The web application URL
*/
getWebAppUrl(): string {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- config.get() can return undefined
return this.configManager.get('webAppUrl') ?? 'http://localhost:3000';
}
Comment on lines 135 to 138
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unnecessary nullish coalescing to fix pipeline failure.

configManager.get('webAppUrl') returns string (not string | undefined) because DevRadarConfig.webAppUrl is typed as string. The fallback in loadConfig() already ensures a default value exists.

🔧 Proposed fix
  getWebAppUrl(): string {
-   return this.configManager.get('webAppUrl') ?? 'http://localhost:3000';
+   return this.configManager.get('webAppUrl');
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getWebAppUrl(): string {
return this.configManager.get('webAppUrl') ?? 'http://localhost:3000';
}
getWebAppUrl(): string {
return this.configManager.get('webAppUrl');
}
🧰 Tools
🪛 GitHub Actions: CI

[error] 136-136: ESLint: '@typescript-eslint/no-unnecessary-condition' - Unnecessary conditional, expected left-hand side of '??' operator to be possibly null or undefined

🪛 GitHub Check: Build and Test

[failure] 136-136:
Unnecessary conditional, expected left-hand side of ?? operator to be possibly null or undefined

🤖 Prompt for AI Agents
In `@apps/extension/src/services/featureGatingService.ts` around lines 135 - 137,
Remove the unnecessary nullish coalescing in getWebAppUrl: update the
getWebAppUrl() method to return the value directly from
this.configManager.get('webAppUrl') since DevRadarConfig.webAppUrl is typed as
string and loadConfig() already provides a default; locate the getWebAppUrl
method in featureGatingService.ts and eliminate the '?? "http://localhost:3000"'
fallback so the method simply returns this.configManager.get('webAppUrl').


dispose(): void {
// No disposables to clean up
}
}
1 change: 1 addition & 0 deletions apps/extension/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { AuthService, type AuthState } from './authService';
export { WebSocketClient, type ConnectionState } from './wsClient';
export { ActivityTracker } from './activityTracker';
export { FriendRequestService } from './friendRequestService';
export { FeatureGatingService } from './featureGatingService';
4 changes: 4 additions & 0 deletions apps/extension/src/utils/configManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as vscode from 'vscode';
export interface DevRadarConfig {
serverUrl: string;
wsUrl: string;
webAppUrl: string;
privacyMode: boolean;
showFileName: boolean;
showProject: boolean;
Expand All @@ -27,10 +28,12 @@ const DEFAULT_CONFIG: DevRadarConfig = {
/* Production */
serverUrl: 'https://wispy-netti-devradar-c95bfbd3.koyeb.app',
wsUrl: 'wss://wispy-netti-devradar-c95bfbd3.koyeb.app/ws',
webAppUrl: 'https://devradar.dev',

/* Development */
// serverUrl: 'http://localhost:3000',
// wsUrl: 'ws://localhost:3000/ws',
// webAppUrl: 'http://localhost:3000',
privacyMode: false,
showFileName: true,
showProject: true,
Expand Down Expand Up @@ -96,6 +99,7 @@ export class ConfigManager implements vscode.Disposable {
return {
serverUrl: config.get<string>('serverUrl') ?? DEFAULT_CONFIG.serverUrl,
wsUrl: config.get<string>('wsUrl') ?? DEFAULT_CONFIG.wsUrl,
webAppUrl: config.get<string>('webAppUrl') ?? DEFAULT_CONFIG.webAppUrl,
privacyMode: config.get<boolean>('privacyMode') ?? DEFAULT_CONFIG.privacyMode,
showFileName: config.get<boolean>('showFileName') ?? DEFAULT_CONFIG.showFileName,
showProject: config.get<boolean>('showProject') ?? DEFAULT_CONFIG.showProject,
Expand Down
Loading
Loading