From 922d520f02bf5dfb3c2cd77071f93d98d4421923 Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Thu, 9 Apr 2026 07:56:37 +0100 Subject: [PATCH] Initial commit: Outlook Web Inbox Count extension v1.4 Chrome/Firefox extension that displays the total inbox item count next to the Inbox label in Outlook Web. Includes build+publish automation via the Chrome Web Store API (see PUBLISHING.md). --- .gitignore | 2 + CLAUDE.md | 41 +++++++ PUBLISHING.md | 128 ++++++++++++++++++++ content.js | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++ icon128.png | Bin 0 -> 5656 bytes icon16.png | Bin 0 -> 749 bytes icon64.png | Bin 0 -> 2994 bytes manifest.json | 34 ++++++ publish.sh | 113 ++++++++++++++++++ 9 files changed, 641 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 PUBLISHING.md create mode 100644 content.js create mode 100644 icon128.png create mode 100644 icon16.png create mode 100644 icon64.png create mode 100644 manifest.json create mode 100755 publish.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da00776 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +outlook_web_inbox_count_v*.zip diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..df5eaa9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,41 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a browser extension (v1.1) that enhances Outlook Web by displaying the total number of items next to the Inbox label. The extension uses a content script to monitor DOM changes and extract inbox count information from the page title. + +### Version History +- **v1.1**: Fixed browser hanging issues with throttling, improved MutationObserver, enhanced DOM selectors, reduced polling frequency +- **v1.0**: Initial release + +## Architecture + +- **manifest.json**: Chrome extension manifest (v3) defining permissions, content scripts, and metadata +- **content.js**: Main content script that runs on Outlook Web pages, implementing: + - DOM element detection for inbox labels + - Item count extraction from page titles using regex + - Dynamic count display with styling + - MutationObserver for real-time DOM change monitoring + - Periodic refresh mechanism (every 5 seconds after initial 10-second startup) + +## Development Workflow + +### Testing the Extension +1. Load as unpacked extension in Chrome/Edge developer mode +2. Navigate to https://outlook.office.com or https://outlook.office365.com +3. Check browser console for debug logs starting with "Outlook Inbox Count Extension" + +### Packaging +The .zip file contains the packaged extension ready for distribution. + +## Key Implementation Details + +- **Performance optimizations (v1.1)**: Throttling mechanism prevents excessive function calls, selective MutationObserver reduces CPU usage +- **Enhanced DOM detection**: Multiple fallback selectors and regex patterns for different Outlook Web versions +- **CSS selectors**: Primary `div[title^="Inbox -"][data-folder-name="inbox"]` with fallbacks for `button` elements and various title formats +- **Count extraction**: Multiple regex patterns including `/Inbox - (\d+) items/`, `/Inbox \((\d+)\)/`, `/(\d+) items/` +- **Duplicate prevention**: Uses `.added-count` class marking to avoid multiple count displays +- **Dynamic content handling**: Optimized interval polling (10s) and intelligent mutation observation +- **Graceful fallback**: Displays "(-)" when count unavailable \ No newline at end of file diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..2b67c11 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,128 @@ +# Publishing to the Chrome Web Store + +This document describes how to release a new version of the Outlook Web Inbox +Count extension to the Chrome Web Store using the `publish.sh` script. + +## Quick release workflow + +For a routine release (once the one-time setup below is done): + +1. Edit `content.js` and bump the version in `manifest.json` (e.g. `1.4` -> `1.5`). +2. Add a changelog entry in the header of `content.js`. +3. Run: + ```bash + ./publish.sh + ``` + This builds `outlook_web_inbox_count_v.zip` from `manifest.json`, + uploads it, and publishes it. Chrome Web Store review typically takes a + few hours to a day before the new version goes live. + +Options: +- `./publish.sh --no-publish` - upload only, leave as Draft in the dashboard. +- `./publish.sh path/to/existing.zip` - upload a specific zip without rebuilding. + +## How it works + +`publish.sh` uses the [Chrome Web Store API](https://developer.chrome.com/docs/webstore/api): + +1. Exchanges the OAuth **refresh token** (from `.env`) for a short-lived + **access token** via `https://oauth2.googleapis.com/token`. +2. `PUT`s the zip to `upload/chromewebstore/v1.1/items/$EXTENSION_ID`. +3. `POST`s to `chromewebstore/v1.1/items/$EXTENSION_ID/publish` to submit the + uploaded draft for review. + +The script reads all credentials from a `.env` file in the project root +(gitignored). Required variables: + +``` +CLIENT_ID= +CLIENT_SECRET= +REFRESH_TOKEN= +EXTENSION_ID=<32-char chrome web store extension id> +``` + +The current extension ID is `mjdfjopdcoiojbjnfkpjhcnpefjknkdn`. + +## One-time setup (already done, for reference) + +### 1. Google Cloud OAuth project + +1. Create a project at https://console.cloud.google.com/. +2. Enable the **Chrome Web Store API** under "APIs & Services". +3. Configure the OAuth consent screen: + - User type: External + - Add yourself as a **test user** (required while the app is in Testing mode). +4. Create OAuth credentials: + - "Credentials" -> "Create Credentials" -> "OAuth client ID" + - Application type: **Desktop app** + - Save the `client_id` and `client_secret` to `.env`. + +### 2. Obtain a refresh token + +Open this URL in a browser (replace `$CLIENT_ID`): + +``` +https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&client_id=$CLIENT_ID&redirect_uri=http://localhost&access_type=offline&prompt=consent +``` + +After approval, the browser will attempt to redirect to `http://localhost/?code=...` +and fail to connect - that is expected. **Copy the `code` value from the URL bar.** + +Exchange the code for a refresh token: + +```bash +curl "https://oauth2.googleapis.com/token" \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=$CLIENT_SECRET" \ + -d "code=$CODE" \ + -d "grant_type=authorization_code" \ + -d "redirect_uri=http://localhost" +``` + +Save the `refresh_token` value from the JSON response into `.env`. + +### 3. Find the extension ID + +Log into https://chrome.google.com/webstore/devconsole and open the extension's +edit page. The URL contains the 32-character extension ID: +`.../devconsole///edit`. + +## Refreshing credentials + +### If the refresh token has expired (7-day limit) + +While the OAuth consent screen is in **Testing** mode, refresh tokens expire +after **7 days**. If `publish.sh` fails with an access-token error, redo step +2 above (obtain a new code and exchange it) and update `REFRESH_TOKEN` in `.env`. + +**To avoid this**, publish the OAuth app (consent screen -> "Publish App"). +Since the only user is the developer and the scope +(`chromewebstore`) is self-owned, verification is not strictly required for +personal use - the app just needs to be in "In production" state. + +### Rotating the client secret + +If the `client_secret` is ever exposed, rotate it in the Google Cloud console +(Credentials page -> edit client -> Reset Secret) and update `.env`. You will +also need to obtain a new refresh token since the old one is tied to the old +secret. + +## Troubleshooting + +- **`Error 403: access_denied` during auth** - you are not added as a test + user on the OAuth consent screen. Add yourself in the console and retry. +- **`uploadState: FAILURE`** - usually means the zip is malformed, or the + version in `manifest.json` is not greater than the currently published + version. Bump the version and rebuild. +- **Publish returns `ITEM_PENDING_REVIEW`** - normal. The item has been + submitted and is in the Chrome review queue. +- **Refresh token suddenly invalid** - most likely the 7-day testing-mode + expiry. Obtain a new one (see above). + +## Files + +- `publish.sh` - build + upload + publish script. +- `.env` - credentials (gitignored; never commit). +- `.gitignore` - excludes `.env`. +- `manifest.json` - source of truth for the version number. +- `outlook_web_inbox_count_v.zip` - build artifact produced by `publish.sh`. diff --git a/content.js b/content.js new file mode 100644 index 0000000..6174460 --- /dev/null +++ b/content.js @@ -0,0 +1,323 @@ +// Outlook Web Inbox Count Extension v1.4 +// Changelog v1.4: +// - Added outlook.cloud.microsoft to manifest matches (Microsoft migrated Outlook Web to new domain) +// Changelog v1.3: +// - Fixed Firefox compatibility by adding browser_specific_settings to manifest +// - Fixed bug where findTotalItems() wasn't returning values due to forEach callbacks +// - Changed forEach loops to for loops to enable proper return statements +// - Extended retry mechanism with more attempts (1s, 2s, 3s, 5s, 8s, 10s, 15s, 20s) +// - Added re-logging of environment info on each retry to track page load progress +// Changelog v1.2: +// - Added comprehensive debugging information to help diagnose user issues +// - Added URL detection and page structure analysis +// - Enhanced logging for DOM element discovery +// - Added language/locale detection +// Changelog v1.1: +// - Fixed browser hanging issue by adding throttling mechanism +// - Improved MutationObserver to be more selective and prevent infinite loops +// - Enhanced DOM selectors with multiple fallbacks for different Outlook versions +// - Reduced polling frequency to improve performance +// - Added better pattern matching for inbox count extraction + +console.log("Outlook Inbox Count Extension v1.4: Script started"); + +// Enhanced debugging information +function logEnvironmentInfo() { + console.log("=== OUTLOOK INBOX EXTENSION DEBUG INFO ==="); + console.log("URL:", window.location.href); + console.log("Domain:", window.location.hostname); + console.log("User Agent:", navigator.userAgent); + console.log("Language:", navigator.language); + console.log("Document ready state:", document.readyState); + console.log("Page title:", document.title); + + // Check for New Outlook indicators + const isNewOutlook = document.querySelector('[data-app-section="NewMailModule"]') || + document.querySelector('div[role="main"][data-app-section]') || + document.body.classList.contains('new-outlook') || + window.location.href.includes('mail.office365.com'); + console.log("Detected New Outlook interface:", isNewOutlook); + + // Log all elements that might be inbox-related + console.log("=== DOM ANALYSIS ==="); + const potentialInboxElements = document.querySelectorAll('[data-folder-name], [title*="Inbox"], [aria-label*="Inbox"], button[title*="nbox"], div[title*="nbox"]'); + console.log("Found", potentialInboxElements.length, "potential inbox elements:"); + potentialInboxElements.forEach((el, i) => { + console.log(`Element ${i}:`, { + tagName: el.tagName, + className: el.className, + title: el.getAttribute('title'), + dataFolderName: el.getAttribute('data-folder-name'), + ariaLabel: el.getAttribute('aria-label'), + textContent: el.textContent?.substring(0, 100) + }); + }); + console.log("=== END DOM ANALYSIS ==="); +} + +let isProcessing = false; +let lastProcessTime = 0; +const THROTTLE_DELAY = 1000; // 1 second throttle + +function findInboxElement() { + console.log("=== SEARCHING FOR INBOX ELEMENT ==="); + + // Try multiple selectors to find inbox element + const selectors = [ + 'div[title^="Inbox -"][data-folder-name="inbox"]', + 'div[data-folder-name="inbox"]', + 'div[title*="Inbox"]', + 'button[title^="Inbox -"]', + 'button[data-folder-name="inbox"]', + // Additional selectors for different Outlook versions + 'button[aria-label*="Inbox"]', + 'div[aria-label*="Inbox"]', + '[data-folder-id="inbox"]', + '[data-automation-id*="inbox"]', + 'span[title*="Inbox"]' + ]; + + console.log("Trying", selectors.length, "different selectors..."); + + for (let i = 0; i < selectors.length; i++) { + const selector = selectors[i]; + console.log(`Trying selector ${i + 1}/${selectors.length}:`, selector); + + const elements = document.querySelectorAll(selector); + console.log(`Found ${elements.length} elements with this selector`); + + if (elements.length > 0) { + elements.forEach((el, idx) => { + console.log(` Element ${idx}:`, { + tagName: el.tagName, + className: el.className, + title: el.getAttribute('title'), + ariaLabel: el.getAttribute('aria-label'), + dataFolderName: el.getAttribute('data-folder-name'), + textContent: el.textContent?.substring(0, 50) + '...' + }); + }); + + const inboxDiv = elements[0]; // Use the first match + console.log("✓ Using first element as inbox:", inboxDiv); + + const span = inboxDiv.querySelector('span:not(.added-count)'); + if (span) { + console.log("✓ Found inner span element:", span); + return span; + } + // If no span found, try to use the element itself + console.log("ℹ No inner span found, using element itself"); + return inboxDiv; + } + } + + console.log("❌ Inbox element not found with any selector"); + console.log("=== END INBOX ELEMENT SEARCH ==="); + return null; +} + +function findTotalItems() { + console.log("=== SEARCHING FOR TOTAL ITEMS COUNT ==="); + + // Try multiple selectors and patterns + const selectors = [ + 'div[title^="Inbox -"][data-folder-name="inbox"]', + 'div[data-folder-name="inbox"]', + 'div[title*="Inbox"]', + 'button[title^="Inbox -"]', + 'button[data-folder-name="inbox"]', + 'button[aria-label*="Inbox"]', + 'div[aria-label*="Inbox"]', + '[data-folder-id="inbox"]' + ]; + + const patterns = [ + /Inbox - (\d+) items/i, + /Inbox \((\d+)\)/i, + /(\d+) items/i, + /\((\d+)\)/i, + // Additional patterns for different languages/formats + /Inbox.*?(\d+)/i, + /(\d+).*?unread/i, + /(\d+).*?message/i, + /(\d+).*?email/i + ]; + + // Also check document title which often contains count + console.log("Checking document title:", document.title); + for (const pattern of patterns) { + const match = document.title.match(pattern); + if (match) { + const totalItems = match[1]; + console.log(`✓ Total items found in document title: ${totalItems}`); + return totalItems; + } + } + + console.log("Checking DOM elements..."); + for (const selector of selectors) { + const elements = document.querySelectorAll(selector); + console.log(`Checking selector: ${selector} (${elements.length} elements)`); + + for (let idx = 0; idx < elements.length; idx++) { + const inboxDiv = elements[idx]; + const sources = [ + { name: 'title', value: inboxDiv.getAttribute('title') }, + { name: 'aria-label', value: inboxDiv.getAttribute('aria-label') }, + { name: 'textContent', value: inboxDiv.textContent } + ]; + + for (const source of sources) { + if (!source.value) continue; + + console.log(` Element ${idx} ${source.name}:`, source.value.substring(0, 100)); + + for (const pattern of patterns) { + const match = source.value.match(pattern); + if (match) { + const totalItems = match[1]; + console.log(`✓ Total items found in ${source.name}: ${totalItems}`); + return totalItems; + } + } + } + } + } + + console.log("❌ Total items count not found in any source"); + console.log("=== END TOTAL ITEMS SEARCH ==="); + return null; +} + +function addInboxCount() { + const now = Date.now(); + + // Throttle execution to prevent excessive calls + if (isProcessing || (now - lastProcessTime) < THROTTLE_DELAY) { + return; + } + + isProcessing = true; + lastProcessTime = now; + console.log("addInboxCount function called"); + + const inboxElement = findInboxElement(); + if (!inboxElement) { + console.log("Inbox element not found, will try again later"); + isProcessing = false; + return; + } + + const totalItems = findTotalItems(); + console.log("Total items:", totalItems); + + // Check if we've already added the count + let countSpan = inboxElement.querySelector('.added-count'); + if (!countSpan) { + // Create a new span element for the count + countSpan = document.createElement('span'); + countSpan.style.marginLeft = '5px'; + countSpan.style.fontWeight = 'bold'; + countSpan.className = 'added-count'; + + // Add the count next to the inbox label + inboxElement.appendChild(countSpan); + console.log("Count span added to inbox label"); + } + + // Update the count + if (totalItems) { + countSpan.textContent = ` (${totalItems})`; + countSpan.style.color = ''; // Reset to default color + } else { + countSpan.textContent = ' (-)'; + countSpan.style.color = '#888'; // Grey color + } + console.log("Count span updated"); + isProcessing = false; +} + +function init() { + console.log("=== INITIALIZING OUTLOOK INBOX COUNT EXTENSION ==="); + + // Log environment information first + logEnvironmentInfo(); + + console.log("Starting initial inbox count attempt..."); + addInboxCount(); + + // Try to add the inbox count multiple times during startup with extended delays + // Firefox may need more time for Outlook's dynamic content to load + console.log("Scheduling additional attempts at 1s, 2s, 3s, 5s, 8s, 10s, 15s, and 20s..."); + const retryTimes = [1000, 2000, 3000, 5000, 8000, 10000, 15000, 20000]; + + retryTimes.forEach(delay => { + setTimeout(() => { + console.log(`=== ${delay/1000}-SECOND RETRY ===`); + logEnvironmentInfo(); // Re-log to see if page has loaded + addInboxCount(); + }, delay); + }); + + // Then, check every 15 seconds (slightly increased for debugging) + setInterval(() => { + console.log("=== PERIODIC CHECK ==="); + addInboxCount(); + }, 15000); + + console.log("=== INITIALIZATION COMPLETE ==="); +} + +// Use a MutationObserver to watch for changes in the DOM +const observer = new MutationObserver((mutations) => { + let shouldUpdate = false; + + for (let mutation of mutations) { + // Only update if attributes changed on elements that might be inbox-related + if (mutation.type === 'attributes' && mutation.attributeName === 'title') { + const target = mutation.target; + if (target.hasAttribute && target.hasAttribute('data-folder-name')) { + shouldUpdate = true; + break; + } + } + // Or if new inbox elements were added + else if (mutation.type === 'childList') { + const nodes = [...(mutation.addedNodes || [])]; + if (nodes.some(node => node.nodeType === 1 && + (node.hasAttribute?.('data-folder-name') || + node.querySelector?.('[data-folder-name="inbox"]')))) { + shouldUpdate = true; + break; + } + } + } + + if (shouldUpdate) { + addInboxCount(); + } +}); + +// Start observing with more targeted configuration +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['title', 'data-folder-name'] +}); + +// Run the init function when the DOM is fully loaded +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); +} else { + init(); +} + +// Listen for messages from the background script +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.message === "URL_CHANGED") { + console.log("URL changed, running inbox count function"); + addInboxCount(); + } +}); diff --git a/icon128.png b/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..487087d6109573b19dc5dfefd0c951a6d69cea0e GIT binary patch literal 5656 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 literal 0 HcmV?d00001 diff --git a/icon64.png b/icon64.png new file mode 100644 index 0000000000000000000000000000000000000000..58b2a91e34cb3f7f753cc929e105ad80480eaaac GIT binary patch literal 2994 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 literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..146a4a5 --- /dev/null +++ b/manifest.json @@ -0,0 +1,34 @@ +{ + "manifest_version": 3, + "name": "Outlook Web Inbox Count", + "version": "1.4", + "description": "Adds the total number of items next to the Inbox label in Outlook Web", + "author": "Giorgio Gilestro", + "homepage_url": "https://lab.gilest.ro", + "browser_specific_settings": { + "gecko": { + "id": "outlook-inbox-count@gilest.ro" + } + }, + "permissions": [], + "host_permissions": [ + "https://outlook.office.com/*", + "https://outlook.office365.com/*", + "https://outlook.cloud.microsoft/*" + ], + "content_scripts": [ + { + "matches": [ + "https://outlook.office.com/*", + "https://outlook.office365.com/*", + "https://outlook.cloud.microsoft/*" + ], + "js": ["content.js"] + } + ], + "icons": { + "16": "icon16.png", + "64": "icon64.png", + "128": "icon128.png" + } +} diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..1929c00 --- /dev/null +++ b/publish.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Build and publish the extension to the Chrome Web Store. +# +# Reads the version from manifest.json, builds outlook_web_inbox_count_v.zip +# (overwriting if present), then uploads and publishes it via the Chrome Web Store API. +# +# Usage: +# ./publish.sh # build from manifest.json, upload, and publish +# ./publish.sh --no-publish # upload only (leave in Draft state in the dashboard) +# ./publish.sh path/to/extension.zip # skip build, upload a specific zip +# +# Requires a .env file in the project root with: +# CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, EXTENSION_ID +# See PUBLISHING.md for how to obtain these. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# --- Parse arguments --- +PUBLISH=true +ZIP_ARG="" +for arg in "$@"; do + case "$arg" in + --no-publish) PUBLISH=false ;; + -h|--help) + sed -n '2,15p' "$0" + exit 0 + ;; + *) ZIP_ARG="$arg" ;; + esac +done + +# --- Load credentials --- +if [[ ! -f .env ]]; then + echo "Error: .env file not found (see PUBLISHING.md)" >&2 + exit 1 +fi +set -a +# shellcheck disable=SC1091 +source .env +set +a + +for var in CLIENT_ID CLIENT_SECRET REFRESH_TOKEN EXTENSION_ID; do + if [[ -z "${!var:-}" ]]; then + echo "Error: $var is not set in .env" >&2 + exit 1 + fi +done + +# --- Determine / build zip --- +if [[ -n "$ZIP_ARG" ]]; then + ZIP="$ZIP_ARG" + if [[ ! -f "$ZIP" ]]; then + echo "Error: $ZIP not found" >&2 + exit 1 + fi +else + VERSION=$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])") + ZIP="outlook_web_inbox_count_v${VERSION}.zip" + echo "Building $ZIP from manifest version $VERSION..." + rm -f "$ZIP" + zip -q "$ZIP" manifest.json content.js icon16.png icon64.png icon128.png +fi +echo "Package: $ZIP" + +# --- Get a fresh access token --- +echo "Fetching access token..." +ACCESS_TOKEN=$(curl -s "https://oauth2.googleapis.com/token" \ + -d "client_id=${CLIENT_ID}" \ + -d "client_secret=${CLIENT_SECRET}" \ + -d "refresh_token=${REFRESH_TOKEN}" \ + -d "grant_type=refresh_token" \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('access_token',''))") + +if [[ -z "$ACCESS_TOKEN" ]]; then + echo "Error: failed to obtain access token. Refresh token may have expired." >&2 + echo "See PUBLISHING.md 'Refreshing credentials' section." >&2 + exit 1 +fi + +# --- Upload --- +echo "Uploading to extension $EXTENSION_ID..." +UPLOAD_RESPONSE=$(curl -s \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "x-goog-api-version: 2" \ + -X PUT \ + -T "$ZIP" \ + "https://www.googleapis.com/upload/chromewebstore/v1.1/items/$EXTENSION_ID") +echo "Upload response: $UPLOAD_RESPONSE" + +UPLOAD_STATE=$(echo "$UPLOAD_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('uploadState',''))" 2>/dev/null || echo "") +if [[ "$UPLOAD_STATE" != "SUCCESS" ]]; then + echo "Error: upload did not succeed (state: $UPLOAD_STATE)" >&2 + exit 1 +fi + +# --- Publish --- +if [[ "$PUBLISH" == "true" ]]; then + echo "Publishing..." + PUBLISH_RESPONSE=$(curl -s \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "x-goog-api-version: 2" \ + -H "Content-Length: 0" \ + -X POST \ + "https://www.googleapis.com/chromewebstore/v1.1/items/$EXTENSION_ID/publish") + echo "Publish response: $PUBLISH_RESPONSE" +else + echo "Skipping publish step (--no-publish). The upload is in Draft state in the dashboard." +fi + +echo "Done."