Pārlūkot izejas kodu

Fix Indeed tab home button to load Indeed homepage instead of exiting browse mode

The house icon in the embedded Indeed browser toolbar previously dismissed the
web view and cleared the Indeed sidebar selection, sending users back to the
app Home screen. It now navigates the current page to https://www.indeed.com/
(the same default URL used when opening Indeed from the sidebar), so browsing
stays in context while users can reset to the Indeed homepage.

Implementation:
- Replace onDismissEmbedded callback with homeURL on IndeedJobBrowserViewController
- Wire goHome() to loadPage(homeURL) instead of notifying the dashboard host
- Set vc.homeURL = indeedBrowseHomeURL when presenting the embedded browser
- Update toolbar tooltip/accessibility copy to "Go to Indeed home" across locales

Release 1.1 (build 5):
- Bump MARKETING_VERSION to 1.1 and CURRENT_PROJECT_VERSION to 5
- Add whats-new.txt for App Store Connect release notes
- Extend update_app_store_metadata.py to upload whatsNew, create missing
  app store versions/localizations (APP_STORE_CREATE_VERSION), and prefer
  editable appInfo states for MAC_OS metadata updates

Co-authored-by: Cursor <cursoragent@cursor.com>
Uzair Tahir 1 mēnesi atpakaļ
vecāks
revīzija
ca42a7f582

+ 4 - 4
App for Indeed.xcodeproj/project.pbxproj

@@ -269,7 +269,7 @@
 				CODE_SIGN_ENTITLEMENTS = "App for Indeed/App for Indeed.entitlements";
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
-				CURRENT_PROJECT_VERSION = 3;
+				CURRENT_PROJECT_VERSION = 5;
 				DEVELOPMENT_TEAM = NNC7V99779;
 				ENABLE_APP_SANDBOX = YES;
 				ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
@@ -287,7 +287,7 @@
 					"$(inherited)",
 					"@executable_path/../Frameworks",
 				);
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = 1.1;
 				PRODUCT_BUNDLE_IDENTIFIER = "com.hwaccount.app-for-indeed";
 				PRODUCT_NAME = "App for Indeed";
 				REGISTER_APP_GROUPS = YES;
@@ -309,7 +309,7 @@
 				CODE_SIGN_ENTITLEMENTS = "App for Indeed/App for Indeed.entitlements";
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
-				CURRENT_PROJECT_VERSION = 3;
+				CURRENT_PROJECT_VERSION = 5;
 				DEVELOPMENT_TEAM = NNC7V99779;
 				ENABLE_APP_SANDBOX = YES;
 				ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
@@ -327,7 +327,7 @@
 					"$(inherited)",
 					"@executable_path/../Frameworks",
 				);
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = 1.1;
 				PRODUCT_BUNDLE_IDENTIFIER = "com.hwaccount.app-for-indeed";
 				PRODUCT_NAME = "App for Indeed";
 				REGISTER_APP_GROUPS = YES;

+ 18 - 17
App for Indeed/Controllers/IndeedJobBrowserWindowController.swift

@@ -11,8 +11,8 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
     /// Shared pool so Indeed / Cloudflare cookies persist across embedded browser sessions.
     private static let sharedProcessPool = WKProcessPool()
 
