diff --git a/src/apps/opportunities/README.md b/src/apps/opportunities/README.md
index 4b25c7221..e04f02be2 100644
--- a/src/apps/opportunities/README.md
+++ b/src/apps/opportunities/README.md
@@ -695,8 +695,11 @@ challenge track, while authored external-URL rows omit that file action. The
browser requests a short-lived URL from
`GET /v6/submissions/:submissionId/download-url` instead of using submission
list URLs; Review API remains authoritative for access and signs only clean
-storage, so pending DMZ and quarantined files cannot be downloaded. Work
-Manager's case-insensitive `submission_type=url` challenge
+storage, so pending DMZ and quarantined files cannot be downloaded. TopGear
+challenges use the URL experience whenever the app is served on the TopGear host
+or the challenge belongs to the configured TopGear group, including when opened
+through `?tab=mine`. This takes precedence over missing or ZIP submission metadata.
+For other challenges, Work Manager's case-insensitive `submission_type=url`
metadata selects the URL experience; every other value, including missing or
malformed metadata, retains the standard ZIP experience. URL submissions
reuse community-app's Topgear flow and exact Wipro instructions, submission
diff --git a/src/apps/opportunities/src/components/ChallengeSubmissionUpload.spec.tsx b/src/apps/opportunities/src/components/ChallengeSubmissionUpload.spec.tsx
index b0be02056..84b79c8be 100644
--- a/src/apps/opportunities/src/components/ChallengeSubmissionUpload.spec.tsx
+++ b/src/apps/opportunities/src/components/ChallengeSubmissionUpload.spec.tsx
@@ -25,6 +25,7 @@ import {
const mockRecordAnalyticsEvent = jest.fn()
jest.mock('~/config', () => ({
+ AppSubdomain: { topgear: 'topgear' },
EnvironmentConfig: {
TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS: [
'wipro365.sharepoint.com',
diff --git a/src/apps/opportunities/src/pages/ChallengeDetailsPage.topgear-submission.spec.tsx b/src/apps/opportunities/src/pages/ChallengeDetailsPage.topgear-submission.spec.tsx
new file mode 100644
index 000000000..037627366
--- /dev/null
+++ b/src/apps/opportunities/src/pages/ChallengeDetailsPage.topgear-submission.spec.tsx
@@ -0,0 +1,149 @@
+/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */
+import '@testing-library/jest-dom'
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
+import { MemoryRouter, Route, Routes } from 'react-router-dom'
+import { SWRConfig } from 'swr'
+
+import { ChallengeOpportunity } from '../models'
+import {
+ createChallengeSubmission,
+ createChallengeUrlSubmission,
+ getChallengeOpportunity,
+ getChallengeRegistration,
+ getChallengeSubmissions,
+} from '../services'
+
+import { ChallengeDetailsPage } from './ChallengeDetailsPage'
+
+let mockSubdomain = 'www'
+
+jest.mock('~/config', () => ({
+ AppSubdomain: { topgear: 'topgear' },
+ EnvironmentConfig: {
+ REVIEW_APP_URL: 'https://review.topcoder-dev.com',
+ get SUBDOMAIN(): string { return mockSubdomain },
+ TOPGEAR: { GROUP_ID: 'topgear-group' },
+ TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS: ['wipro365.sharepoint.com'],
+ URLS: {
+ TERMS_OF_USE: 'https://www.topcoder-dev.com/terms',
+ TOPGEAR_TERMS: 'https://topgear.topcoder-dev.com/challenges/terms/detail/topgear-terms',
+ },
+ },
+}), { virtual: true })
+jest.mock('~/libs/core', () => ({
+ recordAnalyticsEvent: jest.fn(),
+ useProfileContext: () => ({ initialized: true, profile: { handle: 'coder', userId: 123 } }),
+}), { virtual: true })
+jest.mock('~/libs/cms', () => ({}), { virtual: true })
+jest.mock('~/libs/ui', () => {
+ /** Renders a placeholder icon without loading the shared UI library; never throws. */
+ const Icon = (): JSX.Element =>
+ return {
+ ConfirmModal: (): JSX.Element => <>>,
+ IconOutline: new Proxy({}, { get: () => Icon }),
+ LoadingSpinner: (): JSX.Element => Loading,
+ }
+}, { virtual: true })
+jest.mock('react-toastify', () => ({
+ toast: { error: jest.fn(), success: jest.fn() },
+}))
+jest.mock('../components', () => ({
+ ChallengeDescription: (): JSX.Element =>
Requirements
,
+ ChallengeDetailHeader: jest.requireActual('../components/ChallengeDetailHeader').ChallengeDetailHeader,
+ ChallengeSidebar: (): JSX.Element => ,
+ ChallengeSubmissionUpload: jest.requireActual('../components/ChallengeSubmissionUpload').ChallengeSubmissionUpload,
+ ChallengeTermsModal: (): JSX.Element => <>>,
+ extractTableOfContents: (): [] => [],
+ isHtmlDescriptionFormat: (): boolean => false,
+ OpportunityTabLoading: (): JSX.Element => Loading submissions,
+ ReportIssueModal: (): JSX.Element => <>>,
+}))
+jest.mock('../services', () => ({
+ createChallengeSubmission: jest.fn(),
+ createChallengeUrlSubmission: jest.fn(),
+ getChallengeAiReviewConfig: jest.fn(),
+ getChallengeForumTopics: jest.fn(),
+ getChallengeMemberResource: jest.fn(),
+ getChallengeOpportunity: jest.fn(),
+ getChallengeRegistration: jest.fn(),
+ getChallengeSubmissions: jest.fn(),
+}))
+
+const mockGetChallenge = getChallengeOpportunity as jest.MockedFunction
+const mockGetRegistration = getChallengeRegistration as jest.MockedFunction
+const mockGetSubmissions = getChallengeSubmissions as jest.MockedFunction
+const mockCreateUrlSubmission = createChallengeUrlSubmission as jest.MockedFunction
+
+describe('Topgear submissions from the My Submissions deep link', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ jest.spyOn(window, 'scrollTo')
+ .mockImplementation(() => undefined)
+ mockCreateUrlSubmission.mockResolvedValue({ id: 'url-submission-id' })
+ mockGetRegistration.mockResolvedValue({ id: 'registration-id', memberId: '123' })
+ mockGetSubmissions.mockResolvedValue({ items: [], page: 1, perPage: 10, total: 0, totalPages: 0 })
+ })
+
+ afterEach(() => jest.restoreAllMocks())
+
+ it.each([
+ { groups: [], metadata: undefined, subdomain: 'topgear' },
+ { groups: ['topgear-group'], metadata: undefined, subdomain: 'www' },
+ { groups: [], metadata: [{ name: 'submission_type', value: 'zip' }], subdomain: 'topgear' },
+ { groups: ['topgear-group'], metadata: [{ name: 'submission_type', value: 'zip' }], subdomain: 'www' },
+ ])('submits a URL for a Topgear challenge: %j', async ({ groups, metadata, subdomain }: {
+ groups: string[]
+ metadata: ChallengeOpportunity['metadata']
+ subdomain: string
+ }) => {
+ mockSubdomain = subdomain
+ mockGetChallenge.mockResolvedValue({
+ currentPhaseNames: ['Submission'],
+ groups,
+ id: 'challenge-id',
+ metadata,
+ name: 'Topgear challenge',
+ phases: [{ isOpen: true, name: 'Submission' }],
+ status: 'ACTIVE',
+ track: 'Development',
+ type: 'Challenge',
+ })
+
+ render(
+ new Map() }}>
+
+
+ } path='/opportunities/challenge/:challengeId' />
+
+
+ ,
+ )
+
+ await screen.findByText('You have no submissions yet')
+ expect(screen.getByRole('tab', { name: /^My Submissions/ }))
+ .toHaveAttribute('aria-selected', 'true')
+ fireEvent.click(within(screen.getByRole('tabpanel'))
+ .getByRole('button', { name: 'Submit a solution' }))
+
+ const urlInput = await screen.findByLabelText(/Submission URL/)
+ expect(screen.queryByLabelText(/Upload File/)).not.toBeInTheDocument()
+ expect(screen.getByRole('link', { name: 'TopGear terms and conditions' }))
+ .toHaveAttribute('href', 'https://topgear.topcoder-dev.com/challenges/terms/detail/topgear-terms')
+ fireEvent.change(urlInput, { target: { value: 'https://wipro365.sharepoint.com/sites/project/deliverable' } })
+ fireEvent.click(screen.getByRole('button', { name: 'Set URL' }))
+ fireEvent.click(screen.getByRole('checkbox', { name: 'I understand and agree' }))
+ fireEvent.click(screen.getByRole('button', { name: 'Submit' }))
+
+ await waitFor(() => expect(createChallengeUrlSubmission)
+ .toHaveBeenCalledWith(
+ 'challenge-id',
+ '123',
+ 'CONTEST_SUBMISSION',
+ 'https://wipro365.sharepoint.com/sites/project/deliverable',
+ expect.any(AbortSignal),
+ ))
+ expect(createChallengeSubmission).not.toHaveBeenCalled()
+ expect(await screen.findByText('url-submission-id'))
+ .toBeInTheDocument()
+ })
+})
diff --git a/src/apps/opportunities/src/utils/challenge-detail.utils.spec.ts b/src/apps/opportunities/src/utils/challenge-detail.utils.spec.ts
index c147fdb59..70b2c0446 100644
--- a/src/apps/opportunities/src/utils/challenge-detail.utils.spec.ts
+++ b/src/apps/opportunities/src/utils/challenge-detail.utils.spec.ts
@@ -13,6 +13,7 @@ import {
} from './challenge-detail.utils'
jest.mock('~/config', () => ({
+ AppSubdomain: { topgear: 'topgear' },
EnvironmentConfig: {
ADMIN: { ONLINE_REVIEW_URL: 'https://software.topcoder-dev.com/review' },
REVIEW_APP_URL: 'https://review.topcoder-dev.com',
diff --git a/src/apps/opportunities/src/utils/challenge-detail.utils.ts b/src/apps/opportunities/src/utils/challenge-detail.utils.ts
index 09e82626d..c39d888cc 100644
--- a/src/apps/opportunities/src/utils/challenge-detail.utils.ts
+++ b/src/apps/opportunities/src/utils/challenge-detail.utils.ts
@@ -6,12 +6,14 @@ import {
ChallengeOpportunity,
} from '../models'
+import { isTopgearChallenge } from './topgear.utils'
+
export interface ChallengeSidebarLink {
label: string
url: string
}
-/** Submission experiences selected by Work Manager challenge metadata. */
+/** Submission experiences selected by TopGear context or Work Manager metadata. */
export type ChallengeSubmissionMode = 'url' | 'zip'
/**
@@ -93,15 +95,17 @@ export function challengeMetadataValue(
/**
* Resolves whether a challenge accepts a URL or the standard ZIP archive.
*
- * Work Manager persists this choice as `submission_type` metadata. URL mode
- * is deliberately opt-in: missing, malformed, and unsupported values retain
- * the established ZIP flow.
+ * TopGear challenges always use the URL flow, identified by the community host
+ * or configured challenge group. Other challenges opt in through Work Manager's
+ * `submission_type=url` metadata; all other values retain the ZIP flow.
*
* @param challenge raw Challenge API detail response.
- * @returns `url` only for an exact case-insensitive metadata value; otherwise `zip`.
+ * @returns `url` for TopGear or an exact case-insensitive metadata value; otherwise `zip`.
* @throws Does not throw.
*/
export function challengeSubmissionMode(challenge: ChallengeOpportunity): ChallengeSubmissionMode {
+ if (isTopgearChallenge(challenge)) return 'url'
+
const value = challengeMetadataValue(challenge.metadata, 'submission_type')
return typeof value === 'string' && value.trim()
.toLowerCase() === 'url'