@choochmeque/tauri-apple-extensions 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -27
- package/dist/cli.js +29 -42
- package/dist/core/project-yml.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +64 -42
- package/package.json +11 -14
- package/templates/share/ShareViewController.swift +69 -189
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ Add iOS extensions to Tauri apps with a single command.
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
10
|
- Automatic Xcode project configuration via XcodeGen
|
|
11
|
-
-
|
|
11
|
+
- Built-in skeleton templates with TODO markers
|
|
12
|
+
- Plugin-based template system for custom implementations
|
|
12
13
|
- Idempotent - safe to re-run
|
|
13
|
-
- Share Extension support (more extension types coming soon)
|
|
14
14
|
|
|
15
15
|
## Prerequisites
|
|
16
16
|
|
|
@@ -20,34 +20,57 @@ Add iOS extensions to Tauri apps with a single command.
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
+
# npm
|
|
23
24
|
npm install -D @choochmeque/tauri-apple-extensions
|
|
24
|
-
```
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# pnpm
|
|
27
|
+
pnpm add -D @choochmeque/tauri-apple-extensions
|
|
28
|
+
|
|
29
|
+
# yarn
|
|
30
|
+
yarn add -D @choochmeque/tauri-apple-extensions
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
# bun
|
|
33
|
+
bun add -D @choochmeque/tauri-apple-extensions
|
|
34
|
+
```
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
## Usage
|
|
31
37
|
|
|
32
38
|
```bash
|
|
33
|
-
npx @choochmeque/tauri-apple-extensions add share
|
|
39
|
+
npx @choochmeque/tauri-apple-extensions add share
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
This creates a Share Extension with a minimal skeleton template. Open the generated Swift file and implement your logic where you see `// TODO:` comments.
|
|
43
|
+
|
|
44
|
+
### Options
|
|
37
45
|
|
|
38
46
|
```bash
|
|
47
|
+
# Use templates from a plugin (plugin must include tauri-apple-extension config)
|
|
48
|
+
npx @choochmeque/tauri-apple-extensions add share --plugin <plugin-name>
|
|
49
|
+
|
|
50
|
+
# Use custom templates directory
|
|
39
51
|
npx @choochmeque/tauri-apple-extensions add share --templates ./path/to/templates
|
|
40
52
|
```
|
|
41
53
|
|
|
54
|
+
> **Note:** When using `--plugin`, the plugin's `package.json` must contain a `tauri-apple-extension` config. See [For Plugin Developers](#for-plugin-developers) below.
|
|
55
|
+
|
|
42
56
|
## Supported Extensions
|
|
43
57
|
|
|
44
|
-
| Type |
|
|
45
|
-
|
|
46
|
-
| `share` |
|
|
58
|
+
| Type | Description |
|
|
59
|
+
|------|-------------|
|
|
60
|
+
| `share` | Share Extension for receiving shared content |
|
|
61
|
+
|
|
62
|
+
## Post-Setup Steps
|
|
63
|
+
|
|
64
|
+
After running the tool:
|
|
65
|
+
|
|
66
|
+
1. Open the Xcode project (`src-tauri/gen/apple/*.xcodeproj`)
|
|
67
|
+
2. Select your Apple Developer Team for both targets
|
|
68
|
+
3. Enable **App Groups** capability for both targets
|
|
69
|
+
4. Configure App Groups in [Apple Developer Portal](https://developer.apple.com/account/resources/identifiers/list/applicationGroup)
|
|
47
70
|
|
|
48
71
|
## For Plugin Developers
|
|
49
72
|
|
|
50
|
-
To make your plugin compatible
|
|
73
|
+
To make your plugin compatible, add to your `package.json`:
|
|
51
74
|
|
|
52
75
|
```json
|
|
53
76
|
{
|
|
@@ -58,10 +81,6 @@ To make your plugin compatible with this tool, add the following to your `packag
|
|
|
58
81
|
}
|
|
59
82
|
```
|
|
60
83
|
|
|
61
|
-
Your templates directory should contain:
|
|
62
|
-
- Swift source files with `{{VARIABLE}}` placeholders
|
|
63
|
-
- `Info.plist` for the extension
|
|
64
|
-
|
|
65
84
|
### Template Variables
|
|
66
85
|
|
|
67
86
|
| Variable | Description |
|
|
@@ -72,19 +91,10 @@ Your templates directory should contain:
|
|
|
72
91
|
| `{{BUNDLE_IDENTIFIER}}` | Extension bundle identifier |
|
|
73
92
|
| `{{PRODUCT_NAME}}` | App product name |
|
|
74
93
|
|
|
75
|
-
## Post-Setup Steps
|
|
76
|
-
|
|
77
|
-
After running the tool:
|
|
78
|
-
|
|
79
|
-
1. Open the Xcode project (`src-tauri/gen/apple/*.xcodeproj`)
|
|
80
|
-
2. Select your Apple Developer Team for both targets
|
|
81
|
-
3. Enable required capabilities (e.g., App Groups) for both targets
|
|
82
|
-
4. Configure the capability in Apple Developer Portal
|
|
83
|
-
|
|
84
94
|
## Contributing
|
|
85
95
|
|
|
86
|
-
PRs
|
|
96
|
+
PRs welcome! Please open an issue first to discuss what you would like to change.
|
|
87
97
|
|
|
88
98
|
## License
|
|
89
99
|
|
|
90
|
-
MIT
|
|
100
|
+
[MIT](LICENSE)
|
package/dist/cli.js
CHANGED
|
@@ -118,6 +118,25 @@ function addExtensionTarget(projectYml, targetConfig) {
|
|
|
118
118
|
}
|
|
119
119
|
return modified;
|
|
120
120
|
}
|
|
121
|
+
function addUrlSchemeToTarget(projectYml, targetName, urlScheme, bundleIdentifier) {
|
|
122
|
+
let modified = projectYml;
|
|
123
|
+
// Check if URL scheme already exists
|
|
124
|
+
if (modified.includes(`CFBundleURLSchemes`) && modified.includes(urlScheme)) {
|
|
125
|
+
return modified;
|
|
126
|
+
}
|
|
127
|
+
// Find the target's info section and add URL scheme
|
|
128
|
+
const targetRegex = new RegExp(`(${targetName}:[\\s\\S]*?info:[\\s\\S]*?properties:)([\\s\\S]*?)(\\n \\w|\\n \\w|$)`, "m");
|
|
129
|
+
const match = modified.match(targetRegex);
|
|
130
|
+
if (match) {
|
|
131
|
+
const urlSchemeYaml = `
|
|
132
|
+
CFBundleURLTypes:
|
|
133
|
+
- CFBundleURLName: ${bundleIdentifier}
|
|
134
|
+
CFBundleURLSchemes:
|
|
135
|
+
- ${urlScheme}`;
|
|
136
|
+
modified = modified.replace(targetRegex, `$1$2${urlSchemeYaml}$3`);
|
|
137
|
+
}
|
|
138
|
+
return modified;
|
|
139
|
+
}
|
|
121
140
|
function addDependencyToTarget(projectYml, mainTargetName, dependencyConfig) {
|
|
122
141
|
let modified = projectYml;
|
|
123
142
|
// Remove existing dependency if it exists
|
|
@@ -189,41 +208,6 @@ function createExtensionEntitlements(extensionDir, appGroupId) {
|
|
|
189
208
|
return entitlementsPath;
|
|
190
209
|
}
|
|
191
210
|
|
|
192
|
-
function addUrlSchemeToInfoPlist(appleDir, appInfo) {
|
|
193
|
-
const targetName = `${appInfo.productName}_iOS`;
|
|
194
|
-
const infoPlistPath = path.join(appleDir, targetName, "Info.plist");
|
|
195
|
-
const urlScheme = appInfo.productName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
196
|
-
if (!fs.existsSync(infoPlistPath)) {
|
|
197
|
-
console.log(`Info.plist not found at ${infoPlistPath}, skipping URL scheme setup`);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
let infoPlist = fs.readFileSync(infoPlistPath, "utf8");
|
|
201
|
-
// Check if URL schemes already configured
|
|
202
|
-
if (infoPlist.includes("CFBundleURLSchemes")) {
|
|
203
|
-
if (!infoPlist.includes(urlScheme)) {
|
|
204
|
-
console.log(`URL scheme may need manual configuration. Add '${urlScheme}' to CFBundleURLSchemes.`);
|
|
205
|
-
}
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
// Add URL scheme - need to insert before closing </dict></plist>
|
|
209
|
-
const urlSchemeEntry = ` <key>CFBundleURLTypes</key>
|
|
210
|
-
<array>
|
|
211
|
-
<dict>
|
|
212
|
-
<key>CFBundleURLSchemes</key>
|
|
213
|
-
<array>
|
|
214
|
-
<string>${urlScheme}</string>
|
|
215
|
-
</array>
|
|
216
|
-
<key>CFBundleURLName</key>
|
|
217
|
-
<string>${appInfo.identifier}</string>
|
|
218
|
-
</dict>
|
|
219
|
-
</array>
|
|
220
|
-
`;
|
|
221
|
-
// Insert before the last </dict>
|
|
222
|
-
infoPlist = infoPlist.replace(/(\s*)<\/dict>\s*<\/plist>/, `\n${urlSchemeEntry}$1</dict>\n</plist>`);
|
|
223
|
-
fs.writeFileSync(infoPlistPath, infoPlist);
|
|
224
|
-
console.log(`Added URL scheme '${urlScheme}' to Info.plist`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
211
|
function runXcodeGen(appleDir) {
|
|
228
212
|
try {
|
|
229
213
|
console.log("Running xcodegen to regenerate project...");
|
|
@@ -405,6 +389,11 @@ const shareExtension = {
|
|
|
405
389
|
modified = addDependencyToTarget(modified, targetName, {
|
|
406
390
|
target: extensionName,
|
|
407
391
|
});
|
|
392
|
+
// Add URL scheme to main app for deep linking from extension
|
|
393
|
+
const urlScheme = appInfo.productName
|
|
394
|
+
.toLowerCase()
|
|
395
|
+
.replace(/[^a-z0-9]/g, "");
|
|
396
|
+
modified = addUrlSchemeToTarget(modified, targetName, urlScheme, appInfo.identifier);
|
|
408
397
|
return modified;
|
|
409
398
|
},
|
|
410
399
|
};
|
|
@@ -440,8 +429,8 @@ function resolveTemplatesDir(options, extensionType) {
|
|
|
440
429
|
}
|
|
441
430
|
return templatesPath;
|
|
442
431
|
}
|
|
443
|
-
// Option 3: Default templates
|
|
444
|
-
const defaultTemplates = path.join(__dirname$1, "
|
|
432
|
+
// Option 3: Default templates (from bundled dist/cli.js -> ../templates)
|
|
433
|
+
const defaultTemplates = path.join(__dirname$1, "../templates", extensionType);
|
|
445
434
|
if (!fs.existsSync(defaultTemplates)) {
|
|
446
435
|
throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
|
|
447
436
|
}
|
|
@@ -496,13 +485,11 @@ async function addExtension(type, options) {
|
|
|
496
485
|
extension.createFiles(appleDir, appInfo, templatesDir);
|
|
497
486
|
console.log(`\n2. Updating main app entitlements...`);
|
|
498
487
|
updateMainAppEntitlements(appleDir, appInfo);
|
|
499
|
-
console.log(`\n3.
|
|
500
|
-
addUrlSchemeToInfoPlist(appleDir, appInfo);
|
|
501
|
-
console.log(`\n4. Updating project.yml...`);
|
|
488
|
+
console.log(`\n3. Updating project.yml (extension target + URL scheme)...`);
|
|
502
489
|
projectYml = readProjectYml(appleDir);
|
|
503
490
|
projectYml = extension.updateProjectYml(projectYml, appInfo);
|
|
504
491
|
writeProjectYml(appleDir, projectYml);
|
|
505
|
-
console.log(`\
|
|
492
|
+
console.log(`\n4. Regenerating Xcode project...`);
|
|
506
493
|
runXcodeGen(appleDir);
|
|
507
494
|
console.log(`\n========================================`);
|
|
508
495
|
console.log(`${extension.displayName} setup complete!`);
|
|
@@ -526,7 +513,7 @@ const program = new Command();
|
|
|
526
513
|
program
|
|
527
514
|
.name("tauri-apple-extensions")
|
|
528
515
|
.description("Add iOS extensions to Tauri apps")
|
|
529
|
-
.version("0.1.
|
|
516
|
+
.version("0.1.2");
|
|
530
517
|
program
|
|
531
518
|
.command("add <type>")
|
|
532
519
|
.description("Add an extension (e.g., share)")
|
|
@@ -2,4 +2,5 @@ import type { TargetConfig, DependencyConfig } from "../types.js";
|
|
|
2
2
|
export declare function readProjectYml(appleDir: string): string;
|
|
3
3
|
export declare function writeProjectYml(appleDir: string, content: string): void;
|
|
4
4
|
export declare function addExtensionTarget(projectYml: string, targetConfig: TargetConfig): string;
|
|
5
|
+
export declare function addUrlSchemeToTarget(projectYml: string, targetName: string, urlScheme: string, bundleIdentifier: string): string;
|
|
5
6
|
export declare function addDependencyToTarget(projectYml: string, mainTargetName: string, dependencyConfig: DependencyConfig): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { addExtension } from "./commands/add.js";
|
|
2
2
|
export { findProjectRoot, findTauriConfig, findAppleProjectDir, getAppInfo, } from "./core/project-discovery.js";
|
|
3
|
-
export { readProjectYml, writeProjectYml } from "./core/project-yml.js";
|
|
3
|
+
export { readProjectYml, writeProjectYml, addUrlSchemeToTarget, } from "./core/project-yml.js";
|
|
4
4
|
export { updateMainAppEntitlements, createExtensionEntitlements, } from "./core/entitlements.js";
|
|
5
5
|
export { addUrlSchemeToInfoPlist } from "./core/info-plist.js";
|
|
6
6
|
export { runXcodeGen } from "./core/xcodegen.js";
|
package/dist/index.js
CHANGED
|
@@ -116,6 +116,25 @@ function addExtensionTarget(projectYml, targetConfig) {
|
|
|
116
116
|
}
|
|
117
117
|
return modified;
|
|
118
118
|
}
|
|
119
|
+
function addUrlSchemeToTarget(projectYml, targetName, urlScheme, bundleIdentifier) {
|
|
120
|
+
let modified = projectYml;
|
|
121
|
+
// Check if URL scheme already exists
|
|
122
|
+
if (modified.includes(`CFBundleURLSchemes`) && modified.includes(urlScheme)) {
|
|
123
|
+
return modified;
|
|
124
|
+
}
|
|
125
|
+
// Find the target's info section and add URL scheme
|
|
126
|
+
const targetRegex = new RegExp(`(${targetName}:[\\s\\S]*?info:[\\s\\S]*?properties:)([\\s\\S]*?)(\\n \\w|\\n \\w|$)`, "m");
|
|
127
|
+
const match = modified.match(targetRegex);
|
|
128
|
+
if (match) {
|
|
129
|
+
const urlSchemeYaml = `
|
|
130
|
+
CFBundleURLTypes:
|
|
131
|
+
- CFBundleURLName: ${bundleIdentifier}
|
|
132
|
+
CFBundleURLSchemes:
|
|
133
|
+
- ${urlScheme}`;
|
|
134
|
+
modified = modified.replace(targetRegex, `$1$2${urlSchemeYaml}$3`);
|
|
135
|
+
}
|
|
136
|
+
return modified;
|
|
137
|
+
}
|
|
119
138
|
function addDependencyToTarget(projectYml, mainTargetName, dependencyConfig) {
|
|
120
139
|
let modified = projectYml;
|
|
121
140
|
// Remove existing dependency if it exists
|
|
@@ -187,41 +206,6 @@ function createExtensionEntitlements(extensionDir, appGroupId) {
|
|
|
187
206
|
return entitlementsPath;
|
|
188
207
|
}
|
|
189
208
|
|
|
190
|
-
function addUrlSchemeToInfoPlist(appleDir, appInfo) {
|
|
191
|
-
const targetName = `${appInfo.productName}_iOS`;
|
|
192
|
-
const infoPlistPath = path.join(appleDir, targetName, "Info.plist");
|
|
193
|
-
const urlScheme = appInfo.productName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
194
|
-
if (!fs.existsSync(infoPlistPath)) {
|
|
195
|
-
console.log(`Info.plist not found at ${infoPlistPath}, skipping URL scheme setup`);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
let infoPlist = fs.readFileSync(infoPlistPath, "utf8");
|
|
199
|
-
// Check if URL schemes already configured
|
|
200
|
-
if (infoPlist.includes("CFBundleURLSchemes")) {
|
|
201
|
-
if (!infoPlist.includes(urlScheme)) {
|
|
202
|
-
console.log(`URL scheme may need manual configuration. Add '${urlScheme}' to CFBundleURLSchemes.`);
|
|
203
|
-
}
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
// Add URL scheme - need to insert before closing </dict></plist>
|
|
207
|
-
const urlSchemeEntry = ` <key>CFBundleURLTypes</key>
|
|
208
|
-
<array>
|
|
209
|
-
<dict>
|
|
210
|
-
<key>CFBundleURLSchemes</key>
|
|
211
|
-
<array>
|
|
212
|
-
<string>${urlScheme}</string>
|
|
213
|
-
</array>
|
|
214
|
-
<key>CFBundleURLName</key>
|
|
215
|
-
<string>${appInfo.identifier}</string>
|
|
216
|
-
</dict>
|
|
217
|
-
</array>
|
|
218
|
-
`;
|
|
219
|
-
// Insert before the last </dict>
|
|
220
|
-
infoPlist = infoPlist.replace(/(\s*)<\/dict>\s*<\/plist>/, `\n${urlSchemeEntry}$1</dict>\n</plist>`);
|
|
221
|
-
fs.writeFileSync(infoPlistPath, infoPlist);
|
|
222
|
-
console.log(`Added URL scheme '${urlScheme}' to Info.plist`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
209
|
function runXcodeGen(appleDir) {
|
|
226
210
|
try {
|
|
227
211
|
console.log("Running xcodegen to regenerate project...");
|
|
@@ -403,6 +387,11 @@ const shareExtension = {
|
|
|
403
387
|
modified = addDependencyToTarget(modified, targetName, {
|
|
404
388
|
target: extensionName,
|
|
405
389
|
});
|
|
390
|
+
// Add URL scheme to main app for deep linking from extension
|
|
391
|
+
const urlScheme = appInfo.productName
|
|
392
|
+
.toLowerCase()
|
|
393
|
+
.replace(/[^a-z0-9]/g, "");
|
|
394
|
+
modified = addUrlSchemeToTarget(modified, targetName, urlScheme, appInfo.identifier);
|
|
406
395
|
return modified;
|
|
407
396
|
},
|
|
408
397
|
};
|
|
@@ -438,8 +427,8 @@ function resolveTemplatesDir(options, extensionType) {
|
|
|
438
427
|
}
|
|
439
428
|
return templatesPath;
|
|
440
429
|
}
|
|
441
|
-
// Option 3: Default templates
|
|
442
|
-
const defaultTemplates = path.join(__dirname$1, "
|
|
430
|
+
// Option 3: Default templates (from bundled dist/cli.js -> ../templates)
|
|
431
|
+
const defaultTemplates = path.join(__dirname$1, "../templates", extensionType);
|
|
443
432
|
if (!fs.existsSync(defaultTemplates)) {
|
|
444
433
|
throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
|
|
445
434
|
}
|
|
@@ -494,13 +483,11 @@ async function addExtension(type, options) {
|
|
|
494
483
|
extension.createFiles(appleDir, appInfo, templatesDir);
|
|
495
484
|
console.log(`\n2. Updating main app entitlements...`);
|
|
496
485
|
updateMainAppEntitlements(appleDir, appInfo);
|
|
497
|
-
console.log(`\n3.
|
|
498
|
-
addUrlSchemeToInfoPlist(appleDir, appInfo);
|
|
499
|
-
console.log(`\n4. Updating project.yml...`);
|
|
486
|
+
console.log(`\n3. Updating project.yml (extension target + URL scheme)...`);
|
|
500
487
|
projectYml = readProjectYml(appleDir);
|
|
501
488
|
projectYml = extension.updateProjectYml(projectYml, appInfo);
|
|
502
489
|
writeProjectYml(appleDir, projectYml);
|
|
503
|
-
console.log(`\
|
|
490
|
+
console.log(`\n4. Regenerating Xcode project...`);
|
|
504
491
|
runXcodeGen(appleDir);
|
|
505
492
|
console.log(`\n========================================`);
|
|
506
493
|
console.log(`${extension.displayName} setup complete!`);
|
|
@@ -520,4 +507,39 @@ async function addExtension(type, options) {
|
|
|
520
507
|
}
|
|
521
508
|
}
|
|
522
509
|
|
|
523
|
-
|
|
510
|
+
function addUrlSchemeToInfoPlist(appleDir, appInfo) {
|
|
511
|
+
const targetName = `${appInfo.productName}_iOS`;
|
|
512
|
+
const infoPlistPath = path.join(appleDir, targetName, "Info.plist");
|
|
513
|
+
const urlScheme = appInfo.productName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
514
|
+
if (!fs.existsSync(infoPlistPath)) {
|
|
515
|
+
console.log(`Info.plist not found at ${infoPlistPath}, skipping URL scheme setup`);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
let infoPlist = fs.readFileSync(infoPlistPath, "utf8");
|
|
519
|
+
// Check if URL schemes already configured
|
|
520
|
+
if (infoPlist.includes("CFBundleURLSchemes")) {
|
|
521
|
+
if (!infoPlist.includes(urlScheme)) {
|
|
522
|
+
console.log(`URL scheme may need manual configuration. Add '${urlScheme}' to CFBundleURLSchemes.`);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
// Add URL scheme - need to insert before closing </dict></plist>
|
|
527
|
+
const urlSchemeEntry = ` <key>CFBundleURLTypes</key>
|
|
528
|
+
<array>
|
|
529
|
+
<dict>
|
|
530
|
+
<key>CFBundleURLSchemes</key>
|
|
531
|
+
<array>
|
|
532
|
+
<string>${urlScheme}</string>
|
|
533
|
+
</array>
|
|
534
|
+
<key>CFBundleURLName</key>
|
|
535
|
+
<string>${appInfo.identifier}</string>
|
|
536
|
+
</dict>
|
|
537
|
+
</array>
|
|
538
|
+
`;
|
|
539
|
+
// Insert before the last </dict>
|
|
540
|
+
infoPlist = infoPlist.replace(/(\s*)<\/dict>\s*<\/plist>/, `\n${urlSchemeEntry}$1</dict>\n</plist>`);
|
|
541
|
+
fs.writeFileSync(infoPlistPath, infoPlist);
|
|
542
|
+
console.log(`Added URL scheme '${urlScheme}' to Info.plist`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export { addExtension, addUrlSchemeToInfoPlist, addUrlSchemeToTarget, createExtensionEntitlements, findAppleProjectDir, findProjectRoot, findTauriConfig, getAppInfo, readProjectYml, runXcodeGen, shareExtension, updateMainAppEntitlements, writeProjectYml };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@choochmeque/tauri-apple-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Add iOS extensions to Tauri apps with a single command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -12,16 +12,6 @@
|
|
|
12
12
|
"templates",
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "rollup -c",
|
|
17
|
-
"prepublishOnly": "pnpm build",
|
|
18
|
-
"pretest": "pnpm build",
|
|
19
|
-
"test": "vitest run",
|
|
20
|
-
"test:watch": "vitest",
|
|
21
|
-
"lint": "eslint .",
|
|
22
|
-
"format": "prettier --write \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\"",
|
|
23
|
-
"format:check": "prettier --check \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\""
|
|
24
|
-
},
|
|
25
15
|
"keywords": [
|
|
26
16
|
"tauri",
|
|
27
17
|
"ios",
|
|
@@ -45,7 +35,6 @@
|
|
|
45
35
|
"commander": "^14.0.2"
|
|
46
36
|
},
|
|
47
37
|
"devDependencies": {
|
|
48
|
-
"@eslint/js": "^9.39.2",
|
|
49
38
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
50
39
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
51
40
|
"@types/node": "^25.0.3",
|
|
@@ -60,5 +49,13 @@
|
|
|
60
49
|
"engines": {
|
|
61
50
|
"node": ">=18.0.0"
|
|
62
51
|
},
|
|
63
|
-
"
|
|
64
|
-
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "rollup -c",
|
|
54
|
+
"pretest": "pnpm build",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"lint": "eslint .",
|
|
58
|
+
"format": "prettier --write \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\"",
|
|
59
|
+
"format:check": "prettier --check \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\""
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,246 +1,126 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
import Social
|
|
3
|
-
import MobileCoreServices
|
|
4
3
|
import UniformTypeIdentifiers
|
|
5
4
|
|
|
6
5
|
class ShareViewController: UIViewController {
|
|
7
6
|
|
|
8
|
-
// MARK: - Configuration
|
|
7
|
+
// MARK: - Configuration
|
|
9
8
|
private let appGroupIdentifier = "{{APP_GROUP_IDENTIFIER}}"
|
|
10
9
|
private let appURLScheme = "{{APP_URL_SCHEME}}"
|
|
11
10
|
|
|
11
|
+
// MARK: - Lifecycle
|
|
12
|
+
|
|
12
13
|
override func viewDidLoad() {
|
|
13
14
|
super.viewDidLoad()
|
|
14
|
-
|
|
15
|
+
// TODO: Setup your UI here
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
override func viewDidAppear(_ animated: Bool) {
|
|
18
19
|
super.viewDidAppear(animated)
|
|
19
|
-
|
|
20
|
+
processSharedItems()
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
// Check App Groups configuration early
|
|
24
|
-
if FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) == nil {
|
|
25
|
-
showError("App Groups not configured.\n\nPlease enable 'App Groups' capability in Xcode for both the main app and ShareExtension targets, and configure '\(appGroupIdentifier)' in Apple Developer Portal.")
|
|
26
|
-
return
|
|
27
|
-
}
|
|
23
|
+
// MARK: - Share Processing
|
|
28
24
|
|
|
25
|
+
private func processSharedItems() {
|
|
29
26
|
guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
|
|
30
|
-
|
|
27
|
+
complete()
|
|
31
28
|
return
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
var sharedContent: [String: Any] = [:]
|
|
37
|
-
var files: [[String: Any]] = []
|
|
38
|
-
var textContent: String? = nil
|
|
39
|
-
|
|
40
|
-
let group = DispatchGroup()
|
|
41
|
-
|
|
42
|
-
for extensionItem in extensionItems {
|
|
43
|
-
guard let attachments = extensionItem.attachments else { continue }
|
|
31
|
+
for item in extensionItems {
|
|
32
|
+
guard let attachments = item.attachments else { continue }
|
|
44
33
|
|
|
45
34
|
for attachment in attachments {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
// TODO: Handle different content types
|
|
36
|
+
// Examples:
|
|
37
|
+
// - UTType.image.identifier for images
|
|
38
|
+
// - UTType.url.identifier for URLs
|
|
39
|
+
// - UTType.text.identifier for text
|
|
40
|
+
// - UTType.fileURL.identifier for files
|
|
41
|
+
|
|
42
|
+
if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
|
43
|
+
attachment.loadItem(forTypeIdentifier: UTType.url.identifier) { [weak self] item, error in
|
|
52
44
|
if let url = item as? URL {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
} else if let image = item as? UIImage {
|
|
57
|
-
if let fileInfo = self?.saveImageToAppGroup(image: image) {
|
|
58
|
-
resultQueue.sync { files.append(fileInfo) }
|
|
59
|
-
}
|
|
45
|
+
// TODO: Process the URL
|
|
46
|
+
print("Received URL: \(url)")
|
|
60
47
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] item, error in
|
|
64
|
-
defer { group.leave() }
|
|
65
|
-
guard let url = item as? URL else { return }
|
|
66
|
-
|
|
67
|
-
if url.isFileURL {
|
|
68
|
-
if let fileInfo = self?.copyFileToAppGroup(url: url) {
|
|
69
|
-
resultQueue.sync { files.append(fileInfo) }
|
|
70
|
-
}
|
|
71
|
-
} else {
|
|
72
|
-
resultQueue.sync { textContent = url.absoluteString }
|
|
48
|
+
DispatchQueue.main.async {
|
|
49
|
+
self?.complete()
|
|
73
50
|
}
|
|
74
51
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if attachment.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
|
|
56
|
+
attachment.loadItem(forTypeIdentifier: UTType.text.identifier) { [weak self] item, error in
|
|
78
57
|
if let text = item as? String {
|
|
79
|
-
|
|
58
|
+
// TODO: Process the text
|
|
59
|
+
print("Received text: \(text)")
|
|
60
|
+
}
|
|
61
|
+
DispatchQueue.main.async {
|
|
62
|
+
self?.complete()
|
|
80
63
|
}
|
|
81
64
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
|
69
|
+
attachment.loadItem(forTypeIdentifier: UTType.image.identifier) { [weak self] item, error in
|
|
85
70
|
if let url = item as? URL {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
// TODO: Process image file URL
|
|
72
|
+
print("Received image: \(url)")
|
|
73
|
+
} else if let image = item as? UIImage {
|
|
74
|
+
// TODO: Process UIImage directly
|
|
75
|
+
print("Received UIImage")
|
|
76
|
+
}
|
|
77
|
+
DispatchQueue.main.async {
|
|
78
|
+
self?.complete()
|
|
89
79
|
}
|
|
90
80
|
}
|
|
91
|
-
|
|
92
|
-
group.leave()
|
|
81
|
+
return
|
|
93
82
|
}
|
|
94
83
|
}
|
|
95
84
|
}
|
|
96
85
|
|
|
97
|
-
|
|
98
|
-
guard let self = self else { return }
|
|
99
|
-
|
|
100
|
-
if !files.isEmpty {
|
|
101
|
-
sharedContent["type"] = "files"
|
|
102
|
-
sharedContent["files"] = files
|
|
103
|
-
} else if let text = textContent {
|
|
104
|
-
sharedContent["type"] = "text"
|
|
105
|
-
sharedContent["text"] = text
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if !sharedContent.isEmpty {
|
|
109
|
-
_ = self.saveToAppGroup(content: sharedContent)
|
|
110
|
-
self.openMainAppAndComplete()
|
|
111
|
-
} else {
|
|
112
|
-
self.completeRequest()
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private func showError(_ message: String) {
|
|
118
|
-
let alert = UIAlertController(
|
|
119
|
-
title: "ShareKit Error",
|
|
120
|
-
message: message,
|
|
121
|
-
preferredStyle: .alert
|
|
122
|
-
)
|
|
123
|
-
alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
|
|
124
|
-
self?.completeRequest()
|
|
125
|
-
})
|
|
126
|
-
present(alert, animated: true)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private func copyFileToAppGroup(url: URL) -> [String: Any]? {
|
|
130
|
-
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
|
|
131
|
-
return nil
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let sharedFilesDir = containerURL.appendingPathComponent("shared_files", isDirectory: true)
|
|
135
|
-
try? FileManager.default.createDirectory(at: sharedFilesDir, withIntermediateDirectories: true)
|
|
136
|
-
|
|
137
|
-
let fileName = url.lastPathComponent
|
|
138
|
-
let destinationURL = sharedFilesDir.appendingPathComponent(UUID().uuidString + "_" + fileName)
|
|
139
|
-
|
|
140
|
-
do {
|
|
141
|
-
if url.startAccessingSecurityScopedResource() {
|
|
142
|
-
defer { url.stopAccessingSecurityScopedResource() }
|
|
143
|
-
try FileManager.default.copyItem(at: url, to: destinationURL)
|
|
144
|
-
} else {
|
|
145
|
-
try FileManager.default.copyItem(at: url, to: destinationURL)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
var fileInfo: [String: Any] = [
|
|
149
|
-
"path": destinationURL.path,
|
|
150
|
-
"name": fileName
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
if let mimeType = getMimeType(for: url) {
|
|
154
|
-
fileInfo["mimeType"] = mimeType
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if let attributes = try? FileManager.default.attributesOfItem(atPath: destinationURL.path),
|
|
158
|
-
let size = attributes[.size] as? Int64 {
|
|
159
|
-
fileInfo["size"] = size
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return fileInfo
|
|
163
|
-
} catch {
|
|
164
|
-
print("ShareKit: Failed to copy file: \(error)")
|
|
165
|
-
return nil
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private func saveImageToAppGroup(image: UIImage) -> [String: Any]? {
|
|
170
|
-
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
|
|
171
|
-
return nil
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
let sharedFilesDir = containerURL.appendingPathComponent("shared_files", isDirectory: true)
|
|
175
|
-
try? FileManager.default.createDirectory(at: sharedFilesDir, withIntermediateDirectories: true)
|
|
176
|
-
|
|
177
|
-
let fileName = UUID().uuidString + ".png"
|
|
178
|
-
let destinationURL = sharedFilesDir.appendingPathComponent(fileName)
|
|
179
|
-
|
|
180
|
-
guard let data = image.pngData() else { return nil }
|
|
181
|
-
|
|
182
|
-
do {
|
|
183
|
-
try data.write(to: destinationURL)
|
|
184
|
-
|
|
185
|
-
return [
|
|
186
|
-
"path": destinationURL.path,
|
|
187
|
-
"name": fileName,
|
|
188
|
-
"mimeType": "image/png",
|
|
189
|
-
"size": data.count
|
|
190
|
-
]
|
|
191
|
-
} catch {
|
|
192
|
-
print("ShareKit: Failed to save image: \(error)")
|
|
193
|
-
return nil
|
|
194
|
-
}
|
|
86
|
+
complete()
|
|
195
87
|
}
|
|
196
88
|
|
|
197
|
-
|
|
198
|
-
if let uti = UTType(filenameExtension: url.pathExtension) {
|
|
199
|
-
return uti.preferredMIMEType
|
|
200
|
-
}
|
|
201
|
-
return nil
|
|
202
|
-
}
|
|
89
|
+
// MARK: - App Group Storage (Optional)
|
|
203
90
|
|
|
204
|
-
|
|
91
|
+
/// Save data to App Group for main app to read
|
|
92
|
+
private func saveToAppGroup(_ data: Data, forKey key: String) -> Bool {
|
|
205
93
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
|
206
|
-
|
|
94
|
+
print("App Groups not configured")
|
|
207
95
|
return false
|
|
208
96
|
}
|
|
97
|
+
userDefaults.set(data, forKey: key)
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
209
100
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
userDefaults.synchronize()
|
|
214
|
-
return true
|
|
215
|
-
} catch {
|
|
216
|
-
showError("Failed to save shared content: \(error.localizedDescription)")
|
|
217
|
-
return false
|
|
218
|
-
}
|
|
101
|
+
/// Get App Group container URL for file storage
|
|
102
|
+
private func appGroupContainerURL() -> URL? {
|
|
103
|
+
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
|
219
104
|
}
|
|
220
105
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
106
|
+
// MARK: - Open Main App (Optional)
|
|
107
|
+
|
|
108
|
+
private func openMainApp() {
|
|
109
|
+
guard let url = URL(string: "\(appURLScheme)://share") else { return }
|
|
226
110
|
|
|
227
111
|
var responder: UIResponder? = self
|
|
228
|
-
while
|
|
229
|
-
if let application =
|
|
230
|
-
|
|
231
|
-
application.open(url, options: [:], completionHandler: nil)
|
|
232
|
-
} else {
|
|
233
|
-
_ = application.perform(NSSelectorFromString("openURL:"), with: url)
|
|
234
|
-
}
|
|
112
|
+
while let r = responder {
|
|
113
|
+
if let application = r as? UIApplication {
|
|
114
|
+
application.open(url, options: [:], completionHandler: nil)
|
|
235
115
|
break
|
|
236
116
|
}
|
|
237
|
-
responder =
|
|
117
|
+
responder = r.next
|
|
238
118
|
}
|
|
239
|
-
|
|
240
|
-
completeRequest()
|
|
241
119
|
}
|
|
242
120
|
|
|
243
|
-
|
|
121
|
+
// MARK: - Complete
|
|
122
|
+
|
|
123
|
+
private func complete() {
|
|
244
124
|
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
|
245
125
|
}
|
|
246
126
|
}
|