From 0c2556433763c8daa6ac700e9f8a88057b2574e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 16:16:26 +0000 Subject: [PATCH] Normalize redirect URI to fix OAuth invalid_grant (1.2.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TickTick's OAuth server requires byte-exact match between the redirect_uri the plugin sends and the value registered in the developer portal. Users who register the URI with a trailing slash (common on iOS paste, or by hand) get rejected with: error="invalid_grant" error_description="Invalid redirect: ...vercel.app does not match one of the registered values: [...vercel.app/]" (Reported in issue #2.) Normalize the redirect URI by stripping trailing slashes and whitespace at all four use/save sites: - main.ts: startAuthFlow() and exchangeAuthCodeForToken() clean the URI before sending — protects existing users with a slash already saved - settings.ts: both Redirect URI inputs strip on save so the persisted value matches what's displayed - settings.ts: both descriptions now warn against trailing slashes - README: new Troubleshooting entry for the invalid_grant error https://claude.ai/code/session_01V2fsiatACzjUCNwSEhwER9 --- README.md | 1 + main.js | 4 ++-- main.ts | 4 ++-- manifest.json | 2 +- package.json | 2 +- settings.ts | 7 ++++--- versions.json | 3 ++- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6730f84..7292bda 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ After triggering, you'll see a notice confirming the task was created. - **"TickTick auth failed: state mismatch"** — the OAuth flow expired or was started in a different session. Tap Connect again and complete the flow without delays. - **"Failed to obtain access token"** — most often a bad Client ID or Secret, or the Redirect URI you set in the TickTick developer portal doesn't exactly match the one in the plugin. Copy/paste both rather than typing. +- **`"invalid_grant"` / `"Invalid redirect: ... does not match one of the registered values"`** — the URI registered in the TickTick Developer Portal isn't byte-equal to the one the plugin sends. The most common cause is a trailing `/` in the portal entry. Edit your TickTick app and remove any trailing slash so it matches the plugin's Redirect URI field exactly. - **The Connect popup link does nothing** — make sure your default browser is set and you've allowed Obsidian to open external links. Try long-pressing the link to copy it, then paste into Safari/Chrome. - **Callback page shows the code as text instead of returning to Obsidian** — that's the manual-fallback case. Paste the code into the "Authorization code" field; the plugin will exchange it for a token. (This happens if the callback page hasn't been deployed with the deep-link redirect.) - **The "Open in Obsidian" link in TickTick does nothing** — confirm the Advanced URI plugin is installed and enabled in the same vault. diff --git a/main.js b/main.js index 0dc2e89..6e6b308 100644 --- a/main.js +++ b/main.js @@ -3,8 +3,8 @@ THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ -var m=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var U=(o,e)=>{for(var n in e)m(o,n,{get:e[n],enumerable:!0})},N=(o,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of I(e))!A.call(o,t)&&t!==n&&m(o,t,{get:()=>e[t],enumerable:!(i=_(e,t))||i.enumerable});return o};var D=o=>N(m({},"__esModule",{value:!0}),o);var M={};U(M,{default:()=>k});module.exports=D(M);var S=require("obsidian"),r=require("obsidian");var c=require("obsidian"),f={accessToken:"",clientId:"",clientSecret:"",redirectUri:"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",selectionMode:"line",tagPosition:"append"},u=class extends c.PluginSettingTab{constructor(e,n){super(e,n),this.plugin=n}display(){let{containerEl:e}=this;e.empty(),this.plugin.settings.accessToken?this.renderConnectedView(e):this.renderSetupView(e)}renderSetupView(e){e.createEl("h2",{text:"Set up TickTick"}),e.createEl("p",{text:"Connect this plugin to your TickTick account in two steps. You only need to do this once."}),new c.Setting(e).setName("Step 1 \u2014 Enter your TickTick app credentials").setHeading();let n=e.createEl("p");n.appendText("Sign in to the ");let i=n.createEl("a",{text:"TickTick Developer Portal",href:"https://developer.ticktick.com/"});i.setAttr("target","_blank"),i.setAttr("rel","noopener"),n.appendText(", create an app, and copy its Client ID and Client Secret into the fields below. In the portal, set the app's Redirect URI to the value shown in the Redirect URI field."),new c.Setting(e).setName("Client ID").addText(t=>t.setPlaceholder("Your Client ID").setValue(this.plugin.settings.clientId||"").onChange(async s=>{this.plugin.settings.clientId=s.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Client Secret").addText(t=>(t.inputEl.type="password",t.setPlaceholder("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022").setValue(this.plugin.settings.clientSecret||"").onChange(async s=>{this.plugin.settings.clientSecret=s.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(e).setName("Redirect URI").setDesc("Copy this exact value into your TickTick app's Redirect URI field in the developer portal. Only change it here if you self-host the callback page.").addText(t=>t.setPlaceholder(f.redirectUri).setValue(this.plugin.settings.redirectUri||"").onChange(async s=>{this.plugin.settings.redirectUri=s.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Step 2 \u2014 Authorize").setHeading(),e.createEl("p",{text:"Tap Connect, then tap the link in the popup to sign in to TickTick and approve access. The callback page will either return you to Obsidian automatically, or show you an authorization code \u2014 if you see a code, copy it and paste it in the field below."}),new c.Setting(e).setName("Connect to TickTick").addButton(t=>{t.setButtonText("Connect").setCta().onClick(async()=>{await this.plugin.startAuthFlow()})}),new c.Setting(e).setName("Authorization code").setDesc("Only needed if the callback page showed you a code instead of returning you to Obsidian automatically.").addText(t=>t.setPlaceholder("Paste authorization code").onChange(async s=>{s.trim()&&(await this.plugin.exchangeAuthCodeForToken(s.trim()),t.setValue(""),this.display())}))}renderConnectedView(e){e.createEl("h2",{text:"Connected to TickTick"}),e.createEl("p",{text:'Use the "Create TickTick task" command from any note to send the current line or paragraph to TickTick.'}),new c.Setting(e).setName("Plugin behavior").setHeading(),new c.Setting(e).setName("Selection mode").setDesc("Capture only the current line or the entire paragraph.").addDropdown(t=>t.addOption("line","Current line").addOption("paragraph","Entire paragraph").setValue(this.plugin.settings.selectionMode).onChange(async s=>{this.plugin.settings.selectionMode=s,await this.plugin.saveSettings()})),new c.Setting(e).setName("Tag position").setDesc("Where the #ticktick tag is added relative to the text.").addDropdown(t=>t.addOption("append","Append (end)").addOption("prepend","Prepend (beginning)").setValue(this.plugin.settings.tagPosition).onChange(async s=>{this.plugin.settings.tagPosition=s,await this.plugin.saveSettings()}));let n=e.createEl("details",{cls:"ticktick-advanced"});n.style.marginTop="2em";let i=n.createEl("summary",{text:"Advanced \u2014 Connection details"});i.style.cursor="pointer",i.style.fontWeight="600",i.style.padding="0.5em 0",new c.Setting(n).setName("Reconnect").setDesc("Re-run the OAuth flow. Use if your token can no longer be refreshed.").addButton(t=>{t.setButtonText("Reconnect").onClick(async()=>{await this.plugin.startAuthFlow()})}),new c.Setting(n).setName("Authorization code").setDesc("Manual fallback if reconnecting did not return you to Obsidian automatically.").addText(t=>t.setPlaceholder("Paste authorization code").onChange(async s=>{s.trim()&&(await this.plugin.exchangeAuthCodeForToken(s.trim()),t.setValue(""),this.display())})),new c.Setting(n).setName("Disconnect").setDesc("Clear stored tokens. You will need to authorize again to create tasks.").addButton(t=>{t.setButtonText("Disconnect").setWarning().onClick(async()=>{this.plugin.settings.accessToken="",this.plugin.settings.refreshToken=void 0,this.plugin.settings.tokenExpiry=void 0,await this.plugin.saveSettings(),new c.Notice("Disconnected from TickTick."),this.display()})}),new c.Setting(n).setName("Client ID").addText(t=>t.setValue(this.plugin.settings.clientId||"").onChange(async s=>{this.plugin.settings.clientId=s.trim(),await this.plugin.saveSettings()})),new c.Setting(n).setName("Client Secret").addText(t=>(t.inputEl.type="password",t.setPlaceholder("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022").setValue(this.plugin.settings.clientSecret||"").onChange(async s=>{this.plugin.settings.clientSecret=s.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(n).setName("Redirect URI").addText(t=>t.setValue(this.plugin.settings.redirectUri||"").onChange(async s=>{this.plugin.settings.redirectUri=s.trim(),await this.plugin.saveSettings()})),new c.Setting(n).setName("Access token").setDesc("Stored access token (read-only).").addText(t=>{t.inputEl.readOnly=!0,t.setValue(this.plugin.settings.accessToken||"")}),new c.Setting(n).setName("Refresh token").setDesc("Stored refresh token (read-only).").addText(t=>{t.inputEl.readOnly=!0,t.setValue(this.plugin.settings.refreshToken||"")})}};function y(o){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",n="";for(let i=0;i0&&o.getLine(n-1).trim()!=="";)n--;for(;i{for(var n in e)m(o,n,{get:e[n],enumerable:!0})},N=(o,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of _(e))!A.call(o,t)&&t!==n&&m(o,t,{get:()=>e[t],enumerable:!(i=I(e,t))||i.enumerable});return o};var D=o=>N(m({},"__esModule",{value:!0}),o);var M={};U(M,{default:()=>k});module.exports=D(M);var S=require("obsidian"),r=require("obsidian");var c=require("obsidian"),f={accessToken:"",clientId:"",clientSecret:"",redirectUri:"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",selectionMode:"line",tagPosition:"append"},u=class extends c.PluginSettingTab{constructor(e,n){super(e,n),this.plugin=n}display(){let{containerEl:e}=this;e.empty(),this.plugin.settings.accessToken?this.renderConnectedView(e):this.renderSetupView(e)}renderSetupView(e){e.createEl("h2",{text:"Set up TickTick"}),e.createEl("p",{text:"Connect this plugin to your TickTick account in two steps. You only need to do this once."}),new c.Setting(e).setName("Step 1 \u2014 Enter your TickTick app credentials").setHeading();let n=e.createEl("p");n.appendText("Sign in to the ");let i=n.createEl("a",{text:"TickTick Developer Portal",href:"https://developer.ticktick.com/"});i.setAttr("target","_blank"),i.setAttr("rel","noopener"),n.appendText(", create an app, and copy its Client ID and Client Secret into the fields below. In the portal, set the app's Redirect URI to the value shown in the Redirect URI field."),new c.Setting(e).setName("Client ID").addText(t=>t.setPlaceholder("Your Client ID").setValue(this.plugin.settings.clientId||"").onChange(async s=>{this.plugin.settings.clientId=s.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Client Secret").addText(t=>(t.inputEl.type="password",t.setPlaceholder("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022").setValue(this.plugin.settings.clientSecret||"").onChange(async s=>{this.plugin.settings.clientSecret=s.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(e).setName("Redirect URI").setDesc("Copy this exact value into your TickTick app's Redirect URI field in the developer portal. Do not add a trailing slash \u2014 the URIs must match exactly. Only change it here if you self-host the callback page.").addText(t=>t.setPlaceholder(f.redirectUri).setValue(this.plugin.settings.redirectUri||"").onChange(async s=>{this.plugin.settings.redirectUri=s.trim().replace(/\/+$/,""),await this.plugin.saveSettings()})),new c.Setting(e).setName("Step 2 \u2014 Authorize").setHeading(),e.createEl("p",{text:"Tap Connect, then tap the link in the popup to sign in to TickTick and approve access. The callback page will either return you to Obsidian automatically, or show you an authorization code \u2014 if you see a code, copy it and paste it in the field below."}),new c.Setting(e).setName("Connect to TickTick").addButton(t=>{t.setButtonText("Connect").setCta().onClick(async()=>{await this.plugin.startAuthFlow()})}),new c.Setting(e).setName("Authorization code").setDesc("Only needed if the callback page showed you a code instead of returning you to Obsidian automatically.").addText(t=>t.setPlaceholder("Paste authorization code").onChange(async s=>{s.trim()&&(await this.plugin.exchangeAuthCodeForToken(s.trim()),t.setValue(""),this.display())}))}renderConnectedView(e){e.createEl("h2",{text:"Connected to TickTick"}),e.createEl("p",{text:'Use the "Create TickTick task" command from any note to send the current line or paragraph to TickTick.'}),new c.Setting(e).setName("Plugin behavior").setHeading(),new c.Setting(e).setName("Selection mode").setDesc("Capture only the current line or the entire paragraph.").addDropdown(t=>t.addOption("line","Current line").addOption("paragraph","Entire paragraph").setValue(this.plugin.settings.selectionMode).onChange(async s=>{this.plugin.settings.selectionMode=s,await this.plugin.saveSettings()})),new c.Setting(e).setName("Tag position").setDesc("Where the #ticktick tag is added relative to the text.").addDropdown(t=>t.addOption("append","Append (end)").addOption("prepend","Prepend (beginning)").setValue(this.plugin.settings.tagPosition).onChange(async s=>{this.plugin.settings.tagPosition=s,await this.plugin.saveSettings()}));let n=e.createEl("details",{cls:"ticktick-advanced"});n.style.marginTop="2em";let i=n.createEl("summary",{text:"Advanced \u2014 Connection details"});i.style.cursor="pointer",i.style.fontWeight="600",i.style.padding="0.5em 0",new c.Setting(n).setName("Reconnect").setDesc("Re-run the OAuth flow. Use if your token can no longer be refreshed.").addButton(t=>{t.setButtonText("Reconnect").onClick(async()=>{await this.plugin.startAuthFlow()})}),new c.Setting(n).setName("Authorization code").setDesc("Manual fallback if reconnecting did not return you to Obsidian automatically.").addText(t=>t.setPlaceholder("Paste authorization code").onChange(async s=>{s.trim()&&(await this.plugin.exchangeAuthCodeForToken(s.trim()),t.setValue(""),this.display())})),new c.Setting(n).setName("Disconnect").setDesc("Clear stored tokens. You will need to authorize again to create tasks.").addButton(t=>{t.setButtonText("Disconnect").setWarning().onClick(async()=>{this.plugin.settings.accessToken="",this.plugin.settings.refreshToken=void 0,this.plugin.settings.tokenExpiry=void 0,await this.plugin.saveSettings(),new c.Notice("Disconnected from TickTick."),this.display()})}),new c.Setting(n).setName("Client ID").addText(t=>t.setValue(this.plugin.settings.clientId||"").onChange(async s=>{this.plugin.settings.clientId=s.trim(),await this.plugin.saveSettings()})),new c.Setting(n).setName("Client Secret").addText(t=>(t.inputEl.type="password",t.setPlaceholder("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022").setValue(this.plugin.settings.clientSecret||"").onChange(async s=>{this.plugin.settings.clientSecret=s.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(n).setName("Redirect URI").setDesc("Must match the value registered in your TickTick app exactly, with no trailing slash.").addText(t=>t.setValue(this.plugin.settings.redirectUri||"").onChange(async s=>{this.plugin.settings.redirectUri=s.trim().replace(/\/+$/,""),await this.plugin.saveSettings()})),new c.Setting(n).setName("Access token").setDesc("Stored access token (read-only).").addText(t=>{t.inputEl.readOnly=!0,t.setValue(this.plugin.settings.accessToken||"")}),new c.Setting(n).setName("Refresh token").setDesc("Stored refresh token (read-only).").addText(t=>{t.inputEl.readOnly=!0,t.setValue(this.plugin.settings.refreshToken||"")})}};function y(o){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",n="";for(let i=0;i0&&o.getLine(n-1).trim()!=="";)n--;for(;i\s*|#{1,6}\s+|[-*+]\s+\[[ xX]\]\s+|[-*+]\s+|\d+\.\s+)+/,"").replace(/__([^_]+)__/g,"$1").replace(/\*\*?|~~|==|`/g,"").replace(/\s+/g," ").trim()}async function $(){let o=y(64),n=new TextEncoder().encode(o),i=await crypto.subtle.digest("SHA-256",n),t=(0,S.arrayBufferToBase64)(i).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return{codeVerifier:o,codeChallenge:t}}var x=class extends r.Modal{constructor(n,i){super(n);this.authUrl=i}onOpen(){let{contentEl:n}=this;n.empty(),n.createEl("h2",{text:"Authorize TickTick"}),n.createEl("p",{text:"Tap the link below to open the TickTick login page. After you approve access, you will be returned to Obsidian automatically."});let i=n.createEl("a",{text:"Open TickTick login",href:this.authUrl});i.setAttr("target","_blank"),i.setAttr("rel","noopener"),i.style.display="inline-block",i.style.marginTop="0.5em",i.style.fontWeight="bold"}onClose(){this.contentEl.empty()}};function w(o,e,n){console.log(JSON.stringify({timestamp:new Date().toISOString(),plugin:"ticktick-integration",action:e,...n,clientId:o.settings.clientId?"***":void 0,accessToken:o.settings.accessToken?"***":void 0}))}var k=class extends r.Plugin{async onload(){console.log("Loading TickTick Plugin"),await this.loadSettings(),this.addSettingTab(new u(this.app,this)),this.registerObsidianProtocolHandler("ticktick-callback",async e=>{if(!e.code){new r.Notice("TickTick auth failed: no authorization code returned.");return}if(!this.settings.tempState||e.state!==this.settings.tempState){new r.Notice("TickTick auth failed: state mismatch. Please try connecting again.");return}await this.exchangeAuthCodeForToken(e.code)}),this.addCommand({id:"create-ticktick-task",name:"Create TickTick task",icon:"lucide-circle-check-big",editorCallback:(e,n)=>this.createTaskFromEditor(e,n)}),this.registerEvent(this.app.workspace.on("file-menu",(e,n)=>{if(!(n instanceof r.TFile)||n.extension!=="md")return;let i=this.app.workspace.getActiveViewOfType(r.MarkdownView);!i||!i.editor||i.file!==n||e.addItem(t=>{t.setTitle("Create TickTick task").setIcon("lucide-circle-check-big").onClick(async()=>{await this.createTaskFromEditor(i.editor,i)})})}))}async createTaskFromEditor(e,n){let i=e.getCursor();if(!n.file){new r.Notice("No file found for the current view!");return}let t,s,a;if(this.settings.selectionMode==="paragraph"){let{text:h,start:g,end:E}=V(e,i.line);t=h,s=g,a=E}else{let{text:h,line:g}=R(e,i.line);t=h,s=g,a=g}if(!t.trim()){new r.Notice("No text found on the current line!");return}let d=y(8),l=this.settings.tagPosition==="prepend"?`#ticktick ${t} ^${d}`:`${t} #ticktick ^${d}`;e.replaceRange(l,{line:s,ch:0},{line:a,ch:e.getLine(a).length});let p=this.app.vault.getName(),C=n.file.path,b=`obsidian://advanced-uri?vault=${encodeURIComponent(p)}&filepath=${encodeURIComponent(C)}&block=${encodeURIComponent(d)}`,v=`${t} [Open in Obsidian](${b})`,T=O(t),P=T.length>50?T.substring(0,50)+"...":T;await this.ensureFreshToken();try{(await this.createTicktickTask(P,v)).success?new r.Notice("TickTick task created successfully!"):new r.Notice("Failed to create TickTick task.")}catch(h){w(this,"task_creation_failed",{error:h.message}),new r.Notice(`Failed to create task: ${h.message} -Check console for details.`)}}onunload(){console.log("Unloading TickTick Plugin")}async startAuthFlow(){let e=this.settings.clientId;if(!e){new r.Notice("Please enter your Client ID in the settings.");return}let n=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",i=encodeURIComponent("tasks:read tasks:write"),{codeVerifier:t,codeChallenge:s}=await $(),a=y(32);this.settings.tempCodeVerifier=t,this.settings.tempState=a,await this.saveSettings();let d=`https://ticktick.com/oauth/authorize?client_id=${e}&redirect_uri=${encodeURIComponent(n)}&response_type=code&scope=${i}&code_challenge=${s}&code_challenge_method=S256&state=${a}`;new x(this.app,d).open()}async exchangeAuthCodeForToken(e){let n="https://ticktick.com/oauth/token",i=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",t=this.settings.clientId,s=this.settings.clientSecret,a=this.settings.tempCodeVerifier;if(!a||!t||!s){new r.Notice("Missing required credentials. Please update your settings.");return}let d=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:i,client_id:t,client_secret:s,code_verifier:a});try{let l=await(0,r.requestUrl)({url:n,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:d.toString()});if(l.status===200){let p=l.json;p.refresh_token?this.settings.refreshToken=p.refresh_token:(console.warn("Refresh token not received during token exchange."),new r.Notice("Refresh token not received. Re-authentication may be required more often.")),this.settings.accessToken=p.access_token,this.settings.tokenExpiry=Date.now()+p.expires_in*1e3*.85,this.settings.tempCodeVerifier=void 0,this.settings.tempState=void 0,await this.saveSettings(),new r.Notice("TickTick access token obtained successfully!")}else console.error("Token exchange error (status):",l.status),console.log("response.json:",l.json),console.log("response.text:",l.text),new r.Notice("Failed to obtain access token.")}catch(l){console.error("Error during token exchange:",l),new r.Notice("Error during token exchange.")}}async refreshAccessToken(){if(!this.settings.refreshToken)throw new Error("No refresh token available. Please reconnect.");let e="https://ticktick.com/oauth/token",n=this.settings.clientId,i=this.settings.clientSecret,t=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.settings.refreshToken,client_id:n,client_secret:i});try{let s=await(0,r.requestUrl)({url:e,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:t.toString()});if(s.status===200){let a=s.json;a.refresh_token&&(this.settings.refreshToken=a.refresh_token),this.settings.accessToken=a.access_token,this.settings.tokenExpiry=Date.now()+a.expires_in*1e3*.85,await this.saveSettings(),new r.Notice("Access token refreshed successfully!")}else throw console.error("Refresh token error:",s.text),new Error("Failed to refresh token")}catch(s){throw console.error("Error refreshing access token:",s),s}}async ensureFreshToken(){if(this.settings.tokenExpiry&&Date.now()>this.settings.tokenExpiry){w(this,"token_expired",{tokenExpiry:this.settings.tokenExpiry});try{await this.refreshAccessToken()}catch(e){throw new r.Notice("Reauthentication required. Please reconnect to TickTick."),e}}}async createTicktickTask(e,n){let i=this.settings.accessToken;if(!i)return new r.Notice("No access token found. Please connect to TickTick in the settings."),{success:!1};let t="https://api.ticktick.com/open/v1/task",s={title:e,content:n};try{w(this,"task_creation_request",{endpoint:t,title:e});let a=await(0,r.requestUrl)({url:t,method:"POST",headers:{Authorization:`Bearer ${i}`,"Content-Type":"application/json","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:JSON.stringify(s)});return a.status===200?{success:!0}:(console.error("TickTick API error (status):",a.status),console.log("response.json:",a.json),console.log("response.text:",a.text),{success:!1})}catch(a){return console.error("TickTick API error:",a),{success:!1}}}async loadSettings(){this.settings=Object.assign({},f,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}}; +Check console for details.`)}}onunload(){console.log("Unloading TickTick Plugin")}async startAuthFlow(){let e=this.settings.clientId;if(!e){new r.Notice("Please enter your Client ID in the settings.");return}let n=(this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").trim().replace(/\/+$/,""),i=encodeURIComponent("tasks:read tasks:write"),{codeVerifier:t,codeChallenge:s}=await $(),a=y(32);this.settings.tempCodeVerifier=t,this.settings.tempState=a,await this.saveSettings();let d=`https://ticktick.com/oauth/authorize?client_id=${e}&redirect_uri=${encodeURIComponent(n)}&response_type=code&scope=${i}&code_challenge=${s}&code_challenge_method=S256&state=${a}`;new x(this.app,d).open()}async exchangeAuthCodeForToken(e){let n="https://ticktick.com/oauth/token",i=(this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").trim().replace(/\/+$/,""),t=this.settings.clientId,s=this.settings.clientSecret,a=this.settings.tempCodeVerifier;if(!a||!t||!s){new r.Notice("Missing required credentials. Please update your settings.");return}let d=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:i,client_id:t,client_secret:s,code_verifier:a});try{let l=await(0,r.requestUrl)({url:n,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:d.toString()});if(l.status===200){let p=l.json;p.refresh_token?this.settings.refreshToken=p.refresh_token:(console.warn("Refresh token not received during token exchange."),new r.Notice("Refresh token not received. Re-authentication may be required more often.")),this.settings.accessToken=p.access_token,this.settings.tokenExpiry=Date.now()+p.expires_in*1e3*.85,this.settings.tempCodeVerifier=void 0,this.settings.tempState=void 0,await this.saveSettings(),new r.Notice("TickTick access token obtained successfully!")}else console.error("Token exchange error (status):",l.status),console.log("response.json:",l.json),console.log("response.text:",l.text),new r.Notice("Failed to obtain access token.")}catch(l){console.error("Error during token exchange:",l),new r.Notice("Error during token exchange.")}}async refreshAccessToken(){if(!this.settings.refreshToken)throw new Error("No refresh token available. Please reconnect.");let e="https://ticktick.com/oauth/token",n=this.settings.clientId,i=this.settings.clientSecret,t=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.settings.refreshToken,client_id:n,client_secret:i});try{let s=await(0,r.requestUrl)({url:e,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:t.toString()});if(s.status===200){let a=s.json;a.refresh_token&&(this.settings.refreshToken=a.refresh_token),this.settings.accessToken=a.access_token,this.settings.tokenExpiry=Date.now()+a.expires_in*1e3*.85,await this.saveSettings(),new r.Notice("Access token refreshed successfully!")}else throw console.error("Refresh token error:",s.text),new Error("Failed to refresh token")}catch(s){throw console.error("Error refreshing access token:",s),s}}async ensureFreshToken(){if(this.settings.tokenExpiry&&Date.now()>this.settings.tokenExpiry){w(this,"token_expired",{tokenExpiry:this.settings.tokenExpiry});try{await this.refreshAccessToken()}catch(e){throw new r.Notice("Reauthentication required. Please reconnect to TickTick."),e}}}async createTicktickTask(e,n){let i=this.settings.accessToken;if(!i)return new r.Notice("No access token found. Please connect to TickTick in the settings."),{success:!1};let t="https://api.ticktick.com/open/v1/task",s={title:e,content:n};try{w(this,"task_creation_request",{endpoint:t,title:e});let a=await(0,r.requestUrl)({url:t,method:"POST",headers:{Authorization:`Bearer ${i}`,"Content-Type":"application/json","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:JSON.stringify(s)});return a.status===200?{success:!0}:(console.error("TickTick API error (status):",a.status),console.log("response.json:",a.json),console.log("response.text:",a.text),{success:!1})}catch(a){return console.error("TickTick API error:",a),{success:!1}}}async loadSettings(){this.settings=Object.assign({},f,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}}; diff --git a/main.ts b/main.ts index b697a8d..b81f579 100644 --- a/main.ts +++ b/main.ts @@ -236,7 +236,7 @@ export default class TickTickPlugin extends Plugin { new Notice('Please enter your Client ID in the settings.'); return; } - const redirectUri = this.settings.redirectUri || 'https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app'; + const redirectUri = (this.settings.redirectUri || 'https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app').trim().replace(/\/+$/, ''); const scope = encodeURIComponent('tasks:read tasks:write'); const { codeVerifier, codeChallenge } = await generatePKCECodes(); const state = generateRandomString(32); @@ -253,7 +253,7 @@ export default class TickTickPlugin extends Plugin { */ async exchangeAuthCodeForToken(code: string): Promise { const tokenEndpoint = 'https://ticktick.com/oauth/token'; - const redirectUri = this.settings.redirectUri || 'https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app'; + const redirectUri = (this.settings.redirectUri || 'https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app').trim().replace(/\/+$/, ''); const clientId = this.settings.clientId; const clientSecret = this.settings.clientSecret; const codeVerifier = this.settings.tempCodeVerifier; diff --git a/manifest.json b/manifest.json index 4677759..b522850 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "ticktick-quickadd-task", "name": "TickTick Quick Add Task", - "version": "1.2.2", + "version": "1.2.3", "minAppVersion": "1.4.0", "description": "Create TickTick tasks from text at your cursor with automatic deep links back to your notes. Requires the Advanced URI plugin.", "author": "Muxin Li", diff --git a/package.json b/package.json index 96b3cc9..b443614 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ticktick-quickadd-task", - "version": "1.2.2", + "version": "1.2.3", "description": "Create TickTick tasks from text at your cursor with automatic deep links back to your notes. Requires the Advanced URI plugin.", "main": "main.js", "scripts": { diff --git a/settings.ts b/settings.ts index 0f993f6..5ff7cff 100644 --- a/settings.ts +++ b/settings.ts @@ -89,13 +89,13 @@ export class TickTickSettingTab extends PluginSettingTab { new Setting(containerEl) .setName('Redirect URI') - .setDesc('Copy this exact value into your TickTick app\'s Redirect URI field in the developer portal. Only change it here if you self-host the callback page.') + .setDesc('Copy this exact value into your TickTick app\'s Redirect URI field in the developer portal. Do not add a trailing slash — the URIs must match exactly. Only change it here if you self-host the callback page.') .addText(text => text .setPlaceholder(DEFAULT_SETTINGS.redirectUri!) .setValue(this.plugin.settings.redirectUri || '') .onChange(async (value) => { - this.plugin.settings.redirectUri = value.trim(); + this.plugin.settings.redirectUri = value.trim().replace(/\/+$/, ''); await this.plugin.saveSettings(); }) ); @@ -241,11 +241,12 @@ export class TickTickSettingTab extends PluginSettingTab { new Setting(details) .setName('Redirect URI') + .setDesc('Must match the value registered in your TickTick app exactly, with no trailing slash.') .addText(text => text .setValue(this.plugin.settings.redirectUri || '') .onChange(async (value) => { - this.plugin.settings.redirectUri = value.trim(); + this.plugin.settings.redirectUri = value.trim().replace(/\/+$/, ''); await this.plugin.saveSettings(); }) ); diff --git a/versions.json b/versions.json index 713c237..a964f83 100644 --- a/versions.json +++ b/versions.json @@ -3,5 +3,6 @@ "1.1.0": "1.4.0", "1.2.0": "1.4.0", "1.2.1": "1.4.0", - "1.2.2": "1.4.0" + "1.2.2": "1.4.0", + "1.2.3": "1.4.0" }