Fix OAuth launch on iOS by presenting auth URL as tappable link

iOS WebKit only allows window.open() to escape to the system browser
when called inside the synchronous portion of a user gesture. The
existing startAuthFlow awaits PKCE hashing and saveSettings before
calling window.open, so by the time it fires iOS has lost the gesture
context and silently blocks the popup — Connect appeared to do nothing.

Replace the programmatic window.open with an AuthLinkModal that
displays the auth URL as a real <a> element. The user's tap on the
link is itself a synchronous gesture, so WebKit hands off to Safari
as expected. Works the same on desktop (Electron treats the link the
same way).
This commit is contained in:
Claude 2026-05-11 01:59:16 +00:00
parent 23eba63ff7
commit 1e919808ea
No known key found for this signature in database
2 changed files with 37 additions and 7 deletions

View file

@ -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 f=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var A=(r,e)=>{for(var t in e)f(r,t,{get:e[t],enumerable:!0})},I=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of _(e))!E.call(r,s)&&s!==t&&f(r,s,{get:()=>e[s],enumerable:!(n=v(e,s))||n.enumerable});return r};var U=r=>I(f({},"__esModule",{value:!0}),r);var j={};A(j,{default:()=>k});module.exports=U(j);var y=require("obsidian"),o=require("obsidian");var c=require("obsidian"),w={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,t){super(e,t),this.plugin=t}display(){let{containerEl:e}=this;e.empty(),new c.Setting(e).setName("Client id").setDesc("Enter your TickTick Client ID from the Developer Console.").addText(t=>t.setPlaceholder("Your Client ID").setValue(this.plugin.settings.clientId||"").onChange(async n=>{this.plugin.settings.clientId=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Client secret").setDesc("Enter your TickTick Client Secret (input is masked).").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 n=>{this.plugin.settings.clientSecret=n.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(e).setName("Redirect URI").setDesc("Enter the Redirect URI registered in your TickTick Developer Portal. (Default: https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app)").addText(t=>t.setPlaceholder("https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").setValue(this.plugin.settings.redirectUri||"").onChange(async n=>{this.plugin.settings.redirectUri=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Selection mode").setDesc("Choose whether the command captures 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 n=>{this.plugin.settings.selectionMode=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Tag position").setDesc("Choose whether the #ticktick tag is added to the beginning or end of the text.").addDropdown(t=>t.addOption("append","Append (end)").addOption("prepend","Prepend (beginning)").setValue(this.plugin.settings.tagPosition).onChange(async n=>{this.plugin.settings.tagPosition=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Access token").setDesc("Your current access token (read-only).").addText(t=>t.setPlaceholder("Access token will appear here").setValue(this.plugin.settings.accessToken||"").onChange(async n=>{this.plugin.settings.accessToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Refresh token").setDesc("Stored refresh token (for debugging purposes).").addText(t=>t.setPlaceholder("Refresh token").setValue(this.plugin.settings.refreshToken||"").onChange(async n=>{this.plugin.settings.refreshToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Connect to TickTick").setDesc("Opens the OAuth authorization page in your browser. After approving, you will be returned to Obsidian automatically.").addButton(async t=>{t.setButtonText("Connect").onClick(async()=>{await this.plugin.startAuthFlow()})})}};function m(r){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t="";for(let n=0;n<r;n++)t+=e.charAt(Math.floor(Math.random()*e.length));return t}function D(r,e){return{text:r.getLine(e),line:e}}function N(r,e){let t=e,n=e,s=r.lineCount();for(;t>0&&r.getLine(t-1).trim()!=="";)t--;for(;n<s-1&&r.getLine(n+1).trim()!=="";)n++;let a="";for(let i=t;i<=n;i++)a+=r.getLine(i)+`
`;return{text:a.trim(),start:t,end:n}}async function R(){let r=m(64),t=new TextEncoder().encode(r),n=await crypto.subtle.digest("SHA-256",t),s=(0,y.arrayBufferToBase64)(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return{codeVerifier:r,codeChallenge:s}}function T(r,e,t){console.log(JSON.stringify({timestamp:new Date().toISOString(),plugin:"ticktick-integration",action:e,...t,clientId:r.settings.clientId?"***":void 0,accessToken:r.settings.accessToken?"***":void 0}))}var k=class extends o.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 o.Notice("TickTick auth failed: no authorization code returned.");return}if(!this.settings.tempState||e.state!==this.settings.tempState){new o.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",editorCallback:async(e,t)=>{let n=e.getCursor();if(!t.file){new o.Notice("No file found for the current view!");return}let s,a,i;if(this.settings.selectionMode==="paragraph"){let{text:g,start:p,end:P}=N(e,n.line);s=g,a=p,i=P}else{let{text:g,line:p}=D(e,n.line);s=g,a=p,i=p}if(!s.trim()){new o.Notice("No text found on the current line!");return}let d=m(8),l=this.settings.tagPosition==="prepend"?`#ticktick ${s} ^${d}`:`${s} #ticktick ^${d}`;e.replaceRange(l,{line:a,ch:0},{line:i,ch:e.getLine(i).length});let h=this.app.vault.getName(),S=t.file.path,b=`obsidian://advanced-uri?vault=${encodeURIComponent(h)}&filepath=${encodeURIComponent(S)}&block=${encodeURIComponent(d)}`,x=`${s}
var f=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var U=(r,e)=>{for(var t in e)f(r,t,{get:e[t],enumerable:!0})},I=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of _(e))!A.call(r,s)&&s!==t&&f(r,s,{get:()=>e[s],enumerable:!(n=E(e,s))||n.enumerable});return r};var D=r=>I(f({},"__esModule",{value:!0}),r);var j={};U(j,{default:()=>k});module.exports=D(j);var S=require("obsidian"),o=require("obsidian");var c=require("obsidian"),y={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,t){super(e,t),this.plugin=t}display(){let{containerEl:e}=this;e.empty(),new c.Setting(e).setName("Client id").setDesc("Enter your TickTick Client ID from the Developer Console.").addText(t=>t.setPlaceholder("Your Client ID").setValue(this.plugin.settings.clientId||"").onChange(async n=>{this.plugin.settings.clientId=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Client secret").setDesc("Enter your TickTick Client Secret (input is masked).").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 n=>{this.plugin.settings.clientSecret=n.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(e).setName("Redirect URI").setDesc("Enter the Redirect URI registered in your TickTick Developer Portal. (Default: https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app)").addText(t=>t.setPlaceholder("https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").setValue(this.plugin.settings.redirectUri||"").onChange(async n=>{this.plugin.settings.redirectUri=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Selection mode").setDesc("Choose whether the command captures 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 n=>{this.plugin.settings.selectionMode=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Tag position").setDesc("Choose whether the #ticktick tag is added to the beginning or end of the text.").addDropdown(t=>t.addOption("append","Append (end)").addOption("prepend","Prepend (beginning)").setValue(this.plugin.settings.tagPosition).onChange(async n=>{this.plugin.settings.tagPosition=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Access token").setDesc("Your current access token (read-only).").addText(t=>t.setPlaceholder("Access token will appear here").setValue(this.plugin.settings.accessToken||"").onChange(async n=>{this.plugin.settings.accessToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Refresh token").setDesc("Stored refresh token (for debugging purposes).").addText(t=>t.setPlaceholder("Refresh token").setValue(this.plugin.settings.refreshToken||"").onChange(async n=>{this.plugin.settings.refreshToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Connect to TickTick").setDesc("Opens the OAuth authorization page in your browser. After approving, you will be returned to Obsidian automatically.").addButton(async t=>{t.setButtonText("Connect").onClick(async()=>{await this.plugin.startAuthFlow()})})}};function m(r){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t="";for(let n=0;n<r;n++)t+=e.charAt(Math.floor(Math.random()*e.length));return t}function N(r,e){return{text:r.getLine(e),line:e}}function R(r,e){let t=e,n=e,s=r.lineCount();for(;t>0&&r.getLine(t-1).trim()!=="";)t--;for(;n<s-1&&r.getLine(n+1).trim()!=="";)n++;let a="";for(let i=t;i<=n;i++)a+=r.getLine(i)+`
`;return{text:a.trim(),start:t,end:n}}async function O(){let r=m(64),t=new TextEncoder().encode(r),n=await crypto.subtle.digest("SHA-256",t),s=(0,S.arrayBufferToBase64)(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return{codeVerifier:r,codeChallenge:s}}var w=class extends o.Modal{constructor(t,n){super(t);this.authUrl=n}onOpen(){let{contentEl:t}=this;t.empty(),t.createEl("h2",{text:"Authorize TickTick"}),t.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 n=t.createEl("a",{text:"Open TickTick login",href:this.authUrl});n.setAttr("target","_blank"),n.setAttr("rel","noopener"),n.style.display="inline-block",n.style.marginTop="0.5em",n.style.fontWeight="bold"}onClose(){this.contentEl.empty()}};function T(r,e,t){console.log(JSON.stringify({timestamp:new Date().toISOString(),plugin:"ticktick-integration",action:e,...t,clientId:r.settings.clientId?"***":void 0,accessToken:r.settings.accessToken?"***":void 0}))}var k=class extends o.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 o.Notice("TickTick auth failed: no authorization code returned.");return}if(!this.settings.tempState||e.state!==this.settings.tempState){new o.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",editorCallback:async(e,t)=>{let n=e.getCursor();if(!t.file){new o.Notice("No file found for the current view!");return}let s,a,i;if(this.settings.selectionMode==="paragraph"){let{text:h,start:p,end:P}=R(e,n.line);s=h,a=p,i=P}else{let{text:h,line:p}=N(e,n.line);s=h,a=p,i=p}if(!s.trim()){new o.Notice("No text found on the current line!");return}let d=m(8),l=this.settings.tagPosition==="prepend"?`#ticktick ${s} ^${d}`:`${s} #ticktick ^${d}`;e.replaceRange(l,{line:a,ch:0},{line:i,ch:e.getLine(i).length});let g=this.app.vault.getName(),b=t.file.path,x=`obsidian://advanced-uri?vault=${encodeURIComponent(g)}&filepath=${encodeURIComponent(b)}&block=${encodeURIComponent(d)}`,C=`${s}
[Open in Obsidian](${b})`,C=s.length>50?s.substring(0,50)+"...":s;await this.ensureFreshToken();try{(await this.createTicktickTask(C,x)).success?new o.Notice("TickTick task created successfully!"):new o.Notice("Failed to create TickTick task.")}catch(g){T(this,"task_creation_failed",{error:g.message}),new o.Notice(`Failed to create task: ${g.message}
Check console for details.`)}}})}onunload(){console.log("Unloading TickTick Plugin")}async startAuthFlow(){let e=this.settings.clientId;if(!e){new o.Notice("Please enter your Client ID in the settings.");return}let t=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",n=encodeURIComponent("tasks:read tasks:write"),{codeVerifier:s,codeChallenge:a}=await R(),i=m(32);this.settings.tempCodeVerifier=s,this.settings.tempState=i,await this.saveSettings();let d=`https://ticktick.com/oauth/authorize?client_id=${e}&redirect_uri=${encodeURIComponent(t)}&response_type=code&scope=${n}&code_challenge=${a}&code_challenge_method=S256&state=${i}`;window.open(d,"_blank"),new o.Notice("OAuth flow initiated. Please complete it in your browser.")}async exchangeAuthCodeForToken(e){let t="https://ticktick.com/oauth/token",n=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",s=this.settings.clientId,a=this.settings.clientSecret,i=this.settings.tempCodeVerifier;if(!i||!s||!a){new o.Notice("Missing required credentials. Please update your settings.");return}let d=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:n,client_id:s,client_secret:a,code_verifier:i});try{let l=await(0,o.requestUrl)({url:t,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 h=l.json;h.refresh_token?this.settings.refreshToken=h.refresh_token:(console.warn("Refresh token not received during token exchange."),new o.Notice("Refresh token not received. Re-authentication may be required more often.")),this.settings.accessToken=h.access_token,this.settings.tokenExpiry=Date.now()+h.expires_in*1e3*.85,this.settings.tempCodeVerifier=void 0,this.settings.tempState=void 0,await this.saveSettings(),new o.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 o.Notice("Failed to obtain access token.")}catch(l){console.error("Error during token exchange:",l),new o.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",t=this.settings.clientId,n=this.settings.clientSecret,s=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.settings.refreshToken,client_id:t,client_secret:n});try{let a=await(0,o.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:s.toString()});if(a.status===200){let i=a.json;i.refresh_token&&(this.settings.refreshToken=i.refresh_token),this.settings.accessToken=i.access_token,this.settings.tokenExpiry=Date.now()+i.expires_in*1e3*.85,await this.saveSettings(),new o.Notice("Access token refreshed successfully!")}else throw console.error("Refresh token error:",a.text),new Error("Failed to refresh token")}catch(a){throw console.error("Error refreshing access token:",a),a}}async ensureFreshToken(){if(this.settings.tokenExpiry&&Date.now()>this.settings.tokenExpiry){T(this,"token_expired",{tokenExpiry:this.settings.tokenExpiry});try{await this.refreshAccessToken()}catch(e){throw new o.Notice("Reauthentication required. Please reconnect to TickTick."),e}}}async createTicktickTask(e,t){let n=this.settings.accessToken;if(!n)return new o.Notice("No access token found. Please connect to TickTick in the settings."),{success:!1};let s="https://api.ticktick.com/open/v1/task",a={title:e,content:t};try{T(this,"task_creation_request",{endpoint:s,title:e});let i=await(0,o.requestUrl)({url:s,method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:JSON.stringify(a)});return i.status===200?{success:!0}:(console.error("TickTick API error (status):",i.status),console.log("response.json:",i.json),console.log("response.text:",i.text),{success:!1})}catch(i){return console.error("TickTick API error:",i),{success:!1}}}async loadSettings(){this.settings=Object.assign({},w,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}};
[Open in Obsidian](${x})`,v=s.length>50?s.substring(0,50)+"...":s;await this.ensureFreshToken();try{(await this.createTicktickTask(v,C)).success?new o.Notice("TickTick task created successfully!"):new o.Notice("Failed to create TickTick task.")}catch(h){T(this,"task_creation_failed",{error:h.message}),new o.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 o.Notice("Please enter your Client ID in the settings.");return}let t=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",n=encodeURIComponent("tasks:read tasks:write"),{codeVerifier:s,codeChallenge:a}=await O(),i=m(32);this.settings.tempCodeVerifier=s,this.settings.tempState=i,await this.saveSettings();let d=`https://ticktick.com/oauth/authorize?client_id=${e}&redirect_uri=${encodeURIComponent(t)}&response_type=code&scope=${n}&code_challenge=${a}&code_challenge_method=S256&state=${i}`;new w(this.app,d).open()}async exchangeAuthCodeForToken(e){let t="https://ticktick.com/oauth/token",n=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",s=this.settings.clientId,a=this.settings.clientSecret,i=this.settings.tempCodeVerifier;if(!i||!s||!a){new o.Notice("Missing required credentials. Please update your settings.");return}let d=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:n,client_id:s,client_secret:a,code_verifier:i});try{let l=await(0,o.requestUrl)({url:t,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 g=l.json;g.refresh_token?this.settings.refreshToken=g.refresh_token:(console.warn("Refresh token not received during token exchange."),new o.Notice("Refresh token not received. Re-authentication may be required more often.")),this.settings.accessToken=g.access_token,this.settings.tokenExpiry=Date.now()+g.expires_in*1e3*.85,this.settings.tempCodeVerifier=void 0,this.settings.tempState=void 0,await this.saveSettings(),new o.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 o.Notice("Failed to obtain access token.")}catch(l){console.error("Error during token exchange:",l),new o.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",t=this.settings.clientId,n=this.settings.clientSecret,s=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.settings.refreshToken,client_id:t,client_secret:n});try{let a=await(0,o.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:s.toString()});if(a.status===200){let i=a.json;i.refresh_token&&(this.settings.refreshToken=i.refresh_token),this.settings.accessToken=i.access_token,this.settings.tokenExpiry=Date.now()+i.expires_in*1e3*.85,await this.saveSettings(),new o.Notice("Access token refreshed successfully!")}else throw console.error("Refresh token error:",a.text),new Error("Failed to refresh token")}catch(a){throw console.error("Error refreshing access token:",a),a}}async ensureFreshToken(){if(this.settings.tokenExpiry&&Date.now()>this.settings.tokenExpiry){T(this,"token_expired",{tokenExpiry:this.settings.tokenExpiry});try{await this.refreshAccessToken()}catch(e){throw new o.Notice("Reauthentication required. Please reconnect to TickTick."),e}}}async createTicktickTask(e,t){let n=this.settings.accessToken;if(!n)return new o.Notice("No access token found. Please connect to TickTick in the settings."),{success:!1};let s="https://api.ticktick.com/open/v1/task",a={title:e,content:t};try{T(this,"task_creation_request",{endpoint:s,title:e});let i=await(0,o.requestUrl)({url:s,method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:JSON.stringify(a)});return i.status===200?{success:!0}:(console.error("TickTick API error (status):",i.status),console.log("response.json:",i.json),console.log("response.text:",i.text),{success:!1})}catch(i){return console.error("TickTick API error:",i),{success:!1}}}async loadSettings(){this.settings=Object.assign({},y,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}};

36
main.ts
View file

@ -1,5 +1,5 @@
import { arrayBufferToBase64 } from 'obsidian';
import { App, Editor, MarkdownView, Notice, Plugin, requestUrl } from 'obsidian';
import { App, Editor, MarkdownView, Modal, Notice, Plugin, requestUrl } from 'obsidian';
import { TickTickSettingTab, TickTickSettings, DEFAULT_SETTINGS } from './settings';
// Helper function to generate a random alphanumeric string (for block anchors and state)
@ -62,6 +62,37 @@ async function generatePKCECodes(): Promise<{ codeVerifier: string; codeChalleng
return { codeVerifier, codeChallenge };
}
// Modal that presents the OAuth URL as a tappable link.
// iOS blocks programmatic window.open after async work loses the user gesture,
// so we let the user tap the link directly — that tap is itself the gesture.
class AuthLinkModal extends Modal {
constructor(app: App, private authUrl: string) {
super(app);
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl('h2', { text: 'Authorize TickTick' });
contentEl.createEl('p', {
text: 'Tap the link below to open the TickTick login page. After you approve access, you will be returned to Obsidian automatically.'
});
const link = contentEl.createEl('a', {
text: 'Open TickTick login',
href: this.authUrl
});
link.setAttr('target', '_blank');
link.setAttr('rel', 'noopener');
link.style.display = 'inline-block';
link.style.marginTop = '0.5em';
link.style.fontWeight = 'bold';
}
onClose() {
this.contentEl.empty();
}
}
// Structured logging helper
function log(plugin: TickTickPlugin, action: string, metadata?: Record<string, unknown>) {
console.log(JSON.stringify({
@ -181,8 +212,7 @@ export default class TickTickPlugin extends Plugin {
this.settings.tempState = state;
await this.saveSettings();
const authUrl = `https://ticktick.com/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256&state=${state}`;
window.open(authUrl, '_blank');
new Notice('OAuth flow initiated. Please complete it in your browser.');
new AuthLinkModal(this.app, authUrl).open();
}
/**