Skip to content

Commit e45cce6

Browse files
feat(ww): Support appID, appName, and output folder (#408)
* feat(web-wrapper): Support appID, appName, and output folder configuration * output fix * Update build.mjs * add links for publishing instructions * Update MainActivity.kt.handlebars * Update Config.kt.handlebars * Update build.gradle.handlebars * switch from entry domain to entry url * absolute file path * oops! * space * missed a spot * feedback * positional required arguments * fixes
1 parent c105cb3 commit e45cce6

File tree

14 files changed

+176
-66
lines changed

14 files changed

+176
-66
lines changed

x/examples/website-wrapper-app/.scripts/build_sdk_mobileproxy.sh renamed to x/examples/website-wrapper-app/.scripts/build_mobileproxy.sh

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616

1717
PLATFORM="$1"
1818
TAG="${2:-"x/v0.0.1"}"
19+
OUTPUT="${3:-output}"
1920

20-
git clone --depth 1 --branch "${TAG}" https://github.com/Jigsaw-Code/outline-sdk.git output/outline-sdk
21-
cd output/outline-sdk/x
21+
if [[ "$OUTPUT" = "/" ]] || [[ "$OUTPUT" = "*" ]]; then
22+
echo "Error: OUTPUT cannot be '/' or '*'. These are dangerous values."
23+
exit 1
24+
fi
25+
26+
git clone --depth 1 --branch "$TAG" https://github.com/Jigsaw-Code/outline-sdk.git "$OUTPUT/outline-sdk"
27+
cd "$OUTPUT/outline-sdk/x" || exit
2228
go build -o "$(pwd)/out/" golang.org/x/mobile/cmd/gomobile golang.org/x/mobile/cmd/gobind
2329

2430
if [ "$PLATFORM" = "ios" ]; then
@@ -33,5 +39,5 @@ else
3339
fi
3440

3541
cd ../..
36-
rm -rf "$(pwd)/mobileproxy"
37-
mv "$(pwd)/outline-sdk/x/out" "$(pwd)/mobileproxy"
42+
rm -rf "$OUTPUT/mobileproxy"
43+
mv "$(pwd)/outline-sdk/x/out" "mobileproxy"

x/examples/website-wrapper-app/.scripts/start.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ import chalk from "chalk";
2727
throw new Error(`Parameter \`--platform\` not provided.`);
2828
}
2929

