Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 2 additions & 37 deletions server/routes/apps.js
Original file line number Diff line number Diff line change
Expand Up @@ -1112,47 +1112,12 @@ router.get("/:appId/review-submissions/:submissionId", async (req, res) => {
try {
const submissionUrl = `/v1/reviewSubmissions/${submissionId}`
+ "?include=items,appStoreVersionForReview,submittedByActor,lastUpdatedByActor"
+ "&fields[reviewSubmissions]=submittedDate,state,platform"
+ "&fields[reviewSubmissions]=submittedDate,state,platform,items,appStoreVersionForReview,submittedByActor,lastUpdatedByActor"
+ "&fields[reviewSubmissionItems]=state,appStoreVersion"
+ "&fields[appStoreVersions]=versionString,platform,appStoreState"
+ "&fields[actors]=userFirstName,userLastName";

const itemsUrl = `/v1/reviewSubmissions/${submissionId}/items`
+ "?include=appStoreVersion"
+ "&fields[reviewSubmissionItems]=state,appStoreVersion"
+ "&fields[appStoreVersions]=versionString,platform,appStoreState";

const [submissionData, itemsData] = await Promise.all([
ascFetch(account, submissionUrl),
ascFetch(account, itemsUrl),
]);

// Replace item objects with richer ones from items endpoint (they carry appStoreVersion relationship)
if (!submissionData.included) submissionData.included = [];
if (itemsData.data) {
for (const item of itemsData.data) {
const idx = submissionData.included.findIndex((e) => e.type === item.type && e.id === item.id);
if (idx >= 0) submissionData.included[idx] = item;
else submissionData.included.push(item);
}
}
// Merge included version objects from items endpoint
if (itemsData.included) {
for (const inc of itemsData.included) {
const exists = submissionData.included.some((e) => e.type === inc.type && e.id === inc.id);
if (!exists) submissionData.included.push(inc);
}
}

// DEBUG: log raw API responses to understand structure
console.log("=== SUBMISSION DATA ===");
console.log("data.relationships:", JSON.stringify(submissionData.data?.relationships, null, 2));
console.log("included types:", submissionData.included?.map(i => `${i.type}:${i.id}`));
console.log("=== ITEMS DATA ===");
console.log("itemsData.data:", JSON.stringify(itemsData.data, null, 2));
console.log("itemsData.included:", JSON.stringify(itemsData.included, null, 2));
console.log("=== MERGED INCLUDED ===");
console.log("all included:", submissionData.included?.map(i => `${i.type}:${i.id} rels=${Object.keys(i.relationships || {}).join(",")}`));
const submissionData = await ascFetch(account, submissionUrl);

const result = parseReviewSubmissionDetail(submissionData);

Expand Down
7 changes: 7 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ export async function fetchReviewSubmissions(appId, accountId) {
return res.json();
}

export async function fetchReviewSubmissionDetail(appId, submissionId, accountId) {
const params = new URLSearchParams({ accountId });
const res = await fetch(`/api/apps/${appId}/review-submissions/${submissionId}?${params}`);
if (!res.ok) throw new Error(`Failed to fetch review submission detail: ${res.status}`);
return res.json();
}

// ── In-App Purchases ─────────────────────────────────────────────────────────

export async function fetchIAPs(appId, accountId) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/AppDetailPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function StarRating({ rating }) {
return <span className="text-warning tracking-wider">{stars.join("")}</span>;
}

export default function AppDetailPage({ app, accounts, isMobile, onSelectVersion, onViewProducts, onViewXcodeCloud }) {
export default function AppDetailPage({ app, accounts, isMobile, onSelectVersion, onViewProducts, onViewXcodeCloud, onViewReviewDetail }) {
const [lookupData, setLookupData] = useState(null);
const [lookupLoading, setLookupLoading] = useState(true);
const [descExpanded, setDescExpanded] = useState(false);
Expand Down Expand Up @@ -168,7 +168,7 @@ export default function AppDetailPage({ app, accounts, isMobile, onSelectVersion
)}

{/* App Review */}
<AppReviewSection appId={app.id} accountId={app.accountId} />
<AppReviewSection appId={app.id} accountId={app.accountId} onViewDetail={onViewReviewDetail} />

{/* Version History */}
<div>
Expand Down
26 changes: 17 additions & 9 deletions src/components/AppReviewSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ const STATUS_ICONS = {
"Removed": "\u2013",
};

function formatDate(dateString) {
export function formatDate(dateString) {
if (!dateString) return "\u2014";
const d = new Date(dateString);
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}

function ReviewStatus({ status }) {
export function ReviewStatus({ status }) {
const color = STATUS_COLORS[status] || "#8e8e93";
const icon = STATUS_ICONS[status];

Expand All @@ -41,7 +41,7 @@ function ReviewStatus({ status }) {
);
}

function MessagesTable({ messages }) {
function MessagesTable({ messages, onViewDetail }) {
if (messages.length === 0) return null;

return (
Expand All @@ -63,7 +63,11 @@ function MessagesTable({ messages }) {
</thead>
<tbody>
{messages.map((msg) => (
<tr key={msg.id} className="border-b border-dark-border last:border-b-0">
<tr
key={msg.id}
className="border-b border-dark-border last:border-b-0 cursor-pointer hover:bg-dark-hover transition-colors"
onClick={() => onViewDetail(msg.id)}
>
<td className="px-4 py-3 text-[13px] text-accent font-medium whitespace-nowrap">
{formatDate(msg.createdDate)}
</td>
Expand All @@ -82,7 +86,7 @@ function MessagesTable({ messages }) {
);
}

function SubmissionsTable({ submissions }) {
function SubmissionsTable({ submissions, onViewDetail }) {
if (submissions.length === 0) return null;

return (
Expand All @@ -105,7 +109,11 @@ function SubmissionsTable({ submissions }) {
</thead>
<tbody>
{submissions.map((sub) => (
<tr key={sub.id} className="border-b border-dark-border last:border-b-0">
<tr
key={sub.id}
className="border-b border-dark-border last:border-b-0 cursor-pointer hover:bg-dark-hover transition-colors"
onClick={() => onViewDetail(sub.id)}
>
<td className="px-4 py-3 text-[13px] text-accent font-medium whitespace-nowrap">
{formatDate(sub.submittedDate)}
</td>
Expand All @@ -123,7 +131,7 @@ function SubmissionsTable({ submissions }) {
);
}

export default function AppReviewSection({ appId, accountId }) {
export default function AppReviewSection({ appId, accountId, onViewDetail }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
Expand Down Expand Up @@ -180,8 +188,8 @@ export default function AppReviewSection({ appId, accountId }) {
return (
<div className="mb-8">
<h2 className="text-[13px] font-bold text-dark-text uppercase tracking-wide mb-3">App Review</h2>
{hasMessages && <MessagesTable messages={data.messages} />}
{hasSubmissions && <SubmissionsTable submissions={data.submissions} />}
{hasMessages && <MessagesTable messages={data.messages} onViewDetail={onViewDetail} />}
{hasSubmissions && <SubmissionsTable submissions={data.submissions} onViewDetail={onViewDetail} />}
</div>
);
}
50 changes: 50 additions & 0 deletions src/components/AppStoreManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ProductsPage from "./ProductsPage.jsx";
import XcodeCloudPage from "./XcodeCloudPage.jsx";
import BuildDetailPage from "./BuildDetailPage.jsx";
import WorkflowEditPage from "./WorkflowEditPage.jsx";
import ReviewSubmissionDetail from "./ReviewSubmissionDetail.jsx";

function buildGroups(apps, groupBy, accounts) {
if (groupBy === "none") return [{ key: "all", label: null, apps }];
Expand Down Expand Up @@ -49,6 +50,8 @@ function getRouteFromPath() {
if (xcodeCloudBuildMatch) return { appId: xcodeCloudBuildMatch[1], versionId: null, subPage: "xcode-cloud-build", buildId: xcodeCloudBuildMatch[2] };
const xcodeCloudMatch = path.match(/^\/app\/([^/]+)\/xcode-cloud$/);
if (xcodeCloudMatch) return { appId: xcodeCloudMatch[1], versionId: null, subPage: "xcode-cloud" };
const reviewMatch = path.match(/^\/app\/([^/]+)\/review\/([^/]+)$/);
if (reviewMatch) return { appId: reviewMatch[1], versionId: null, subPage: "review", submissionId: reviewMatch[2] };
const appMatch = path.match(/^\/app\/([^/]+)$/);
if (appMatch) return { appId: appMatch[1], versionId: null, subPage: null };
return { appId: null, versionId: null, subPage: null };
Expand Down Expand Up @@ -114,6 +117,7 @@ export default function AppStoreManager() {

const [selectedBuildRun, setSelectedBuildRun] = useState(null);
const [selectedWorkflowId, setSelectedWorkflowId] = useState(null);
const [selectedSubmissionId, setSelectedSubmissionId] = useState(null);

const navigateToWorkflowEdit = useCallback((workflow, app) => {
setSelectedApp(app);
Expand All @@ -140,6 +144,18 @@ export default function AppStoreManager() {
);
}, []);

const navigateToReviewDetail = useCallback((submissionId, app) => {
setSelectedApp(app);
setSelectedVersion(null);
setSelectedSubmissionId(submissionId);
setCurrentView("review");
window.history.pushState(
{ appId: app.id, submissionId, subPage: "review" },
"",
`/app/${app.id}/review/${submissionId}`
);
}, []);

const loadData = useCallback(async (fresh = false) => {
try {
setError(null);
Expand Down Expand Up @@ -187,6 +203,13 @@ export default function AppStoreManager() {
// Build deep-link resolution failed, page will show with available data
}
}
} else if (route.subPage === "review") {
const appMatch = appsList.find((a) => a.id === route.appId);
if (appMatch) {
setSelectedApp(appMatch);
setSelectedSubmissionId(route.submissionId);
setCurrentView("review");
}
} else if (route.subPage === "xcode-cloud") {
const appMatch = appsList.find((a) => a.id === route.appId);
if (appMatch) {
Expand Down Expand Up @@ -279,6 +302,20 @@ export default function AppStoreManager() {
return;
}

if (route.subPage === "review") {
const appMatch = apps.find((a) => a.id === route.appId);
if (appMatch) {
setSelectedApp(appMatch);
setSelectedSubmissionId(route.submissionId);
setCurrentView("review");
} else {
setSelectedApp(null);
setSelectedSubmissionId(null);
setCurrentView(null);
}
return;
}

if (route.subPage === "xcode-cloud") {
const appMatch = apps.find((a) => a.id === route.appId);
setSelectedApp(appMatch || null);
Expand Down Expand Up @@ -337,6 +374,18 @@ export default function AppStoreManager() {
);
}

if (currentView === "review" && selectedApp && selectedSubmissionId) {
return (
<div className="font-sans bg-dark-bg text-dark-text min-h-screen antialiased">
<ReviewSubmissionDetail
app={selectedApp}
submissionId={selectedSubmissionId}
isMobile={isMobile}
/>
</div>
);
}

if (currentView === "xcode-cloud-workflow" && selectedApp && selectedWorkflowId) {
return (
<div className="font-sans bg-dark-bg text-dark-text min-h-screen antialiased">
Expand Down Expand Up @@ -386,6 +435,7 @@ export default function AppStoreManager() {
onSelectVersion={(version) => selectVersion(version, selectedApp)}
onViewProducts={() => navigateToProducts(selectedApp)}
onViewXcodeCloud={() => navigateToXcodeCloud(selectedApp)}
onViewReviewDetail={(submissionId) => navigateToReviewDetail(submissionId, selectedApp)}
/>
</div>
);
Expand Down
Loading
Loading