Compare commits

...

2 commits
1.2.1 ... main

Author SHA1 Message Date
Claude
0c25564337
Normalize redirect URI to fix OAuth invalid_grant (1.2.3)
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
2026-06-05 16:16:26 +00:00
Claude
c036f5ab67
Strip Markdown from TickTick task titles (1.2.2)
The task title was built verbatim from the source line, so list/checkbox
markers (- [ ]), bold/italic, links, headings, etc. leaked into TickTick.
Add a stripMarkdown() helper that cleans the title to plain text before
truncation:

- Removes leading list/checkbox/heading/blockquote/ordered markers
- Unwraps links and wiki links to their text
- Strips asterisk emphasis, ~~strike~~, ==highlight==, `code` (matched or
  dangling, e.g. a leading ** with no closing pair)
- Leaves single/snake_case underscores intact

The task description still includes the original text and the Open in
Obsidian link unchanged.

https://claude.ai/code/session_01V2fsiatACzjUCNwSEhwER9
2026-06-04 20:48:21 +00:00
7 changed files with 45 additions and 13 deletions

View file

@ -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.

File diff suppressed because one or more lines are too long

34
main.ts
View file

@ -37,6 +37,33 @@ function getParagraph(editor: Editor, currentLine: number): { text: string, star
return { text: paragraphText.trim(), start, end };
}
// Strip Markdown formatting so the TickTick task title is plain text.
// Removes leading list/checkbox/heading/quote markers and inline emphasis,
// keeping the inner text. Leaves single underscores alone to preserve
// snake_case identifiers.
function stripMarkdown(text: string): string {
return text
// Images: ![alt](url) -> alt
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
// Markdown links: [text](url) -> text
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
// Wiki links: [[page|alias]] -> alias, [[page]] -> page
.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '$2')
.replace(/\[\[([^\]]+)\]\]/g, '$1')
// Leading blockquote / heading / list / checkbox / ordered-list markers
.replace(/^\s*(?:>\s*|#{1,6}\s+|[-*+]\s+\[[ xX]\]\s+|[-*+]\s+|\d+\.\s+)+/, '')
// Bold/italic via underscores: only strip matched pairs, so
// snake_case identifiers are left intact.
.replace(/__([^_]+)__/g, '$1')
// Asterisk emphasis, highlight, strikethrough, inline code:
// remove the markers whether or not they're balanced (a dangling
// "**" at the start of a line should still be stripped).
.replace(/\*\*?|~~|==|`/g, '')
// Collapse remaining whitespace/newlines
.replace(/\s+/g, ' ')
.trim();
}
// Helper function to generate PKCE codes: codeVerifier and corresponding codeChallenge
async function generatePKCECodes(): Promise<{ codeVerifier: string; codeChallenge: string }> {
const codeVerifier = generateRandomString(64); // between 43 and 128 characters
@ -177,7 +204,8 @@ export default class TickTickPlugin extends Plugin {
const filePath = view.file.path;
const advancedUri = `obsidian://advanced-uri?vault=${encodeURIComponent(vaultName)}&filepath=${encodeURIComponent(filePath)}&block=${encodeURIComponent(blockId)}`;
const taskDescription = `${taskText}\n\n[Open in Obsidian](${advancedUri})`;
const taskTitle = taskText.length > 50 ? taskText.substring(0, 50) + "..." : taskText;
const cleanTitle = stripMarkdown(taskText);
const taskTitle = cleanTitle.length > 50 ? cleanTitle.substring(0, 50) + "..." : cleanTitle;
await this.ensureFreshToken();
@ -208,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);
@ -225,7 +253,7 @@ export default class TickTickPlugin extends Plugin {
*/
async exchangeAuthCodeForToken(code: string): Promise<void> {
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;

View file

@ -1,7 +1,7 @@
{
"id": "ticktick-quickadd-task",
"name": "TickTick Quick Add Task",
"version": "1.2.1",
"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",

View file

@ -1,6 +1,6 @@
{
"name": "ticktick-quickadd-task",
"version": "1.2.1",
"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": {

View file

@ -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();
})
);

View file

@ -2,5 +2,7 @@
"1.0.0": "0.15.0",
"1.1.0": "1.4.0",
"1.2.0": "1.4.0",
"1.2.1": "1.4.0"
"1.2.1": "1.4.0",
"1.2.2": "1.4.0",
"1.2.3": "1.4.0"
}