30-
if (!args.entryDomain) {
31-
throw new Error(`Parameter \`--entryDomain\` not provided.`);
30+
if (!args.entryUrl) {
31+
throw new Error(`Parameter \`--entryUrl\` not provided.`);
32+
}
33+
34+
if (args.output) {
35+
throw new Error(`Parameter \`output\` is not supported in the 'start' script`);
3236
}
3337

3438
let proxyLocations = {};

x/examples/website-wrapper-app/README.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ To verify that your system has the necessary dependencies to generate your web w
2020
> [!WARNING]
2121
> You can only build iOS apps on MacOS.
2222
23-
* You will need your site's domain.
23+
* You will need the url you want to load initially in your app.
2424
* You will need [go](https://golang.org/) to build the SDK library.
2525
* You will need [Node.js](https://nodejs.org/en/) for the project setup and web server.
2626
* You will need [XCode](https://developer.apple.com/xcode/).
@@ -30,7 +30,7 @@ To verify that your system has the necessary dependencies to generate your web w
3030

3131
```sh
3232
npm run reset
33-
npm run build:project -- --platform=ios --entryDomain="www.example.com"
33+
npm run build:project -- --platform=ios --entryUrl="https://www.example.com"
3434
npm run open:ios
3535
```
3636

@@ -62,16 +62,20 @@ Many sites don't handle their own navigation - if this applies to you, you can r
6262
* You will need an [ngrok account](https://ngrok.com/), from which you can get your [`--navigatorToken`](https://dashboard.ngrok.com/get-started/your-authtoken)
6363

6464
```sh
65-
npm run start -- --platform=ios --entryDomain="www.example.com" \
65+
npm run start -- --platform=ios --entryUrl="https://www.example.com" \
6666
--navigatorToken="<YOUR_NGROK_AUTH_TOKEN>" --navigatorPath="/nav"
6767
```
6868

69+
### Publishing your app in the App Store
70+
71+
[Follow these instructions on how to publish your app for beta testing and the App Store.](https://developer.apple.com/documentation/xcode/distributing-your-app-for-beta-testing-and-releases)
72+
6973
## Building the app project for **Android**
7074

7175
> [!WARNING]
7276
> If you want to build Android on Windows, please use [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/install)
7377
74-
* You will need your site's domain.
78+
* You will need the url you want to load initially in your app.
7579
* You will need [Node.js](https://nodejs.org/en/) for the project setup and web server.
7680
* You will need [go](https://golang.org/) to build the SDK library.
7781
* You will need [JDK 17](https://stackoverflow.com/a/70649641) to build the app.
@@ -83,7 +87,7 @@ npm run start -- --platform=ios --entryDomain="www.example.com" \
8387

8488
```sh
8589
npm run reset
86-
npm run build:project -- --platform=android --entryDomain="www.example.com"
90+
npm run build:project -- --platform=android --entryUrl="https://www.example.com"
8791
npm run open:android
8892
```
8993

@@ -115,16 +119,23 @@ Many sites don't handle their own navigation - if this applies to you, you can r
115119
* You will need an [ngrok account](https://ngrok.com/), from which you can get your [`--navigatorToken`](https://dashboard.ngrok.com/get-started/your-authtoken)
116120

117121
```sh
118-
npm run start -- --platform=android --entryDomain="www.example.com" \
122+
npm run start -- --platform=android --entryUrl="https://www.example.com" \
119123
--navigatorToken="<YOUR_NGROK_AUTH_TOKEN>" --navigatorPath="/nav"
120124
```
121125

126+
### Publishing your app in the Google Play Store
127+
128+
[Follow these instructions to learn how to publish your app to the Google Play Store](https://developer.android.com/studio/publish)
129+
122130
## Available Configuration Options
123131

124132
| Option | Description | Possible Values |
125133
| ------------------- | ------------------------------------------------------------------------------- | ------------------------ |
126134
| `--platform` | **(Required)** Specifies the target platform for the build. | `"ios"` or `"android"` |
127-
| `--entryDomain` | **(Required)** The primary domain of your website. | Any valid domain name |
135+
| `--entryUrl` | **(Required)** The primary url of your website. | Any valid url |
136+
| `--appId` | The unique identifier for the app (e.g., iOS Bundle ID, Android Application ID). | A reverse domain name string (e.g., `com.company.appname`) |
137+
| `--appName` | The user-visible name of the application. | Any valid application name string (e.g., "My Awesome App") |
138+
| `--output` | The directory where the generated app project files will be saved. | A valid, absolute file path (e.g., `/users/me/my-generated-app`) |
128139
| `--additionalDomains` | A list of other domains that should be accessible within the app. | Comma-separated domains |
129140
| `--smartDialerConfig` | A JSON string containing the configuration for the [smart dialer feature](../../smart#yaml-config-for-the-smart-dialer). | Valid JSON string |
130141
| `--navigatorToken` | Your ngrok authentication token for using the navigation proxy. | Your [ngrok auth token](https://dashboard.ngrok.com/get-started/your-authtoken) |

x/examples/website-wrapper-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"license": "Apache-2.0",
1616
"scripts": {
17-
"build:sdk_mobileproxy": "bash ./.scripts/build_sdk_mobileproxy.sh",
17+
"build:mobileproxy": "bash ./.scripts/build_mobileproxy.sh",
1818
"build:project": "node ./wrapper_app_project/.scripts/build.mjs",
1919
"clean": "npx rimraf node_modules/ output/",
2020
"doctor": "./doctor",

x/examples/website-wrapper-app/wrapper_app_project/.scripts/build.mjs

Lines changed: 121 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { exec } from "node:child_process";
15+
import { exec, execFile } from "node:child_process";
1616
import { glob } from "glob";
1717
import { promisify } from "node:util";
1818
import chalk from "chalk";
@@ -23,19 +23,10 @@ import path from "node:path";
2323
import minimist from "minimist";
2424

2525
const OUTPUT_DIR = path.join(process.cwd(), "output");
26-
2726
const WRAPPER_APP_TEMPLATE_DIR = path.join(
2827
process.cwd(),
2928
"wrapper_app_project/template",
3029
);
31-
const WRAPPER_APP_OUTPUT_DIR = path.join(OUTPUT_DIR, "wrapper_app_project");
32-
const WRAPPER_APP_OUTPUT_ZIP = path.join(OUTPUT_DIR, "wrapper_app_project.zip");
33-
34-
const SDK_MOBILEPROXY_OUTPUT_DIR = path.join(OUTPUT_DIR, "mobileproxy");
35-
const WRAPPER_APP_OUTPUT_SDK_MOBILEPROXY_DIR = path.join(
36-
WRAPPER_APP_OUTPUT_DIR,
37-
"mobileproxy",
38-
);
3930

4031
const DEFAULT_SMART_DIALER_CONFIG = {
4132
dns: [{
@@ -46,18 +37,41 @@ const DEFAULT_SMART_DIALER_CONFIG = {
4637

4738
export default async function main(
4839
{
40+
additionalDomains = [],
41+
appId,
42+
appName,
43+
entryUrl = "https://www.example.com",
44+
navigationUrl,
45+
output = OUTPUT_DIR,
4946
platform,
5047
sdkVersion = "x/v0.0.1",
51-
entryDomain = "www.example.com",
52-
navigationUrl,
5348
smartDialerConfig = DEFAULT_SMART_DIALER_CONFIG,
54-
additionalDomains = [],
5549
},
5650
) {
51+
const WRAPPER_APP_OUTPUT_DIR = path.resolve(output, "wrapper_app_project");
52+
const WRAPPER_APP_OUTPUT_ZIP = path.resolve(
53+
output,
54+
"wrapper_app_project.zip",
55+
);
56+
57+
const SDK_MOBILEPROXY_OUTPUT_DIR = path.resolve(output, "mobileproxy");
58+
const WRAPPER_APP_OUTPUT_SDK_MOBILEPROXY_DIR = path.resolve(
59+
WRAPPER_APP_OUTPUT_DIR,
60+
"mobileproxy",
61+
);
62+
5763
if (!fs.existsSync(SDK_MOBILEPROXY_OUTPUT_DIR)) {
58-
console.log(`Building the Outline SDK mobileproxy library for ${platform}...`);
64+
console.log(
65+
`Building the Outline SDK mobileproxy library for ${platform}...`,
66+
);
5967

60-
await promisify(exec)(`npm run build:sdk_mobileproxy ${platform} ${sdkVersion}`);
68+
await promisify(execFile)("npm", [
69+
"run",
70+
"build:mobileproxy",
71+
platform,
72+
sdkVersion,
73+
output,
74+
], { shell: false });
6175
}
6276

6377
const sourceFilepaths = await glob(
@@ -70,6 +84,22 @@ export default async function main(
7084

7185
console.log("Building project from template...");
7286

87+
let templateArguments;
88+
89+
try {
90+
templateArguments = resolveTemplateArguments(platform, entryUrl, {
91+
additionalDomains,
92+
appId,
93+
appName,
94+
navigationUrl,
95+
smartDialerConfig,
96+
});
97+
} catch (cause) {
98+
throw new TypeError("Failed to resolve the project template arguments", {
99+
cause,
100+
});
101+
}
102+
73103
for (const sourceFilepath of sourceFilepaths) {
74104
const destinationFilepath = path.join(
75105
WRAPPER_APP_OUTPUT_DIR,
@@ -91,22 +121,9 @@ export default async function main(
91121
fs.readFileSync(sourceFilepath, "utf8"),
92122
);
93123

94-
if (typeof additionalDomains === "string") {
95-
additionalDomains = [additionalDomains];
96-
}
97-
98124
fs.writeFileSync(
99125
destinationFilepath.replace(/\.handlebars$/, ""),
100-
template({
101-
platform,
102-
entryDomain,
103-
navigationUrl: navigationUrl
104-
? navigationUrl
105-
: `https://${entryDomain}/`,
106-
domainList: [entryDomain, ...additionalDomains].join("\\n"),
107-
smartDialerConfig: typeof smartDialerConfig === "object" ? JSON.stringify(smartDialerConfig).replaceAll('"', '\\"') : smartDialerConfig,
108-
additionalDomains,
109-
}),
126+
template(templateArguments),
110127
"utf8",
111128
);
112129
}
@@ -149,21 +166,93 @@ function zip(root, destination) {
149166
});
150167
}
151168

169+
function resolveTemplateArguments(
170+
platform,
171+
entryUrl,
172+
{
173+
appId,
174+
appName,
175+
navigationUrl,
176+
additionalDomains,
177+
smartDialerConfig,
178+
},
179+
) {
180+
const result = {
181+
platform,
182+
entryUrl,
183+
entryDomain: new URL(entryUrl).hostname,
184+
};
185+
186+
if (!appId) {
187+
// Infer an app ID from the entry domain by reversing it (e.g. `www.example.com` becomes `com.example.www`)
188+
// It must be lower case, and hyphens are not allowed.
189+
result.appId = result.entryDomain.replaceAll("-", "")
190+
.toLocaleLowerCase().split(".").reverse().join(".");
191+
}
192+
193+
if (!appName) {
194+
// Infer an app name from the base entry domain part by title casing the root domain:
195+
// (e.g. `www.my-example-app.com` becomes "My Example App")
196+
const rootDomain = result.entryDomain.split(".").reverse()[1];
197+
198+
result.appName = rootDomain.toLocaleLowerCase().replaceAll(
199+
/\w[a-z0-9]*[-_]*/g,
200+
(match) => {
201+
match = match.replace(/[-_]+/, " ");
202+
203+
return match.charAt(0).toUpperCase() + match.slice(1).toLowerCase();
204+
},
205+
);
206+
}
207+
208+
if (navigationUrl) {
209+
result.entryUrl = navigationUrl;
210+
result.entryDomain = new URL(navigationUrl).hostname;
211+
}
212+
213+
if (typeof additionalDomains === "string") {
214+
result.additionalDomains = additionalDomains.split(/,\s*/);
215+
result.domainList = [result.entryDomain, ...result.additionalDomains].join(
216+
"\\n",
217+
);
218+
} else if (typeof additionalDomains === "object") {
219+
result.additionalDomains = additionalDomains;
220+
result.domainList = [result.entryDomain, ...result.additionalDomains].join(
221+
"\\n",
222+
);
223+
} else {
224+
result.domainList = [result.entryDomain];
225+
}
226+
227+
if (typeof smartDialerConfig === "string") {
228+
result.smartDialerConfig = smartDialerConfig.replaceAll('"', '\\"');
229+
} else if (typeof smartDialerConfig === "object") {
230+
result.smartDialerConfig = JSON.stringify(smartDialerConfig).replaceAll(
231+
'"',
232+
'\\"',
233+
);
234+
}
235+
236+
return result;
237+
}
238+
152239
if (import.meta.url.endsWith(process.argv[1])) {
153240
const args = minimist(process.argv.slice(2));
154241

155242
if (!args.platform) {
156243
throw new Error(`Parameter \`--platform\` not provided.`);
157244
}
158245

159-
if (!args.entryDomain) {
160-
throw new Error(`Parameter \`--entryDomain\` not provided.`);
246+
if (!args.entryUrl) {
247+
throw new Error(`Parameter \`--entryUrl\` not provided.`);
161248
}
162249

163250
main({
164251
...args,
165252
additionalDomains: args.additionalDomains?.split(/,\s*/) ?? [],
166-
smartDialerConfig: args.smartDialerConfig ? JSON.parse(args.smartDialerConfig) : undefined,
253+
smartDialerConfig: args.smartDialerConfig
254+
? JSON.parse(args.smartDialerConfig)
255+
: undefined,
167256
})
168257
.catch(console.error);
169258
}

x/examples/website-wrapper-app/wrapper_app_project/template/android/app/build.gradle renamed to x/examples/website-wrapper-app/wrapper_app_project/template/android/app/build.gradle.handlebars

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
apply plugin: 'com.android.application'
22

33
android {
4-
namespace "org.getoutline.pwa"
4+
namespace "{{appId}}"
55
compileSdk rootProject.ext.compileSdkVersion
66
defaultConfig {
7-
applicationId "org.getoutline.pwa"
7+
applicationId "{{appId}}"
88
minSdkVersion rootProject.ext.minSdkVersion
99
targetSdkVersion rootProject.ext.targetSdkVersion
1010
versionCode 1

x/examples/website-wrapper-app/wrapper_app_project/template/android/app/src/main/java/org/getoutline/pwa/Config.kt.handlebars

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.getoutline.pwa
1+
package {{appId}}
22

33
object Config {
44
var domainList: String = "{{domainList}}"
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.getoutline.pwa
1+
package {{appId}}
22

33
import android.os.Bundle
44
import com.getcapacitor.BridgeActivity
@@ -63,4 +63,4 @@ class MainActivity : BridgeActivity() {
6363

6464
return true
6565
}
66-
}
66+
}

x/examples/website-wrapper-app/wrapper_app_project/template/android/app/src/main/res/values/strings.xml

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version='1.0' encoding='utf-8'?>
2+
<resources>
3+
<string name="app_name">{{appName}}</string>
4+
<string name="title_activity_main">{{appName}}</string>
5+
<string name="package_name">{{appId}}</string>
6+
<string name="custom_url_scheme">{{appId}}</string>
7+
</resources>

0 commit comments

Comments
 (0)