-    /// When set, a leading **Home** control calls this so the host can hide the embedded browser (same-window UX).
-    var onDismissEmbedded: (() -> Void)?
+    /// When set, the leading **Home** toolbar control loads this URL (e.g. Indeed homepage).
+    var homeURL: URL?
     private let webView: WKWebView = {
         let configuration = WKWebViewConfiguration()
         configuration.processPool = IndeedJobBrowserViewController.sharedProcessPool
@@ -31,7 +31,7 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
     private let backButton = NSButton()
     private let forwardButton = NSButton()
     private let reloadButton = NSButton()
-    private let dismissEmbeddedButton = NSButton()
+    private let homeButton = NSButton()
     private let toolbarContainer = NSView()
     private var appearanceObserver: NSObjectProtocol?
     private var languageObserver: NSObjectProtocol?
@@ -51,18 +51,18 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
         configureSymbolToolbarButton(forwardButton, symbolName: "chevron.forward", action: #selector(goForward))
         configureSymbolToolbarButton(reloadButton, symbolName: "arrow.clockwise", action: #selector(reload))
         configureSymbolToolbarButton(
-            dismissEmbeddedButton,
+            homeButton,
             symbolName: "house.fill",
-            action: #selector(dismissEmbedded),
-            toolTipKey: "Return to the previous screen"
+            action: #selector(goHome),
+            toolTipKey: "Go to Indeed home"
         )
 
         toolbarContainer.translatesAutoresizingMaskIntoConstraints = false
         toolbarContainer.wantsLayer = true
 
         let barStack: NSStackView
-        if onDismissEmbedded != nil {
-            barStack = NSStackView(views: [dismissEmbeddedButton, backButton, forwardButton, reloadButton, NSView()])
+        if homeURL != nil {
+            barStack = NSStackView(views: [homeButton, backButton, forwardButton, reloadButton, NSView()])
         } else {
             barStack = NSStackView(views: [backButton, forwardButton, reloadButton, NSView()])
         }
@@ -98,10 +98,10 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
             webView.topAnchor.constraint(equalTo: toolbarContainer.bottomAnchor),
             webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
         ]
-        if onDismissEmbedded != nil {
+        if homeURL != nil {
             layoutConstraints.append(contentsOf: [
-                dismissEmbeddedButton.widthAnchor.constraint(equalToConstant: 32),
-                dismissEmbeddedButton.heightAnchor.constraint(equalToConstant: 28)
+                homeButton.widthAnchor.constraint(equalToConstant: 32),
+                homeButton.heightAnchor.constraint(equalToConstant: 28)
             ])
         }
         NSLayoutConstraint.activate(layoutConstraints)
@@ -197,17 +197,17 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
     private func applyCurrentAppearance() {
         toolbarContainer.layer?.backgroundColor = AppDashboardTheme.chromeBackground.cgColor
         let labelColor = AppDashboardTheme.primaryText
-        dismissEmbeddedButton.contentTintColor = labelColor
+        homeButton.contentTintColor = labelColor
         reloadButton.contentTintColor = labelColor
         updateNavigationButtons()
     }
 
     private func applyLocalizedStrings() {
-        dismissEmbeddedButton.toolTip = L("Return to the previous screen")
+        homeButton.toolTip = L("Go to Indeed home")
         updateSymbolToolbarButtonImage(
-            dismissEmbeddedButton,
+            homeButton,
             symbolName: "house.fill",
-            accessibilityLabelKey: "Return to the previous screen"
+            accessibilityLabelKey: "Go to Indeed home"
         )
         updateSymbolToolbarButtonImage(backButton, symbolName: "chevron.backward")
         updateSymbolToolbarButtonImage(forwardButton, symbolName: "chevron.forward")
@@ -238,8 +238,9 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
         webView.reload()
     }
 
-    @objc private func dismissEmbedded() {
-        onDismissEmbedded?()
+    @objc private func goHome() {
+        guard let homeURL else { return }
+        loadPage(homeURL)
     }
 
     func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {

+ 1 - 3
App for Indeed/Views/DashboardView.swift

@@ -1303,9 +1303,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
 
         if indeedJobBrowserViewController == nil {
             let vc = IndeedJobBrowserViewController()
-            vc.onDismissEmbedded = { [weak self] in
-                self?.dismissIndeedJobBrowserEmbedded()
-            }
+            vc.homeURL = Self.indeedBrowseHomeURL
             vc.embed(in: indeedJobBrowserHost, parent: parentVC)
             indeedJobBrowserViewController = vc
         }

+ 1 - 1
App for Indeed/ar.lproj/Localizable.strings

@@ -342,7 +342,7 @@
 "IMPACT" = "التأثير";
 
 // MARK: - متصفح الوظائف
-"Return to the previous screen" = "العودة إلى الشاشة السابقة";
+"Go to Indeed home" = "الانتقال إلى الصفحة الرئيسية لـ Indeed";
 
 // MARK: - الأخطاء
 "We couldn't reach the server. Check your internet connection and try again." = "لم نتمكن من الوصول إلى الخادم. تحقق من اتصالك بالإنترنت وحاول مرة أخرى.";

+ 1 - 1
App for Indeed/de.lproj/Localizable.strings

@@ -342,7 +342,7 @@
 "IMPACT" = "WIRKUNG";
 
 // MARK: - Job-Browser
-"Return to the previous screen" = "Zurück zum vorherigen Bildschirm";
+"Go to Indeed home" = "Zur Indeed-Startseite";
 
 // MARK: - Fehler
 "We couldn't reach the server. Check your internet connection and try again." = "Wir konnten den Server nicht erreichen. Überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.";

+ 1 - 1
App for Indeed/en.lproj/Localizable.strings

@@ -320,7 +320,7 @@
 "IMPACT" = "IMPACT";
 
 // MARK: - Job Browser
-"Return to the previous screen" = "Return to the previous screen";
+"Go to Indeed home" = "Go to Indeed home";
 
 // MARK: - Errors
 "We couldn't reach the server. Check your internet connection and try again." = "We couldn't reach the server. Check your internet connection and try again.";

+ 1 - 1
App for Indeed/fr-CA.lproj/Localizable.strings

@@ -342,7 +342,7 @@
 "IMPACT" = "RÉALISATIONS";
 
 // MARK: - Navigateur d'emplois
-"Return to the previous screen" = "Revenir à l'écran précédent";
+"Go to Indeed home" = "Aller à l'accueil Indeed";
 
 // MARK: - Erreurs
 "We couldn't reach the server. Check your internet connection and try again." = "Nous ne pouvons pas joindre le serveur. Vérifiez votre connexion Internet et réessayez.";

+ 1 - 1
App for Indeed/fr.lproj/Localizable.strings

@@ -342,7 +342,7 @@
 "IMPACT" = "RÉALISATIONS";
 
 // MARK: - Navigateur d'emplois
-"Return to the previous screen" = "Revenir à l'écran précédent";
+"Go to Indeed home" = "Aller à l'accueil Indeed";
 
 // MARK: - Erreurs
 "We couldn't reach the server. Check your internet connection and try again." = "Nous ne pouvons pas joindre le serveur. Vérifiez votre connexion Internet et réessayez.";

+ 1 - 1
App for Indeed/sv.lproj/Localizable.strings

@@ -342,7 +342,7 @@
 "IMPACT" = "PÅVERKAN";
 
 // MARK: - Jobbläsare
-"Return to the previous screen" = "Återgå till föregående skärm";
+"Go to Indeed home" = "Gå till Indeed-startsidan";
 
 // MARK: - Fel
 "We couldn't reach the server. Check your internet connection and try again." = "Vi kunde inte nå servern. Kontrollera din internetanslutning och försök igen.";

+ 1 - 1
App for Indeed/zh-Hans.lproj/Localizable.strings

@@ -343,7 +343,7 @@
 "IMPACT" = "影响力";
 
 // MARK: - 工作浏览器
-"Return to the previous screen" = "返回上一屏幕";
+"Go to Indeed home" = "前往 Indeed 首页";
 
 // MARK: - 错误
 "We couldn't reach the server. Check your internet connection and try again." = "无法连接服务器。请检查您的网络连接后重试。";

+ 1 - 1
App for Indeed/zh-Hant.lproj/Localizable.strings

@@ -342,7 +342,7 @@
 "IMPACT" = "影響力";
 
 // MARK: - 工作瀏覽器
-"Return to the previous screen" = "返回上一畫面";
+"Go to Indeed home" = "前往 Indeed 首頁";
 
 // MARK: - 錯誤
 "We couldn't reach the server. Check your internet connection and try again." = "無法連線到伺服器。請檢查您的網路連線後再試一次。";

+ 6 - 2
app-connect/README.md

@@ -47,9 +47,13 @@ python3 scripts/update_app_store_metadata.py
 ```
 
 - **App Information** (macOS): name, subtitle, privacy policy URL  
-- **Version 1.0** (default): copyright, description, keywords, promotional text, marketing URL, support URL  
+- **Version metadata** (default `1.0`): copyright, description, keywords, promotional text, what’s new, marketing URL, support URL  
 
-Override version or locale: `APP_STORE_VERSION=1.0 APP_STORE_LOCALE=en-US python3 scripts/update_app_store_metadata.py`
+Override version or locale: `APP_STORE_VERSION=1.1 APP_STORE_LOCALE=en-US python3 scripts/update_app_store_metadata.py`
+
+Create the version in App Store Connect if it does not exist yet:
+
+`APP_STORE_VERSION=1.1 APP_STORE_CREATE_VERSION=1 python3 scripts/update_app_store_metadata.py`
 
 Custom Terms of Use (EULA URL) must still use Apple’s standard EULA or be set manually in App Store Connect if you need a custom license agreement.
 

+ 1 - 0
app-connect/app-data/README.md

@@ -14,6 +14,7 @@ Copy-ready text for **App for Indeed** (macOS). Each file maps to a field in App
 | `link-support.txt` | Support URL | — |
 | `link-privacy.txt` | Privacy Policy URL | — |
 | `link-terms.txt` | License Agreement / Terms URL (EULA) | — |
+| `whats-new.txt` | What’s New in This Version | 4,000 characters |
 
 URLs match `AppLegalURLs.swift` and `Paywall.storekit` in the Xcode project.
 

+ 2 - 0
app-connect/app-data/whats-new.txt

@@ -0,0 +1,2 @@
+Whats New 1.1
+Bug Fixes and Updates

+ 65 - 17
app-connect/scripts/update_app_store_metadata.py

@@ -97,6 +97,21 @@ class ASCClient:
             )
         return response.json()
 
+    def post(self, resource_type: str, attributes: dict, relationships: dict) -> dict:
+        body = {
+            "data": {
+                "type": resource_type,
+                "attributes": attributes,
+                "relationships": relationships,
+            }
+        }
+        response = self.request("POST", f"/{resource_type}", json=body)
+        if not response.ok:
+            raise RuntimeError(
+                f"POST {resource_type} failed ({response.status_code}): {response.text}"
+            )
+        return response.json()["data"]
+
 
 def find_app(client: ASCClient, bundle_id: str) -> dict:
     data = client.get_json("/apps", params={"filter[bundleId]": bundle_id, "limit": 1})
@@ -108,23 +123,31 @@ def find_app(client: ASCClient, bundle_id: str) -> dict:
 
 def find_mac_app_info(client: ASCClient, app_id: str) -> dict:
     data = client.get_json(f"/apps/{app_id}/appInfos")
-    for info in data.get("data", []):
-        attrs = info.get("attributes", {})
-        if attrs.get("appStoreState") is not None or attrs.get("brazilAgeRating") is not None:
-            # Prefer MAC_OS if platform is listed in included relationships; filter by bundle platform.
-            pass
-    # App infos are per platform — pick MAC_OS via appStoreAgeRating or secondary query.
-    for info in data.get("data", []):
+    infos = data.get("data", [])
+    if not infos:
+        raise RuntimeError(f"No appInfos for app {app_id}")
+
+    editable_states = {
+        "PREPARE_FOR_SUBMISSION",
+        "DEVELOPER_REJECTED",
+        "REJECTED",
+        "METADATA_REJECTED",
+    }
+    for info in infos:
+        state = info.get("attributes", {}).get("appStoreState")
+        if state in editable_states:
+            return info
+
+    for info in infos:
         info_id = info["id"]
         detail = client.get_json(f"/appInfos/{info_id}")
         platform = detail.get("data", {}).get("attributes", {}).get("platform")
         if platform == "MAC_OS":
             return detail["data"]
-    # Fallback: single app info entry.
-    infos = data.get("data", [])
+
     if len(infos) == 1:
         return infos[0]
-    raise RuntimeError("Could not determine MAC_OS appInfo; inspect /apps/{id}/appInfos")
+    return infos[0]
 
 
 def find_app_info_localization(client: ASCClient, app_info_id: str, locale: str) -> dict:
@@ -135,7 +158,7 @@ def find_app_info_localization(client: ASCClient, app_info_id: str, locale: str)
     raise RuntimeError(f"No appInfoLocalization for locale {locale!r}")
 
 
-def find_version(client: ASCClient, app_id: str, version_string: str) -> dict:
+def find_version(client: ASCClient, app_id: str, version_string: str, *, create: bool = False) -> dict:
     data = client.get_json(
         f"/apps/{app_id}/appStoreVersions",
         params={"filter[platform]": "MAC_OS", "limit": 50},
@@ -146,7 +169,15 @@ def find_version(client: ASCClient, app_id: str, version_string: str) -> dict:
         if v.get("attributes", {}).get("versionString") == version_string
     ]
     if not matches:
-        raise RuntimeError(f"No MAC_OS appStoreVersion {version_string!r}")
+        if not create:
+            raise RuntimeError(f"No MAC_OS appStoreVersion {version_string!r}")
+        created = client.post(
+            "appStoreVersions",
+            {"platform": "MAC_OS", "versionString": version_string},
+            {"app": {"data": {"type": "apps", "id": app_id}}},
+        )
+        print(f"Created version {version_string}: {created['id']}")
+        return created
     # Prefer editable state if multiple.
     for v in matches:
         state = v.get("attributes", {}).get("appStoreState")
@@ -155,12 +186,22 @@ def find_version(client: ASCClient, app_id: str, version_string: str) -> dict:
     return matches[0]
 
 
-def find_version_localization(client: ASCClient, version_id: str, locale: str) -> dict:
+def find_version_localization(
+    client: ASCClient, version_id: str, locale: str, *, create: bool = False
+) -> dict:
     data = client.get_json(f"/appStoreVersions/{version_id}/appStoreVersionLocalizations")
     for loc in data.get("data", []):
         if loc.get("attributes", {}).get("locale") == locale:
             return loc
-    raise RuntimeError(f"No appStoreVersionLocalization for locale {locale!r}")
+    if not create:
+        raise RuntimeError(f"No appStoreVersionLocalization for locale {locale!r}")
+    created = client.post(
+        "appStoreVersionLocalizations",
+        {"locale": locale},
+        {"appStoreVersion": {"data": {"type": "appStoreVersions", "id": version_id}}},
+    )
+    print(f"Created version localization ({locale}): {created['id']}")
+    return created
 
 
 def main() -> int:
@@ -208,7 +249,8 @@ def main() -> int:
     client.patch("appInfoLocalizations", info_loc_id, info_patch)
     print(f"Updated App Information localization ({LOCALE}): name, subtitle, privacyPolicyUrl")
 
-    version = find_version(client, app_id, VERSION_STRING)
+    create_version = os.environ.get("APP_STORE_CREATE_VERSION", "").lower() in ("1", "true", "yes")
+    version = find_version(client, app_id, VERSION_STRING, create=create_version)
     version_id = version["id"]
     version_state = version.get("attributes", {}).get("appStoreState")
     print(f"Version {VERSION_STRING}: {version_id} (state: {version_state})")
@@ -217,7 +259,7 @@ def main() -> int:
     client.patch("appStoreVersions", version_id, {"copyright": copyright_text})
     print(f"Updated version copyright")
 
-    version_loc = find_version_localization(client, version_id, LOCALE)
+    version_loc = find_version_localization(client, version_id, LOCALE, create=create_version)
     version_loc_id = version_loc["id"]
 
     version_patch = {
@@ -227,8 +269,14 @@ def main() -> int:
         "marketingUrl": read_field("link-marketing.txt"),
         "supportUrl": read_field("link-support.txt"),
     }
+    whats_new_path = APP_DATA / "whats-new.txt"
+    if whats_new_path.exists():
+        version_patch["whatsNew"] = whats_new_path.read_text(encoding="utf-8").strip()
     client.patch("appStoreVersionLocalizations", version_loc_id, version_patch)
-    print(f"Updated version {VERSION_STRING} localization ({LOCALE}): description, keywords, promotionalText, marketingUrl, supportUrl")
+    fields = "description, keywords, promotionalText, marketingUrl, supportUrl"
+    if "whatsNew" in version_patch:
+        fields += ", whatsNew"
+    print(f"Updated version {VERSION_STRING} localization ({LOCALE}): {fields}")
 
     # Custom EULA URL (Terms) — attach to app if not using standard Apple EULA.
     terms_url = read_field("link-terms.txt")