├── .dockerignore
├── .github
└── workflows
│ └── deploy-to-staging.yml
├── .gitignore
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── LICENSE
├── Microsoft.PWABuilder.IOS.Web
├── Common
│ ├── DirectoryInfoExtensions.cs
│ ├── FileInfoExtensions.cs
│ ├── HttpClientExtensions.cs
│ ├── ImageTargetSizeExtensions.cs
│ ├── UriExtensions.cs
│ └── ZipArchiveExtensions.cs
├── Controllers
│ ├── HomeController.cs
│ └── PackagesController.cs
├── Dockerfile
├── Microsoft.PWABuilder.IOS.Web.csproj
├── Microsoft.PWABuilder.IOS.Web.sln
├── Models
│ ├── AnalyticsInfo.cs
│ ├── AppSettings.cs
│ ├── Color.cs
│ ├── HttpMessageStream.cs
│ ├── IOSAppPackageOptions.cs
│ ├── IOSAppShortcut.cs
│ ├── IconFormat.cs
│ ├── ImageGeneratorResult.cs
│ ├── ImageGeneratorServiceZipFile.cs
│ ├── ImageSource.cs
│ ├── ImageTargetSize.cs
│ ├── WebAppManifest.cs
│ ├── WebAppManifestContext.cs
│ ├── XcodeFile.cs
│ ├── XcodeFolder.cs
│ ├── XcodeItem.cs
│ ├── XcodeProject.cs
│ └── XcodePwaShellProject.cs
├── Program.cs
├── Properties
│ ├── ServiceDependencies
│ │ └── pwabuilder-ios - Web Deploy
│ │ │ └── profile.arm.json
│ └── launchSettings.json
├── Resources
│ ├── ios-project-src
│ │ ├── .DS_Store
│ │ ├── .gitignore
│ │ ├── LICENSE
│ │ ├── Podfile
│ │ ├── launch-128.png
│ │ ├── launch-192.png
│ │ ├── launch-64.png
│ │ ├── pwa-shell.xcodeproj
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata
│ │ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ │ └── WorkspaceSettings.xcsettings
│ │ │ └── xcshareddata
│ │ │ │ └── xcschemes
│ │ │ │ └── pwa-shell.xcscheme
│ │ ├── pwa-shell.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── pwa-shell
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Assets.xcassets
│ │ │ ├── AppIcon.appiconset
│ │ │ │ ├── 100.png
│ │ │ │ ├── 1024.png
│ │ │ │ ├── 114.png
│ │ │ │ ├── 120.png
│ │ │ │ ├── 144.png
│ │ │ │ ├── 152.png
│ │ │ │ ├── 167.png
│ │ │ │ ├── 180.png
│ │ │ │ ├── 20.png
│ │ │ │ ├── 29.png
│ │ │ │ ├── 40.png
│ │ │ │ ├── 50.png
│ │ │ │ ├── 57.png
│ │ │ │ ├── 58.png
│ │ │ │ ├── 60.png
│ │ │ │ ├── 72.png
│ │ │ │ ├── 76.png
│ │ │ │ ├── 80.png
│ │ │ │ ├── 87.png
│ │ │ │ ├── AppIcon-128.png
│ │ │ │ ├── AppIcon-128@2x.png
│ │ │ │ ├── AppIcon-16.png
│ │ │ │ ├── AppIcon-16@2x.png
│ │ │ │ ├── AppIcon-256.png
│ │ │ │ ├── AppIcon-256@2x.png
│ │ │ │ ├── AppIcon-32.png
│ │ │ │ ├── AppIcon-32@2x.png
│ │ │ │ ├── AppIcon-512.png
│ │ │ │ ├── AppIcon-512@2x.png
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ └── LaunchIcon.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── launch-128.png
│ │ │ │ ├── launch-192.png
│ │ │ │ ├── launch-256.png
│ │ │ │ ├── launch-512.png
│ │ │ │ └── launch-64.png
│ │ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ │ ├── Entitlements
│ │ │ ├── .gitignore
│ │ │ └── Entitlements.plist
│ │ │ ├── GoogleService-Info.plist
│ │ │ ├── Info.plist
│ │ │ ├── Printer.swift
│ │ │ ├── PushNotifications.swift
│ │ │ ├── SceneDelegate.swift
│ │ │ ├── Settings.swift
│ │ │ ├── ViewController.swift
│ │ │ └── WebView.swift
│ └── next-steps.html
├── Services
│ ├── AnalyticsService.cs
│ ├── IOSPackageCreator.cs
│ ├── ImageGenerator.cs
│ └── TempDirectory.cs
├── Startup.cs
├── appsettings.Development.json
├── appsettings.Production.json
├── appsettings.json
└── wwwroot
│ ├── index.html
│ └── index.js
├── README.md
├── docker-compose.debug.yml
└── docker-compose.yml
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/bin
15 | **/charts
16 | **/docker-compose*
17 | **/compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | LICENSE
25 | README.md
26 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-to-staging.yml:
--------------------------------------------------------------------------------
1 | # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
2 | # More GitHub Actions for Azure: https://github.com/Azure/actions
3 |
4 | # This workflow will build and deploy the ASP.Net Core app to the staging slot of the pwabuilder-ios web app.
5 |
6 | name: Build and deploy ASP.Net Core app to Azure Web App - pwabuilder-ios
7 |
8 | on:
9 | push:
10 | branches:
11 | - main
12 | workflow_dispatch:
13 |
14 | jobs:
15 | build:
16 | runs-on: windows-latest
17 | permissions:
18 | contents: read
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - name: Set up .NET
24 | uses: actions/setup-dotnet@v4
25 | with:
26 | dotnet-version: '7.x'
27 |
28 | - name: Build with .NET
29 | working-directory: ./Microsoft.PWABuilder.IOS.Web
30 | run: dotnet build --configuration Release
31 |
32 | - name: dotnet publish
33 | working-directory: ./Microsoft.PWABuilder.IOS.Web
34 | run: dotnet publish -c Release -o "${{env.DOTNET_ROOT}}/myapp"
35 |
36 | - name: Upload artifact for deployment job
37 | uses: actions/upload-artifact@v4
38 | with:
39 | name: .net-app
40 | path: ${{env.DOTNET_ROOT}}/myapp
41 |
42 | deploy:
43 | runs-on: windows-latest
44 | needs: build
45 |
46 | permissions:
47 | id-token: write #This is required for requesting the JWT
48 | contents: read #This is required for actions/checkout
49 |
50 | steps:
51 | - name: Download artifact from build job
52 | uses: actions/download-artifact@v4
53 | with:
54 | name: .net-app
55 |
56 | - name: Login to Azure
57 | uses: azure/login@v2
58 | with:
59 | client-id: ${{ secrets.AZURE_APP_ID }}
60 | tenant-id: ${{ secrets.AZURE_TENANT_ID }}
61 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
62 |
63 | - name: Deploy to Azure Web App
64 | id: deploy-to-webapp
65 | uses: azure/webapps-deploy@v3
66 | with:
67 | app-name: 'pwabuilder-ios'
68 | slot-name: 'staging'
69 | package: .
70 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
352 | # MacOS finder temp
353 | *.DS_Store
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": ".NET Core Launch (web)",
6 | "type": "coreclr",
7 | "request": "launch",
8 | "preLaunchTask": "build",
9 | "program": "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/bin/Debug/net7.0/Microsoft.PWABuilder.IOS.Web.dll",
10 | "args": [],
11 | "cwd": "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web",
12 | "stopAtEntry": false,
13 | "serverReadyAction": {
14 | "action": "openExternally",
15 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
16 | },
17 | "env": {
18 | "ASPNETCORE_ENVIRONMENT": "Development"
19 | },
20 | "sourceFileMap": {
21 | "/Views": "${workspaceFolder}/Views"
22 | }
23 | },
24 | {
25 | "name": ".NET Core Attach",
26 | "type": "coreclr",
27 | "request": "attach"
28 | },
29 | {
30 | "name": "Docker .NET Core Launch",
31 | "type": "docker",
32 | "request": "launch",
33 | "preLaunchTask": "docker-run: debug",
34 | "netCore": {
35 | "appProject": "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj"
36 | }
37 | }
38 | ]
39 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.eol": "\n",
3 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | },
16 | {
17 | "label": "publish",
18 | "command": "dotnet",
19 | "type": "process",
20 | "args": [
21 | "publish",
22 | "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj",
23 | "/property:GenerateFullPaths=true",
24 | "/consoleloggerparameters:NoSummary"
25 | ],
26 | "problemMatcher": "$msCompile"
27 | },
28 | {
29 | "label": "watch",
30 | "command": "dotnet",
31 | "type": "process",
32 | "args": [
33 | "watch",
34 | "run",
35 | "--project",
36 | "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj"
37 | ],
38 | "problemMatcher": "$msCompile"
39 | },
40 | {
41 | "type": "docker-build",
42 | "label": "docker-build: debug",
43 | "dependsOn": [
44 | "build"
45 | ],
46 | "dockerBuild": {
47 | "tag": "pwabuilderios:dev",
48 | "target": "base",
49 | "dockerfile": "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Dockerfile",
50 | "context": "${workspaceFolder}",
51 | "pull": true
52 | },
53 | "netCore": {
54 | "appProject": "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj"
55 | }
56 | },
57 | {
58 | "type": "docker-build",
59 | "label": "docker-build: release",
60 | "dependsOn": [
61 | "build"
62 | ],
63 | "dockerBuild": {
64 | "tag": "pwabuilderios:latest",
65 | "dockerfile": "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Dockerfile",
66 | "context": "${workspaceFolder}",
67 | "pull": true
68 | },
69 | "netCore": {
70 | "appProject": "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj"
71 | }
72 | },
73 | {
74 | "type": "docker-run",
75 | "label": "docker-run: debug",
76 | "dependsOn": [
77 | "docker-build: debug"
78 | ],
79 | "dockerRun": {},
80 | "netCore": {
81 | "appProject": "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj",
82 | "enableDebugging": true
83 | }
84 | },
85 | {
86 | "type": "docker-run",
87 | "label": "docker-run: release",
88 | "dependsOn": [
89 | "docker-build: release"
90 | ],
91 | "dockerRun": {},
92 | "netCore": {
93 | "appProject": "${workspaceFolder}/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj"
94 | }
95 | }
96 | ]
97 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 PWABuilder
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Common/DirectoryInfoExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.PWABuilder.IOS.Web.Common
8 | {
9 | public static class DirectoryInfoExtensions
10 | {
11 | ///
12 | /// Copies the contents of a directory to another directory.
13 | ///
14 | /// The source directory whose contents will be copied.
15 | /// The destination directory to receive the contents of the .
16 | public static void CopyContents(this DirectoryInfo source, DirectoryInfo target)
17 | {
18 | var directoriesToCopy = new Queue<(DirectoryInfo source, DirectoryInfo target)>();
19 | var enqueueSubdirectories = new Action((currentSource, currentTarget) =>
20 | {
21 | // Create the target directory.
22 | Directory.CreateDirectory(currentTarget.FullName);
23 |
24 | // Copy each file into the new directory.
25 | foreach (var file in currentSource.EnumerateFiles())
26 | {
27 | file.CopyTo(Path.Combine(currentTarget.FullName, file.Name), true);
28 | }
29 |
30 | // Enqueue the subdirectories.
31 | foreach (var subDir in currentSource.GetDirectories())
32 | {
33 | var nextTargetSubDir = currentTarget.CreateSubdirectory(subDir.Name);
34 | directoriesToCopy.Enqueue((subDir, nextTargetSubDir));
35 | }
36 | });
37 |
38 | enqueueSubdirectories(source, target);
39 | while (directoriesToCopy.Count > 0)
40 | {
41 | var (currentSrc, currentTarget) = directoriesToCopy.Dequeue();
42 | enqueueSubdirectories(currentSrc, currentTarget);
43 | }
44 | }
45 |
46 | ///
47 | /// Renames a directory.
48 | ///
49 | /// The directory to rename.
50 | /// The new name.
51 | public static void Rename(this DirectoryInfo directory, string newName)
52 | {
53 | var parentPath = directory.Parent?.FullName ?? string.Empty;
54 | var destination = Path.Combine(parentPath, newName);
55 | directory.MoveTo(destination);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Common/FileInfoExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.PWABuilder.IOS.Web.Common
8 | {
9 | ///
10 | /// Extensions for FileInfo
11 | ///
12 | public static class FileInfoExtensions
13 | {
14 | ///
15 | /// Renames the specified file.
16 | ///
17 | ///
18 | ///
19 | public static void Rename(this FileInfo file, string newName)
20 | {
21 | var directory = file.DirectoryName ?? string.Empty;
22 | var newFullPath = Path.Combine(directory, newName);
23 | file.MoveTo(newFullPath);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Common/HttpClientExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.PWABuilder.IOS.Web.Common
8 | {
9 | public static class HttpClientExtensions
10 | {
11 | public static void AddLatestEdgeUserAgent(this HttpClient http)
12 | {
13 | var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 Edg/96.0.1054.57 PWABuilderHttpAgent";
14 | http.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Common/ImageTargetSizeExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.PWABuilder.IOS.Web.Models;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.PWABuilder.IOS.Web.Common
8 | {
9 | public static class ImageTargetSizeExtensions
10 | {
11 | public static string ToFileName(this ImageTargetSize size)
12 | {
13 | return size switch
14 | {
15 | ImageTargetSize.Size16x16 => "16",
16 | ImageTargetSize.Size20x20 => "20",
17 | ImageTargetSize.Size29x29 => "29",
18 | ImageTargetSize.Size32x32 => "32",
19 | ImageTargetSize.Size40x40 => "40",
20 | ImageTargetSize.Size50x50 => "50",
21 | ImageTargetSize.Size57x57 => "57",
22 | ImageTargetSize.Size58x58 => "58",
23 | ImageTargetSize.Size60x60 => "60",
24 | ImageTargetSize.Size64x64 => "64",
25 | ImageTargetSize.Size72x72 => "72",
26 | ImageTargetSize.Size76x76 => "76",
27 | ImageTargetSize.Size80x80 => "80",
28 | ImageTargetSize.Size87x87 => "87",
29 | ImageTargetSize.Size100x100 => "100",
30 | ImageTargetSize.Size114x114 => "114",
31 | ImageTargetSize.Size120x120 => "120",
32 | ImageTargetSize.Size128x128 => "128",
33 | ImageTargetSize.Size144x144 => "144",
34 | ImageTargetSize.Size152x152 => "152",
35 | ImageTargetSize.Size167x167 => "167",
36 | ImageTargetSize.Size180x180 => "180",
37 | ImageTargetSize.Size192x192 => "192",
38 | ImageTargetSize.Size256x256 => "256",
39 | ImageTargetSize.Size512x512 => "512",
40 | ImageTargetSize.Size1024x1024 => "1024",
41 | _ => throw new NotImplementedException("Unexpected target size " + size)
42 | };
43 | }
44 |
45 | public static (int width, int height) GetDimensions(this ImageTargetSize size)
46 | {
47 | return size switch
48 | {
49 | ImageTargetSize.Size16x16 => (16, 16),
50 | ImageTargetSize.Size20x20 => (20, 20),
51 | ImageTargetSize.Size29x29 => (29, 29),
52 | ImageTargetSize.Size32x32 => (32, 32),
53 | ImageTargetSize.Size40x40 => (40, 40),
54 | ImageTargetSize.Size50x50 => (50, 50),
55 | ImageTargetSize.Size57x57 => (57, 57),
56 | ImageTargetSize.Size58x58 => (58, 58),
57 | ImageTargetSize.Size60x60 => (60, 60),
58 | ImageTargetSize.Size64x64 => (64, 64),
59 | ImageTargetSize.Size72x72 => (72, 72),
60 | ImageTargetSize.Size76x76 => (76, 76),
61 | ImageTargetSize.Size80x80 => (80, 80),
62 | ImageTargetSize.Size87x87 => (87, 87),
63 | ImageTargetSize.Size100x100 => (100, 100),
64 | ImageTargetSize.Size114x114 => (114, 114),
65 | ImageTargetSize.Size120x120 => (120, 120),
66 | ImageTargetSize.Size128x128 => (128, 128),
67 | ImageTargetSize.Size144x144 => (144, 144),
68 | ImageTargetSize.Size152x152 => (152, 152),
69 | ImageTargetSize.Size167x167 => (167, 167),
70 | ImageTargetSize.Size180x180 => (180, 180),
71 | ImageTargetSize.Size192x192 => (192, 192),
72 | ImageTargetSize.Size256x256 => (256, 256),
73 | ImageTargetSize.Size512x512 => (512, 512),
74 | ImageTargetSize.Size1024x1024 => (1024, 1024),
75 | _ => throw new NotSupportedException("Unknown image target size " + size)
76 | };
77 | }
78 |
79 | public static IEnumerable GetAll()
80 | {
81 | return new[]
82 | {
83 | ImageTargetSize.Size16x16,
84 | ImageTargetSize.Size20x20,
85 | ImageTargetSize.Size29x29,
86 | ImageTargetSize.Size32x32,
87 | ImageTargetSize.Size40x40,
88 | ImageTargetSize.Size50x50,
89 | ImageTargetSize.Size57x57,
90 | ImageTargetSize.Size58x58,
91 | ImageTargetSize.Size60x60,
92 | ImageTargetSize.Size64x64,
93 | ImageTargetSize.Size72x72,
94 | ImageTargetSize.Size76x76,
95 | ImageTargetSize.Size80x80,
96 | ImageTargetSize.Size87x87,
97 | ImageTargetSize.Size100x100,
98 | ImageTargetSize.Size114x114,
99 | ImageTargetSize.Size120x120,
100 | ImageTargetSize.Size128x128,
101 | ImageTargetSize.Size144x144,
102 | ImageTargetSize.Size152x152,
103 | ImageTargetSize.Size167x167,
104 | ImageTargetSize.Size180x180,
105 | ImageTargetSize.Size192x192,
106 | ImageTargetSize.Size256x256,
107 | ImageTargetSize.Size512x512,
108 | ImageTargetSize.Size1024x1024,
109 | };
110 | }
111 |
112 | public static bool IsLaunchIconSize(this ImageTargetSize targetSize)
113 | {
114 | return targetSize switch
115 | {
116 | ImageTargetSize.Size64x64 => true,
117 | ImageTargetSize.Size128x128 => true,
118 | ImageTargetSize.Size192x192 => true,
119 | ImageTargetSize.Size256x256 => true,
120 | ImageTargetSize.Size512x512 => true,
121 | _ => false
122 | };
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Common/UriExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Microsoft.PWABuilder.IOS.Web.Common
4 | {
5 | public static class UriExtensions
6 | {
7 | ///
8 | /// Converts the URI to a string while omitting the protocol and trailing slash.
9 | ///
10 | ///
11 | ///
12 | public static string ToIOSHostString(this Uri uri)
13 | {
14 | return uri.ToString()
15 | .Replace(uri.Scheme + "://", string.Empty)
16 | .TrimEnd('/');
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Common/ZipArchiveExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.IO.Compression;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace Microsoft.PWABuilder.IOS.Web.Common
9 | {
10 | public static class ZipArchiveExtensions
11 | {
12 | ///
13 | /// Adds a directory and all its contents to a zip file.
14 | ///
15 | ///
16 | ///
17 | ///
18 | ///
19 | public static void CreateEntryFromDirectory(this ZipArchive zip, string directory, string entryName)
20 | {
21 | if (Directory.Exists(directory))
22 | {
23 | var files = Directory.GetFiles(directory);
24 | foreach (var file in files)
25 | {
26 | zip.CreateEntryFromFile(file, $"{entryName}/{Path.GetFileName(file)}");
27 | }
28 |
29 | var directories = Directory.GetDirectories(directory);
30 | foreach (var subDirectory in directories)
31 | {
32 | var dirEntryName = $"{entryName}/{new DirectoryInfo(subDirectory).Name}";
33 | zip.CreateEntryFromDirectory(subDirectory, dirEntryName);
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Controllers/HomeController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.PWABuilder.IOS.Web.Controllers
8 | {
9 | public class HomeController : ControllerBase
10 | {
11 | [Route("/")]
12 | public IActionResult Index()
13 | {
14 | return File("index.html", "text/html");
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Controllers/PackagesController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.PWABuilder.IOS.Web.Models;
4 | using Microsoft.PWABuilder.IOS.Web.Services;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Threading.Tasks;
9 |
10 | namespace Microsoft.PWABuilder.IOS.Web.Controllers
11 | {
12 | [ApiController]
13 | [Route("[controller]/[action]")]
14 | public class PackagesController : ControllerBase
15 | {
16 | private readonly ILogger logger;
17 | private readonly IOSPackageCreator packageCreator;
18 | private readonly AnalyticsService analytics;
19 |
20 | public PackagesController(
21 | IOSPackageCreator packageCreator,
22 | AnalyticsService analytics,
23 | ILogger logger)
24 | {
25 | this.packageCreator = packageCreator;
26 | this.analytics = analytics;
27 | this.logger = logger;
28 | }
29 |
30 | [HttpPost]
31 | public async Task Create(IOSAppPackageOptions options)
32 | {
33 | AnalyticsInfo analyticsInfo = new();
34 |
35 | if (HttpContext?.Request.Headers != null)
36 | {
37 | analyticsInfo.platformId = HttpContext.Request.Headers.TryGetValue("platform-identifier", out var id) ? id.ToString() : null;
38 | analyticsInfo.platformIdVersion = HttpContext.Request.Headers.TryGetValue("platform-identifier-version", out var version) ? version.ToString() : null;
39 | analyticsInfo.correlationId = HttpContext.Request.Headers.TryGetValue("correlation-id", out var corrId) ? corrId.ToString() : null;
40 | analyticsInfo.referrer = HttpContext.Request.Query.TryGetValue("ref", out var referrer) ? referrer.ToString() : null;
41 | }
42 |
43 | try
44 | {
45 | var optionsValidated = ValidateOptions(options);
46 | var packageBytes = await packageCreator.Create(optionsValidated);
47 | analytics.Record(optionsValidated.Url.ToString(), success: true, optionsValidated, analyticsInfo, error: null);
48 | return File(packageBytes, "application/zip", $"{options.Name}-ios-app-package.zip");
49 | }
50 | catch (Exception error)
51 | {
52 | analytics.Record(options.Url ?? "https://EMPTY_URL", success: false, null, analyticsInfo, error: error.ToString());
53 | throw;
54 | }
55 | }
56 |
57 | private IOSAppPackageOptions.Validated ValidateOptions(IOSAppPackageOptions options)
58 | {
59 | try
60 | {
61 | return options.Validate();
62 | }
63 | catch (Exception error)
64 | {
65 | logger.LogError(error, "Invalid package options");
66 | throw;
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
2 | WORKDIR /app
3 | EXPOSE 5000
4 |
5 | ENV ASPNETCORE_URLS=http://+:5000
6 |
7 | # Creates a non-root user with an explicit UID and adds permission to access the /app folder
8 | # For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers
9 | RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
10 | USER appuser
11 |
12 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
13 | WORKDIR /src
14 | COPY ["Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj", "Microsoft.PWABuilder.IOS.Web/"]
15 | RUN dotnet restore "Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj"
16 | COPY . .
17 | WORKDIR "/src/Microsoft.PWABuilder.IOS.Web"
18 | RUN dotnet build "Microsoft.PWABuilder.IOS.Web.csproj" -c Release -o /app/build
19 |
20 | FROM build AS publish
21 | RUN dotnet publish "Microsoft.PWABuilder.IOS.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false
22 |
23 | FROM base AS final
24 | WORKDIR /app
25 | COPY --from=publish /app/publish .
26 | ENTRYPOINT ["dotnet", "Microsoft.PWABuilder.IOS.Web.dll"]
27 |
28 | USER root
29 | RUN chown -R appuser /app
30 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | PreserveNewest
16 |
17 |
18 | PreserveNewest
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Microsoft.PWABuilder.IOS.Web.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.31112.23
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PWABuilder.IOS.Web", "Microsoft.PWABuilder.IOS.Web.csproj", "{EBAE62D1-9FFA-4CB9-8836-2DA3944AD677}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2B9615B2-4ECC-400F-81A7-CD74BC862276}"
9 | ProjectSection(SolutionItems) = preProject
10 | ..\faq.md = ..\faq.md
11 | ..\next-steps.md = ..\next-steps.md
12 | ..\README.md = ..\README.md
13 | ..\submit-to-app-store.md = ..\submit-to-app-store.md
14 | EndProjectSection
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Release|Any CPU = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {EBAE62D1-9FFA-4CB9-8836-2DA3944AD677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {EBAE62D1-9FFA-4CB9-8836-2DA3944AD677}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {EBAE62D1-9FFA-4CB9-8836-2DA3944AD677}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {EBAE62D1-9FFA-4CB9-8836-2DA3944AD677}.Release|Any CPU.Build.0 = Release|Any CPU
26 | EndGlobalSection
27 | GlobalSection(SolutionProperties) = preSolution
28 | HideSolutionNode = FALSE
29 | EndGlobalSection
30 | GlobalSection(ExtensibilityGlobals) = postSolution
31 | SolutionGuid = {6AE0E01E-4C6C-473B-9B08-968B14CF652A}
32 | EndGlobalSection
33 | EndGlobal
34 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/AnalyticsInfo.cs:
--------------------------------------------------------------------------------
1 | namespace Microsoft.PWABuilder.IOS.Web.Models
2 | {
3 | public class AnalyticsInfo
4 | {
5 | public string? platformId { get; set; } = null;
6 | public string? platformIdVersion { get; set; } = null;
7 | public string? correlationId { get; set; } = null;
8 | public string? referrer { get; set; } = null;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/AppSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Microsoft.PWABuilder.IOS.Web.Models
7 | {
8 | public class AppSettings
9 | {
10 | public string IOSSourceCodePath { get; set; } = string.Empty;
11 | public string NextStepsPath { get; set; } = string.Empty;
12 | public string ImageGeneratorApiUrl { get; set; } = string.Empty;
13 | public string AnalyticsUrl { get; set; } = string.Empty;
14 | public string ApplicationInsightsConnectionString { get; set; } = string.Empty;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/Color.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics.CodeAnalysis;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.PWABuilder.IOS.Web.Models
8 | {
9 | public class Color
10 | {
11 | public Color(byte r, byte g, byte b)
12 | {
13 | this.R = r;
14 | this.G = g;
15 | this.B = b;
16 | }
17 |
18 | public byte R { get; init; }
19 | public byte G { get; init; }
20 | public byte B { get; init; }
21 |
22 | ///
23 | /// Gets a value between 0 and 1 representing the percentage of the value to the max of 255.
24 | ///
25 | ///
26 | ///
27 | public double GetRgbPercentage(byte val) => (double)val / 255;
28 |
29 | public string ToStoryboardColorString()
30 | {
31 | return $"red=\"{GetRgbPercentage(R)}\" green=\"{GetRgbPercentage(G)}\" blue=\"{GetRgbPercentage(B)}\"";
32 | }
33 |
34 | public static bool TryParseHexColor(string? hexString, [NotNullWhen(true)] out Color? validColor)
35 | {
36 | if (string.IsNullOrWhiteSpace(hexString))
37 | {
38 | validColor = null;
39 | return false;
40 | }
41 |
42 | var hexRegex = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"; // See https://www.geeksforgeeks.org/how-to-validate-hexadecimal-color-code-using-regular-expression/
43 | if (System.Text.RegularExpressions.Regex.IsMatch(hexString, hexRegex))
44 | {
45 | // If it's shorthand hex (#fff), convert to longhand (#ffffff).
46 | if (hexString.Length == 4)
47 | {
48 | hexString = $"#{hexString[1]}{hexString[1]}{hexString[2]}{hexString[2]}{hexString[3]}{hexString[3]}";
49 | }
50 |
51 | // We should never hit this, as regex checks for this above.
52 | if (hexString.Length != 7)
53 | {
54 | validColor = null;
55 | return false;
56 | }
57 |
58 | var r = Convert.ToByte(hexString[1..3], 16);
59 | var g = Convert.ToByte(hexString[3..5], 16);
60 | var b = Convert.ToByte(hexString[5..], 16);
61 | validColor = new Color(r, g, b);
62 | return true;
63 | }
64 |
65 | validColor = null;
66 | return false;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/HttpMessageStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net.Http;
6 | using System.Threading.Tasks;
7 |
8 | namespace Microsoft.PWABuilder.IOS.Web.Models
9 | {
10 | ///
11 | /// A readable stream opened from an . Upon disposal, both the stream and the message are disposed.
12 | ///
13 | public class HttpMessageStream : Stream
14 | {
15 | private readonly Stream stream;
16 | private readonly HttpResponseMessage message;
17 |
18 | public HttpMessageStream(Stream stream, HttpResponseMessage message)
19 | {
20 | this.stream = stream;
21 | this.message = message;
22 | }
23 |
24 | public override bool CanRead => stream.CanRead;
25 |
26 | public override bool CanSeek => stream.CanSeek;
27 |
28 | public override bool CanWrite => stream.CanWrite;
29 |
30 | public override long Length => stream.Length;
31 |
32 | public override long Position { get => stream.Position; set => stream.Position = value; }
33 |
34 | public override void Flush() => stream.Flush();
35 |
36 | public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count);
37 |
38 | public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin);
39 |
40 | public override void SetLength(long value) => stream.SetLength(value);
41 |
42 | public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count);
43 |
44 | protected override void Dispose(bool disposing)
45 | {
46 | if (disposing)
47 | {
48 | this.message.Dispose();
49 | this.stream.Dispose();
50 | }
51 |
52 | base.Dispose(disposing);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/IOSAppPackageOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Microsoft.PWABuilder.IOS.Web.Models
7 | {
8 | ///
9 | /// Options for creating an iOS PWA package.
10 | ///
11 | public class IOSAppPackageOptions
12 | {
13 | ///
14 | /// The app name.
15 | ///
16 | public string? Name { get; set; }
17 |
18 | ///
19 | /// The bundle ID to use for the package.
20 | /// Apple recommends using a reverse-domain name style string (i.e., com.domainname.appname)
21 | /// This should be at least 3 characters in length. It cannot contain an asterisk (*).
22 | ///
23 | public string? BundleId { get; set; }
24 |
25 | ///
26 | /// Your PWA's URL.
27 | ///
28 | public string? Url { get; set; }
29 |
30 | ///
31 | /// The URL of image for your app icon. We recommend a 512x512 square PNG or larger.
32 | ///
33 | public string? ImageUrl { get; set; }
34 |
35 | ///
36 | /// The color to use as the background of your app's splash screen.
37 | ///
38 | public string? SplashColor { get; set; }
39 |
40 | ///
41 | /// The color of the loading progress bar on your app's splash screen.
42 | ///
43 | public string? ProgressBarColor { get; set; }
44 |
45 | ///
46 | /// The color of the iOS status bar while your app is running. The status bar shows at the top of the phone, and contains system information reception bars, battery life indicator, time, etc.
47 | /// This should typically be the prominent background color of your app.
48 | ///
49 | public string? StatusBarColor { get; set; }
50 |
51 | ///
52 | /// The list of domains your app is permitted to navigate. This will automatically include , so no need to include it again.
53 | /// This should contain any domains you expect your users to navigate to while using your app, for example, account.google.com or other authentication domains.
54 | /// It is not necessary to include the protocol with the URL.
55 | ///
56 | public List? PermittedUrls { get; set; }
57 |
58 | ///
59 | /// Your PWA's web manifest.
60 | ///
61 | public WebAppManifest? Manifest { get; set; }
62 |
63 | ///
64 | /// The URL to your PWA's manifest.
65 | ///
66 | public string? ManifestUrl { get; set; }
67 |
68 | public Validated Validate()
69 | {
70 | if (!Uri.TryCreate(ManifestUrl, UriKind.Absolute, out var manifestUri))
71 | {
72 | throw new ArgumentException("Manifest url must a valid, absolute URI");
73 | }
74 | if (string.IsNullOrWhiteSpace(Name))
75 | {
76 | throw new ArgumentNullException(nameof(Name));
77 | }
78 | if (string.IsNullOrWhiteSpace(Url))
79 | {
80 | throw new ArgumentNullException(nameof(Url));
81 | }
82 | if (!Uri.TryCreate(manifestUri, Url, out var uri))
83 | {
84 | throw new ArgumentException("Url must be a valid, absolute URI");
85 | }
86 | if (string.IsNullOrWhiteSpace(ImageUrl))
87 | {
88 | throw new ArgumentNullException(nameof(ImageUrl));
89 | }
90 | if (!Uri.TryCreate(manifestUri, ImageUrl, out var imageUri))
91 | {
92 | throw new ArgumentException("Image url must be a valid, absolute URI");
93 | }
94 | if (Manifest == null)
95 | {
96 | throw new ArgumentNullException(nameof(Manifest));
97 | }
98 | if (string.IsNullOrWhiteSpace(BundleId))
99 | {
100 | throw new ArgumentNullException(nameof(BundleId));
101 | }
102 | if (BundleId.Length < 3)
103 | {
104 | throw new ArgumentOutOfRangeException(nameof(BundleId), BundleId, "Bundle ID must be at least 3 characters in length");
105 | }
106 | if (BundleId.Contains("*"))
107 | {
108 | throw new ArgumentOutOfRangeException(nameof(BundleId), BundleId, "Bundle ID cannot contain an asterisk (*).");
109 | }
110 |
111 | var validSplashColor = GetValidColor(this.SplashColor, this.Manifest.Background_color, "#ffffff");
112 | var validProgressColor = GetValidColor(this.ProgressBarColor, this.Manifest.Theme_color, "#000000");
113 | var validStatusBarColor = GetValidColor(this.StatusBarColor, this.Manifest.Background_color, "#ffffff");
114 | var permittedUris = (PermittedUrls ?? new List(0))
115 | .Select(url => GetUriFromWithProtocol(url))
116 | .Where(url => url != null)
117 | .Select(url => url!)
118 | .ToList();
119 | return new Validated(
120 | Name.Trim(),
121 | BundleId.Trim(),
122 | uri,
123 | imageUri,
124 | validSplashColor,
125 | validProgressColor,
126 | validStatusBarColor,
127 | permittedUris,
128 | Manifest,
129 | manifestUri);
130 | }
131 |
132 | private static Color GetValidColor(string? desiredColor, string? manifestColor, string fallbackColor)
133 | {
134 | var colors = new[] { desiredColor?.Trim(), manifestColor?.Trim(), fallbackColor };
135 | foreach (var color in colors)
136 | {
137 | if (Color.TryParseHexColor(color, out var validColor))
138 | {
139 | return validColor;
140 | }
141 | }
142 |
143 | throw new ArgumentException("None of the potential colors were valid hex colors");
144 | }
145 |
146 | private static Uri? GetUriFromWithProtocol(string input)
147 | {
148 | if (Uri.TryCreate(input, UriKind.Absolute, out var uri))
149 | {
150 | return uri;
151 | }
152 |
153 | if (Uri.TryCreate("https://" + input, UriKind.Absolute, out var httpsUri))
154 | {
155 | return httpsUri;
156 | }
157 |
158 | return null;
159 | }
160 |
161 | public record Validated(
162 | string Name,
163 | string BundleId,
164 | Uri Url,
165 | Uri ImageUri,
166 | Color SplashColor,
167 | Color ProgressBarColor,
168 | Color StatusBarColor,
169 | List PermittedUrls,
170 | WebAppManifest Manifest,
171 | Uri ManifestUri);
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/IOSAppShortcut.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Microsoft.PWABuilder.IOS.Web.Models
4 | {
5 | ///
6 | /// Represents an iOS app shortcut.
7 | ///
8 | public class IOSAppShortcut
9 | {
10 | public IOSAppShortcut(string name, Uri uri)
11 | {
12 | this.Name = name;
13 | this.Uri = uri;
14 | }
15 |
16 | ///
17 | /// Attempts to create an iOS app shortcut from a W3C web manifest shortcut.
18 | ///
19 | /// The web manifest shortcut to try to convert to an iOS app shortcut.
20 | /// The URI of the web manifest, used for resolving relative shortcut URIs to absolute URIs.
21 | /// An iOS web app shortcut, or null if one could not be created.
22 | public static IOSAppShortcut? FromWebManifestShortcut(WebManifestShortcutItem webManifestShortcut, Uri webManifestUri)
23 | {
24 | var name = !string.IsNullOrWhiteSpace(webManifestShortcut.Name) ? webManifestShortcut.Name : webManifestShortcut.Short_name;
25 | if (string.IsNullOrWhiteSpace(name))
26 | {
27 | return null;
28 | }
29 |
30 | Uri.TryCreate(webManifestUri, webManifestShortcut.Url, out var uri);
31 | if (uri == null)
32 | {
33 | return null;
34 | }
35 |
36 | return new IOSAppShortcut(name, uri);
37 | }
38 |
39 | public string Name { get; init; }
40 | public Uri Uri { get; init; }
41 |
42 | ///
43 | /// Converts the iOS app shortcut into an Info.plist-ready XML element string.
44 | ///
45 | /// Function that converts a plain string into an XML value-safe string.
46 | /// An XML fragment containing the app shortcut formatted for Info.plist
47 | public string ToInfoPlistEntry(Func safeXmlValueConverter)
48 | {
49 | return "\t\t\n" +
50 | "\t\t\tUIApplicationShortcutItemType\n" +
51 | $"\t\t\t{safeXmlValueConverter(this.Uri.ToString())}\n" +
52 | "\t\t\tUIApplicationShortcutItemTitle\n" +
53 | $"\t\t\t{safeXmlValueConverter(this.Name)}\n" +
54 | "\t\t";
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/IconFormat.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Microsoft.PWABuilder.IOS.Web.Models
7 | {
8 | ///
9 | /// Common formats for web manifest icons.
10 | ///
11 | public enum IconFormat
12 | {
13 | ///
14 | /// .png format
15 | ///
16 | Png,
17 | ///
18 | /// .jpg format
19 | ///
20 | Jpg,
21 | ///
22 | /// .gif format
23 | ///
24 | Gif,
25 | ///
26 | /// .svg format
27 | ///
28 | Svg,
29 | ///
30 | /// Icon format
31 | ///
32 | Ico,
33 | ///
34 | /// .webp format
35 | ///
36 | Webp,
37 | ///
38 | /// No format is specified.
39 | ///
40 | Unspecified,
41 | ///
42 | /// An unknown format is specified but we don't handle it in the Windows platform code.
43 | ///
44 | Other
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/ImageGeneratorResult.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Microsoft.PWABuilder.IOS.Web.Models
7 | {
8 | public record ImageGeneratorResult(List ImagePaths);
9 | }
10 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/ImageGeneratorServiceZipFile.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.PWABuilder.IOS.Web.Common;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO.Compression;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace Microsoft.PWABuilder.IOS.Web.Models
9 | {
10 | public sealed class ImageGeneratorServiceZipFile : IDisposable
11 | {
12 | private readonly ZipArchive zip;
13 |
14 | public ImageGeneratorServiceZipFile(ZipArchive zip)
15 | {
16 | this.zip = zip;
17 | }
18 |
19 | public ZipArchiveEntry? GetTargetSize(ImageTargetSize size)
20 | {
21 | // Square44x44Logo isn't a typo here - the image generator service uses those files names, then appends the actual size to the file name.
22 | return zip.GetEntry($"ios/{size.ToFileName()}.png");
23 | }
24 |
25 | public void Dispose()
26 | {
27 | zip.Dispose();
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/ImageSource.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.PWABuilder.IOS.Web.Common;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO.Compression;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace Microsoft.PWABuilder.IOS.Web.Models
9 | {
10 | ///
11 | /// An image source used for the app package. Contains 2 potential sources of an image:
12 | /// an image URI specified in the web app manifest,
13 | /// an image entry in a zip file generated by the PWABuilder app image service.
14 | ///
15 | public class ImageSource
16 | {
17 | ///
18 | /// Gets the URI of the image taken from the PWA's web manifest.
19 | /// This has priority 1.
20 | ///
21 | public Uri? WebManifestSource { get; set; }
22 |
23 | ///
24 | /// Gets the zip entry of the image that was generated on behalf of the user.
25 | /// This is the lowest priority source, priority 2.
26 | ///
27 | public ZipArchiveEntry? GeneratedImageSource { get; set; }
28 |
29 | ///
30 | /// Gets the target file name.
31 | ///
32 | public string TargetFileName { get; set; } = string.Empty;
33 |
34 | ///
35 | /// Gets the size of the image source.
36 | ///
37 | public ImageTargetSize Size { get; set; }
38 |
39 | ///
40 | /// Creates an ImageSource for the specified scale set.
41 | ///
42 | ///
43 | ///
44 | ///
45 | ///
46 | ///
47 | ///
48 | public static ImageSource From(ImageTargetSize targetSize, WebAppManifestContext webManifest, ImageGeneratorServiceZipFile zip)
49 | {
50 | return new ImageSource
51 | {
52 | Size = targetSize,
53 | TargetFileName = targetSize.ToFileName() + ".png",
54 | WebManifestSource = webManifest.GetIconUriFromTargetSize(targetSize),
55 | GeneratedImageSource = zip.GetTargetSize(targetSize)
56 | };
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/ImageTargetSize.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Microsoft.PWABuilder.IOS.Web.Models
7 | {
8 | public enum ImageTargetSize
9 | {
10 | Size16x16,
11 | Size20x20,
12 | Size29x29,
13 | Size32x32,
14 | Size40x40,
15 | Size50x50,
16 | Size57x57,
17 | Size58x58,
18 | Size60x60,
19 | Size64x64,
20 | Size72x72,
21 | Size76x76,
22 | Size80x80,
23 | Size87x87,
24 | Size100x100,
25 | Size114x114,
26 | Size120x120,
27 | Size128x128,
28 | Size144x144,
29 | Size152x152,
30 | Size167x167,
31 | Size180x180,
32 | Size192x192,
33 | Size256x256,
34 | Size512x512,
35 | Size1024x1024
36 | // any additions to this list should be updated in ImageTargetSizeExtensions
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/WebAppManifest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Microsoft.PWABuilder.IOS.Web.Models
7 | {
8 | ///
9 | /// W3C web manifest. https://www.w3.org/TR/appmanifest/
10 | ///
11 | public class WebAppManifest
12 | {
13 | public string? Background_color { get; set; }
14 | public string? Description { get; set; }
15 | public string? Dir { get; set; }
16 | public string? Display { get; set; }
17 | public string? Lang { get; set; }
18 | public string? Name { get; set; }
19 | public string? Orientation { get; set; }
20 | public bool? Prefer_related_applications { get; set; }
21 | public string? Scope { get; set; }
22 | public string? Short_name { get; set; }
23 | public string? Start_url { get; set; }
24 | public string? Theme_color { get; set; }
25 | public string? Url { get; set; }
26 | public List? Categories { get; set; }
27 | public List? Screenshots { get; set; }
28 | public string? Iarc_rating_id { get; set; }
29 | public List? Icons { get; set; }
30 | public List? Shortcuts { get; set; }
31 |
32 | ///
33 | /// Finds a general purpose icon with the specified dimensions.
34 | ///
35 | /// A match
36 | public WebManifestIcon? GetIconWithDimensions(string dimensions, string purpose = "any")
37 | {
38 | var widthAndHeight = dimensions.Split('x', StringSplitOptions.RemoveEmptyEntries);
39 | if (!int.TryParse(widthAndHeight.ElementAtOrDefault(0), out var width) || !int.TryParse(widthAndHeight.ElementAtOrDefault(1), out var height))
40 | {
41 | throw new ArgumentException($"Invalid dimensions string. Expected format 100x100, but received {dimensions}", nameof(dimensions));
42 | }
43 |
44 | return GetIconsWithDimensions(width, height)
45 | .Where(i => i.HasPurpose(purpose))
46 | .FirstOrDefault();
47 | }
48 |
49 | ///
50 | /// Finds a general purpose icon with the specified dimensions.
51 | ///
52 | /// A match
53 | public IEnumerable GetIconsWithDimensions(int width, int height)
54 | {
55 | if (this.Icons == null)
56 | {
57 | return Enumerable.Empty();
58 | }
59 |
60 | // Find icons that have the specified dimensions, ordered by those with purpose = "any" (or empty), then ordered by png, then jpg.
61 | return this.Icons
62 | .Where(i => i.GetAllDimensions().Any(d => d.width == width && d.height == height))
63 | .OrderBy(i => !string.IsNullOrEmpty(i.Src) ? 0 : 1)
64 | .ThenBy(i => i.GetImageFormatPreferredSortOrder());
65 | }
66 | }
67 |
68 | public class WebManifestIcon
69 | {
70 | public string? Src { get; set; }
71 | public string? Type { get; set; }
72 | public string? Sizes { get; set; }
73 | public string? Purpose { get; set; } // "any" | "maskable" | "monochrome";
74 | public string? Platform { get; set; }
75 |
76 | ///
77 | /// Color scheme. See https://github.com/w3c/image-resource/issues/26
78 | ///
79 | public string? Color_scheme { get; set; }
80 |
81 | public Uri? GetSrcUri(Uri manifestUri)
82 | {
83 | if (Uri.TryCreate(manifestUri, this.Src, out var iconUri))
84 | {
85 | return iconUri;
86 | }
87 |
88 | return null;
89 | }
90 |
91 | public IconFormat GetFormat()
92 | {
93 | return this.Type switch
94 | {
95 | "image/png" => IconFormat.Png,
96 | "image/jpeg" => IconFormat.Jpg,
97 | "image/jpg" => IconFormat.Jpg,
98 | "image/gif" => IconFormat.Gif,
99 | "image/x-icon" => IconFormat.Ico,
100 | "image/vnd.microsoft.icon" => IconFormat.Ico,
101 | "image/svg+xml" => IconFormat.Svg,
102 | "image/svg" => IconFormat.Svg,
103 | "image/webp" => IconFormat.Webp,
104 | _ => GuessFormatFromExtension()
105 | };
106 | }
107 |
108 | ///
109 | /// Whether the is "light".
110 | ///
111 | ///
112 | public bool IsLightMode()
113 | {
114 | return string.Equals(this.Color_scheme, "light", StringComparison.OrdinalIgnoreCase);
115 | }
116 |
117 | public bool HasPurpose(string purpose)
118 | {
119 | // Special case: if purpose is empty, it should match "any" purpose.
120 | var isLookingAnyPurpose = string.Equals(purpose, "any", StringComparison.InvariantCultureIgnoreCase);
121 | if (string.IsNullOrWhiteSpace(this.Purpose))
122 | {
123 | return isLookingAnyPurpose;
124 | }
125 |
126 | return this.Purpose.Split(' ')
127 | .Any(p => string.Equals(p, purpose, StringComparison.InvariantCultureIgnoreCase));
128 | }
129 |
130 | public bool IsAnyPurpose()
131 | {
132 | return this.HasPurpose("any");
133 | }
134 |
135 | public bool IsSquare()
136 | {
137 | if (this.Sizes == null)
138 | {
139 | return false;
140 | }
141 |
142 | return this.GetAllDimensions()
143 | .Any(d => d.width == d.height);
144 | }
145 |
146 | ///
147 | /// Gets the largest dimension for the image.
148 | ///
149 | ///
150 | public (int width, int height)? GetLargestDimension()
151 | {
152 | var largest = GetAllDimensions()
153 | .OrderByDescending(i => i.width + i.height)
154 | .FirstOrDefault();
155 | if (largest.height == 0 && largest.width == 0)
156 | {
157 | return null;
158 | }
159 |
160 | return largest;
161 | }
162 |
163 | ///
164 | /// Finds the largest dimension from the property
165 | ///
166 | /// The largest dimension from the string. If no valid size could be found, null.
167 | public List<(int width, int height)> GetAllDimensions()
168 | {
169 | if (this.Sizes == null)
170 | {
171 | return new List<(int width, int height)>(0);
172 | }
173 |
174 | return this.Sizes.Split(' ', StringSplitOptions.RemoveEmptyEntries)
175 | .Select(size => size.Split('x', StringSplitOptions.RemoveEmptyEntries))
176 | .Select(widthAndHeight =>
177 | {
178 | if (int.TryParse(widthAndHeight.ElementAtOrDefault(0), out var width) &&
179 | int.TryParse(widthAndHeight.ElementAtOrDefault(1), out var height))
180 | {
181 | return (width, height);
182 | }
183 | return (width: 0, height: 0);
184 | })
185 | .Where(d => d.width != 0 && d.height != 0)
186 | .ToList();
187 | }
188 |
189 | public int GetImageFormatPreferredSortOrder()
190 | {
191 | return this.GetFormat() switch
192 | {
193 | IconFormat.Png => 0, // best format
194 | IconFormat.Jpg => 1, // Windows apps can use JPG
195 | IconFormat.Unspecified => 2, // If the format is unspecified, let's gamble and hope for the best.
196 | _ => 3, // deprioritize others because Windows app packages won't work with them: https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-uap-visualelements
197 | };
198 | }
199 |
200 | private IconFormat GuessFormatFromExtension()
201 | {
202 | // No src? Punt.
203 | if (string.IsNullOrWhiteSpace(this.Src))
204 | {
205 | return IconFormat.Unspecified;
206 | }
207 |
208 | var extensionFormats = new Dictionary
209 | {
210 | { ".png", IconFormat.Png },
211 | { ".jpg", IconFormat.Jpg },
212 | { ".jpeg", IconFormat.Jpg },
213 | { ".gif", IconFormat.Gif },
214 | { ".ico", IconFormat.Ico },
215 | { ".svg", IconFormat.Svg },
216 | { ".webp", IconFormat.Webp }
217 | };
218 |
219 | foreach (var (extension, format) in extensionFormats)
220 | {
221 | if (this.Src.EndsWith(extension, StringComparison.InvariantCultureIgnoreCase))
222 | {
223 | return format;
224 | }
225 | }
226 |
227 | return IconFormat.Other;
228 | }
229 | }
230 |
231 | public class WebManifestShortcutItem {
232 | public string? Name { get; set; }
233 | public string? Url { get; set; }
234 | public string? Description { get; set; }
235 | public string? Short_name { get; set; }
236 | public List? Icons { get; set; }
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/WebAppManifestContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.PWABuilder.IOS.Web.Common;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.PWABuilder.IOS.Web.Models
8 | {
9 | ///
10 | /// Contains a web app manifest and the URL where the manifest was located.
11 | ///
12 | public class WebAppManifestContext : WebAppManifest
13 | {
14 | ///
15 | /// The URI from which the manifest was fetched.
16 | ///
17 | public Uri ManifestUri { get; set; } = new Uri("https://localhost");
18 |
19 | ///
20 | /// Creates a web app manifest context
21 | ///
22 | ///
23 | ///
24 | ///
25 | public static WebAppManifestContext From(WebAppManifest manifest, Uri manifestUri)
26 | {
27 | var context = new WebAppManifestContext
28 | {
29 | ManifestUri = manifestUri
30 | };
31 |
32 | var manifestProps = typeof(WebAppManifest).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
33 | foreach (var prop in manifestProps.Where(p => p.CanRead && p.CanWrite))
34 | {
35 | var propVal = prop.GetValue(manifest);
36 | prop.SetValue(context, propVal);
37 | }
38 |
39 | return context;
40 | }
41 |
42 | ///
43 | /// Gets an icon from the specified target size.
44 | ///
45 | ///
46 | ///
47 | ///
48 | public Uri? GetIconUriFromTargetSize(ImageTargetSize size)
49 | {
50 | // NOTE: we ignore altForm here because the web manifest doesn't support light mode icons or unplated icons.
51 | // If web manifest supports these in the future, we should use those here.
52 |
53 | // Find images with the right dimensions.
54 | var (width, height) = size.GetDimensions();
55 | var iconsMatchingDimensions = GetIconsWithDimensions(width, height)
56 | .Where(i => i.GetFormat() == IconFormat.Png);
57 |
58 | return iconsMatchingDimensions
59 | .Select(i => i.GetSrcUri(this.ManifestUri))
60 | .FirstOrDefault();
61 | }
62 |
63 | ///
64 | /// Finds the largest available icon that can be used as an app icon for iOS apps.
65 | /// Requirements:
66 | /// - Must be "any" purpose
67 | /// - Must be square
68 | /// - Must be PNG
69 | ///
70 | ///
71 | /// For more info, see https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-uap-visualelements
72 | ///
73 | /// The URI of the square icon, or null if no such icon could be found.
74 | /// The minimum dimensions to find.
75 | public Uri? GetIconSuitableForIOSApps(int minDimensions)
76 | {
77 | var isSuitable = new Func(i =>
78 | i.IsAnyPurpose() &&
79 | i.IsSquare() &&
80 | i.GetLargestDimension().GetValueOrDefault().width >= minDimensions);
81 |
82 | var iconsOrderByLargest = GetIconsOrderedByLargest();
83 | iconsOrderByLargest.TryGetValue(IconFormat.Png, out var pngIcons);
84 | iconsOrderByLargest.TryGetValue(IconFormat.Unspecified, out var unknownFormatIcons);
85 | var candidates = new[]
86 | {
87 | pngIcons?.FirstOrDefault(isSuitable),
88 | unknownFormatIcons?.FirstOrDefault(isSuitable) // Risky, but some manifests don't provide enough metadata to determine format. If it's the wrong format, we'll get an exception while building the package.
89 | };
90 | return candidates
91 | .Where(i => i != null) // first suitable icon
92 | .Select(i => i!.GetSrcUri(this.ManifestUri)) // grab its URL
93 | .Where(uri => uri != null) // filter out any that don't have a URL
94 | .FirstOrDefault();
95 | }
96 |
97 | ///
98 | /// Resolves a URI to an absolute path relative to this web manifest's path.
99 | ///
100 | /// The path to resolve.
101 | /// A new URI containing an absolute path relative to this web manifest's path.
102 | public Uri ResolveUri(string path)
103 | {
104 | return new Uri(this.ManifestUri, path);
105 | }
106 |
107 | ///
108 | /// Gets all the icons grouped by format. The icons (values of the dictionary) are sorted from largest to smallest.
109 | ///
110 | /// A dictionary containing keys and a list of values.
111 | private Dictionary> GetIconsOrderedByLargest()
112 | {
113 | var iconsOrEmpty = this.Icons ?? Enumerable.Empty();
114 | return iconsOrEmpty
115 | .GroupBy(i => i.GetFormat())
116 | .ToDictionary(
117 | i => i.Key, // icon format is the key
118 | i => i.OrderByDescending(i => i.GetLargestDimension().GetValueOrDefault().height + i.GetLargestDimension().GetValueOrDefault().width).ToList() // Icons sorted by largest dimensions are the value
119 | );
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/XcodeFile.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.PWABuilder.IOS.Web.Models
8 | {
9 | ///
10 | /// A file within an XCode workspace.
11 | ///
12 | public class XcodeFile : XcodeItem
13 | {
14 | private Queue>? sourceTransforms;
15 | private string? newFileName;
16 |
17 | public XcodeFile(string filePath)
18 | : base(filePath)
19 | {
20 | this.Name = Path.GetFileName(filePath);
21 | }
22 |
23 | ///
24 | /// Gets the file name.
25 | ///
26 | public override string Name
27 | {
28 | get;
29 | protected set;
30 | }
31 |
32 | ///
33 | /// Queues an update to rename the file. The update will be applied when is called.
34 | ///
35 | /// The new name.
36 | public void Rename(string newName)
37 | {
38 | this.newFileName = newName;
39 | }
40 |
41 | ///
42 | /// Queues an update to the file that replaces a string with another string. The update will be applied when is called.
43 | ///
44 | /// The string to replace.
45 | /// The replacement string.
46 | public void Replace(string existing, string replacement)
47 | {
48 | var replaceFunc = new Func(contents =>
49 | {
50 | if (!contents.Contains(existing))
51 | {
52 | throw new ArgumentException($"Expected {this.Name} to contain \"{existing}\", but it did not contain that string.");
53 | }
54 |
55 | return contents.Replace(existing, replacement);
56 | });
57 |
58 | if (this.sourceTransforms == null)
59 | {
60 | this.sourceTransforms = new Queue>(2);
61 | }
62 |
63 | this.sourceTransforms.Enqueue(replaceFunc);
64 | }
65 |
66 | ///
67 | /// Applies any queued changes to the file.
68 | ///
69 | ///
70 | public async Task ApplyChanges()
71 | {
72 | if (this.sourceTransforms == null || this.sourceTransforms.Count == 0)
73 | {
74 | return;
75 | }
76 |
77 | var contents = await File.ReadAllTextAsync(this.ItemPath);
78 | foreach (var transform in this.sourceTransforms)
79 | {
80 | contents = transform(contents);
81 | }
82 |
83 | sourceTransforms.Clear();
84 | await File.WriteAllTextAsync(this.ItemPath, contents);
85 |
86 | // Move the file if need be.
87 | if (!string.IsNullOrWhiteSpace(this.newFileName))
88 | {
89 | var directoryPath = Path.GetDirectoryName(this.ItemPath);
90 | var newFilePath = Path.Combine(directoryPath!, this.newFileName);
91 | File.Move(this.ItemPath, newFilePath);
92 |
93 | this.newFileName = null;
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/XcodeFolder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.PWABuilder.IOS.Web.Common;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace Microsoft.PWABuilder.IOS.Web.Models
9 | {
10 | ///
11 | /// A folder within an Xcode workspace.
12 | ///
13 | public class XcodeFolder : XcodeItem
14 | {
15 | private string? newDirectoryName;
16 |
17 | public XcodeFolder(string directoryPath)
18 | : base(directoryPath)
19 | {
20 | this.Name = System.IO.Path.GetFileName(directoryPath.TrimEnd('\\').TrimEnd('/'));
21 | }
22 |
23 | ///
24 | /// Gets the directory name.
25 | ///
26 | public override string Name
27 | {
28 | get;
29 | protected set;
30 | }
31 |
32 | public void Rename(string newName)
33 | {
34 | this.newDirectoryName = newName;
35 | }
36 |
37 | public void ApplyChanges()
38 | {
39 | if (string.IsNullOrWhiteSpace(this.newDirectoryName))
40 | {
41 | return;
42 | }
43 |
44 | new DirectoryInfo(this.ItemPath).Rename(this.newDirectoryName);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/XcodeItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace Microsoft.PWABuilder.IOS.Web.Models
7 | {
8 | public abstract class XcodeItem
9 | {
10 | protected XcodeItem(string path)
11 | {
12 | this.ItemPath = path;
13 | }
14 |
15 | public string ItemPath { get; protected init; }
16 |
17 | public abstract string Name { get; protected set; }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/XcodeProject.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.PWABuilder.IOS.Web.Models
8 | {
9 | ///
10 | /// Represents an XCode project workspace.
11 | /// Contains helper methods for finding and updating files within the workspace.
12 | ///
13 | public class XcodeProject
14 | {
15 | private readonly string rootDirectory;
16 | private readonly List folders = new(20);
17 | private readonly List files = new(80);
18 |
19 | public XcodeProject(string rootDirectory)
20 | {
21 | this.rootDirectory = rootDirectory;
22 | }
23 |
24 | ///
25 | /// Loads all the paths of the files and folders within the workspace.
26 | ///
27 | ///
28 | public void Load()
29 | {
30 | var directories = new Queue();
31 | directories.Enqueue(this.rootDirectory);
32 |
33 | // Go through the whole project and push files and folders into our items list.
34 | while (directories.Count > 0)
35 | {
36 | var dir = directories.Dequeue();
37 | this.folders.Add(new XcodeFolder(dir));
38 |
39 | var subDirs = Directory.EnumerateDirectories(dir);
40 | foreach (var subDir in subDirs)
41 | {
42 | directories.Enqueue(subDir);
43 | }
44 |
45 | foreach (var file in Directory.EnumerateFiles(dir))
46 | {
47 | this.files.Add(new XcodeFile(file));
48 | }
49 | }
50 | }
51 |
52 | public XcodeFile GetFile(string fileName)
53 | {
54 | var file = this.files.FirstOrDefault(f => string.Equals(f.Name, fileName, StringComparison.OrdinalIgnoreCase));
55 | if (file == null)
56 | {
57 | throw new FileNotFoundException("Unable to find file " + fileName);
58 | }
59 |
60 | return file;
61 | }
62 |
63 | public XcodeFile GetFileByPath(string partialOrCompletePath)
64 | {
65 | var file = this.files.FirstOrDefault(f => f.ItemPath.Contains(partialOrCompletePath, StringComparison.OrdinalIgnoreCase));
66 | if (file == null)
67 | {
68 | throw new FileNotFoundException("Unable to find file with path " + partialOrCompletePath);
69 | }
70 |
71 | return file;
72 | }
73 |
74 | public XcodeFolder GetFolder(string folderName)
75 | {
76 | var folder = this.folders.FirstOrDefault(f => string.Equals(f.Name, folderName, StringComparison.OrdinalIgnoreCase));
77 | if (folder == null)
78 | {
79 | throw new FileNotFoundException("Unable to find folder " + folder);
80 | }
81 |
82 | return folder;
83 | }
84 |
85 | ///
86 | /// Saves all the changes to disk.
87 | ///
88 | ///
89 | public async Task Save()
90 | {
91 | // Apply the changes to the files.
92 | foreach (var file in this.files)
93 | {
94 | await file.ApplyChanges();
95 | }
96 |
97 | // Move directories if need.
98 | foreach (var folder in this.folders)
99 | {
100 | folder.ApplyChanges();
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Models/XcodePwaShellProject.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.PWABuilder.IOS.Web.Common;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.IO;
7 |
8 | namespace Microsoft.PWABuilder.IOS.Web.Models
9 | {
10 | ///
11 | /// Models the pwa-shell Xcode project that serves as the template for generated PWA packages.
12 | ///
13 | public class XcodePwaShellProject : XcodeProject
14 | {
15 | private readonly IOSAppPackageOptions.Validated options;
16 | private readonly string macSafeProjectName;
17 | private readonly string swiftModuleName;
18 |
19 | public XcodePwaShellProject(IOSAppPackageOptions.Validated options, string rootDirectory)
20 | : base(rootDirectory)
21 | {
22 | this.options = options;
23 | this.macSafeProjectName = GetMacSafeFileName(options.Name);
24 | this.swiftModuleName = GetSwiftSafeModuleName(options.Name);
25 | }
26 |
27 | public async Task ApplyChanges()
28 | {
29 | UpdateAppColors();
30 | UpdateAppNameAndUrls();
31 | UpdateAppBundleId();
32 | RenameProjectFolders();
33 | UpdateProjectFolderReferences();
34 | UpdateModuleReferences();
35 |
36 | await this.Save();
37 | }
38 |
39 | private void UpdateAppColors()
40 | {
41 | var launchScreenStoryboard = GetFile("LaunchScreen.storyboard");
42 | var mainStoryboard = GetFile("Main.storyboard");
43 |
44 | // Set the splash color.
45 | // var existingSplashColorLine = "{{PWABuilder.iOS.splashBgColor}}";
46 | // var desiredSplashColorLine = $"";
47 | // launchScreenStoryboard.Replace(existingSplashColorLine, desiredSplashColorLine);
48 | // mainStoryboard.Replace(existingSplashColorLine, desiredSplashColorLine);
49 |
50 | // Set the status bar color
51 | // var existingStatusBarColorLine = "{{PWABuilder.iOS.statusBarColor}}";
52 | // var desiredStatusBarColorLine = $"";
53 | // mainStoryboard.Replace(existingStatusBarColorLine, desiredStatusBarColorLine);
54 |
55 | // Set the progress var color
56 | var existingProgressBarColorLine = "{{PWABuilder.iOS.progressBarColor}}";
57 | var desiredProgressBarColorLine = $"";
58 | mainStoryboard.Replace(existingProgressBarColorLine, desiredProgressBarColorLine);
59 | }
60 |
61 | private void UpdateAppNameAndUrls()
62 | {
63 | var infoPlistXmlFile = GetFile("Info.plist");
64 | var settingsFile = GetFile("Settings.swift");
65 | var entitlementsXmlFile = GetFile("Entitlements.plist");
66 |
67 | // Update app name
68 | var appNameExisting = "{{PWABuilder.iOS.appName}}";
69 | var appNameDesired = $"{GetXmlSafeNodeValue(options.Name)}";
70 | infoPlistXmlFile.Replace(appNameExisting, appNameDesired);
71 |
72 | // Add URL and permitted URLs to app bound domains (used for service worker) in Info.plist
73 | var urlExisting = "{{PWABuilder.iOS.permittedUrls}}";
74 | var urlDesiredBuilder = new System.Text.StringBuilder();
75 |
76 | // Append the URL of the PWA
77 | urlDesiredBuilder.Append($"{GetXmlSafeNodeValue(options.Url.ToIOSHostString())}"); // Append the URL of the PWA
78 |
79 | // Append the permitted URLs
80 | options.PermittedUrls
81 | .Select(url => url.ToIOSHostString())
82 | .Select(url => GetXmlSafeNodeValue(url))
83 | .ToList()
84 | .ForEach(url => urlDesiredBuilder.Append($"\n{url}"));
85 | infoPlistXmlFile.Replace(urlExisting, urlDesiredBuilder.ToString());
86 |
87 | // Append shortcuts
88 | UpdateShortcuts(infoPlistXmlFile);
89 |
90 | // Update app URL in Settings.swift
91 | var settingsUrlExisting = "{{PWABuilder.iOS.url}}";
92 | var settingsUrlDesired = options.Url.ToString().TrimEnd('/');
93 | settingsFile.Replace(settingsUrlExisting, settingsUrlDesired);
94 |
95 | // Update allowed origin in Settings.swift
96 | var allowedOriginExisting = "{{PWABuilder.iOS.urlHost}}";
97 | var allowedOriginDesired = options.Url.Host; // Should be Host here, not ToIOSHostString, as ToIOSHostString() can include query strings and URI paths.
98 | settingsFile.Replace(allowedOriginExisting, allowedOriginDesired);
99 |
100 | // Update authOrigins in Settings.swift
101 | var authOriginsExisting = "\"{{PWABuilder.iOS.permittedHosts}}\"";
102 | var authOriginsPermittedUrls = options.PermittedUrls
103 | .Select(url => url.ToIOSHostString())
104 | .Select(url => $"\"{url}\"");
105 | var authOriginsDesired = string.Join(',', authOriginsPermittedUrls);
106 | settingsFile.Replace(authOriginsExisting, authOriginsDesired);
107 |
108 | // Update app URL in Entitlements.plist. This lets the PWA app handle links to the domain.
109 | // Note: value here must be the host only. Apple says, "Make sure to only include the desired subdomain and the top-level domain. Don’t include path and query components or a trailing slash (/)."
110 | // See https://developer.apple.com/documentation/xcode/supporting-associated-domains
111 | var entitlementsAppUrlExisting = "{{PWABuilder.iOS.universalLinksHost}}";
112 | var entitlementsAppUrlDesired = $"applinks:{GetXmlSafeNodeValue(options.Url.Host)}";
113 | entitlementsXmlFile.Replace(entitlementsAppUrlExisting, entitlementsAppUrlDesired);
114 |
115 | // Update webcredentials URL in Entitlements.plist. This lets the PWA app share credentials with the domain.
116 | // See https://developer.apple.com/documentation/xcode/supporting-associated-domains
117 | var entitlementsWebcredentialsUrlExisting = "{{PWABuilder.iOS.sharedCredentialsHost}}";
118 | var entitlementsWebcredentialsUrlDesired = $"webcredentials:{GetXmlSafeNodeValue(options.Url.Host)}";
119 | entitlementsXmlFile.Replace(entitlementsWebcredentialsUrlExisting, entitlementsWebcredentialsUrlDesired);
120 | }
121 |
122 | private void UpdateShortcuts(XcodeFile infoPlistXmlFile)
123 | {
124 | var shortcutsTemplate = "{{PWABuilder.iOS.shortcuts}}";
125 | var shortcuts = (this.options.Manifest.Shortcuts ?? new List(0))
126 | .Select(s => IOSAppShortcut.FromWebManifestShortcut(s, options.ManifestUri))
127 | .Where(s => s != null)
128 | .Select(s => s!) // because of the above null check
129 | .Take(4) // iOS allows a max of 4 shortcuts
130 | .ToList();
131 | if (shortcuts.Count == 0)
132 | {
133 | // No web app manifest shortcuts? Remove the shortcuts key from Info.plist.
134 | infoPlistXmlFile.Replace(shortcutsTemplate, string.Empty);
135 | }
136 | else
137 | {
138 | var shortcutsBuilder = new StringBuilder();
139 | shortcutsBuilder.Append("UIApplicationShortcutItems\n");
140 | shortcutsBuilder.Append("\n");
141 | shortcutsBuilder.Append(string.Join('\n', shortcuts.Select(s => s.ToInfoPlistEntry(GetXmlSafeNodeValue))));
142 | shortcutsBuilder.Append("");
143 |
144 | infoPlistXmlFile.Replace(shortcutsTemplate, shortcutsBuilder.ToString());
145 | }
146 | }
147 |
148 | private void UpdateAppBundleId()
149 | {
150 | var projFile = GetFile("project.pbxproj");
151 | var existingBundleText = "{{PWABuilder.iOS.bundleId}}";
152 | var desiredBundleText = options.BundleId;
153 | projFile.Replace(existingBundleText, desiredBundleText);
154 | }
155 |
156 | private void RenameProjectFolders()
157 | {
158 | // Rename the pwa-shell directory.
159 | var pwaShell = GetFolder("pwa-shell");
160 | pwaShell.Rename(macSafeProjectName);
161 |
162 | // Rename the pwa-shell.xcworkspace directory.
163 | var workspace = GetFolder("pwa-shell.xcworkspace"); // looks like a file, but actually is a directory
164 | workspace.Rename($"{macSafeProjectName}.xcworkspace");
165 |
166 | // Rename the pwa-shell.xcodeproj directory.
167 | var projDir = GetFolder("pwa-shell.xcodeproj"); // Likewise looks like a file, but is a directory
168 | projDir.Rename($"{macSafeProjectName}.xcodeproj");
169 |
170 | // Rename pwa-shell.xcscheme.
171 | var schemeFile = GetFile("pwa-shell.xcscheme"); // This one's a file.
172 | schemeFile.Rename($"{macSafeProjectName}.xcscheme");
173 | }
174 |
175 | private void UpdateProjectFolderReferences()
176 | {
177 | var oldDirName = "pwa-shell";
178 |
179 | GetFile("Podfile").Replace(oldDirName, macSafeProjectName);
180 | GetFileByPath(Path.Combine("project.xcworkspace", "contents.xcworkspacedata")).Replace(oldDirName, macSafeProjectName);
181 | GetFileByPath(Path.Combine("pwa-shell.xcworkspace", "contents.xcworkspacedata")).Replace(oldDirName, macSafeProjectName);
182 | GetFile("pwa-shell.xcscheme").Replace(oldDirName, macSafeProjectName);
183 |
184 | // project.pbxproj has some references to the old directory name.
185 | // It also has reference to "Pods_pwa_shell.framework", which is kinda the directory name.
186 | var pbxProj = GetFile("project.pbxproj");
187 | pbxProj.Replace(oldDirName, macSafeProjectName);
188 | pbxProj.Replace("Pods_pwa_shell", $"Pods_{swiftModuleName}"); // We use the swift module name here because running 'pod install' on names with spaces throws errors. So, use the more stringent module name instead.
189 | }
190 |
191 | private void UpdateModuleReferences()
192 | {
193 | // Some of the files have reference to PWAShell swift module.
194 | // Rename these.
195 | var oldModuleName = "PWAShell";
196 |
197 | GetFile("PushNotifications.swift").Replace(oldModuleName, swiftModuleName);
198 | GetFile("ViewController.swift").Replace(oldModuleName, swiftModuleName);
199 | GetFile("Main.storyboard").Replace(oldModuleName, swiftModuleName);
200 | GetFile("project.pbxproj").Replace(oldModuleName, swiftModuleName);
201 | GetFile("pwa-shell.xcscheme").Replace(oldModuleName, swiftModuleName);
202 | GetFile("SceneDelegate.swift").Replace(oldModuleName, swiftModuleName);
203 | //GetFile("AppDelegate.swift").Replace(oldModuleName, swiftModuleName);
204 | }
205 |
206 | // TODO: When we want to enable push notifications, revisit this.
207 | //private void UpdatePushSubscription()
208 | //{
209 | //
210 | // \pwa-shell\PushNotifications.swift
211 | // \pwa-shell\AppDelegate.swift
212 | // \pwa-shell\WebView.swift
213 | // \pwa-shell\Settings.swift has gcmMessageIDKey
214 | //}
215 |
216 | private static string GetMacSafeFileName(string desiredFileOrFolderName)
217 | {
218 | var validChars = desiredFileOrFolderName
219 | .Replace(':', '_') // Mac doesn't allow colons
220 | .Replace('/', '_') // doesn't allow forward slash
221 | .TrimStart('.') // can't begin with a period
222 | .Trim(); // shouldn't have space at beginning or end
223 | return validChars.Length switch
224 | {
225 | <= 255 => validChars,
226 | _ => validChars.Substring(0, 255) // must be 255 or less
227 | };
228 | }
229 |
230 | private static string GetSwiftSafeModuleName(string name)
231 | {
232 | var nameBuilder = new System.Text.StringBuilder(name.Length);
233 | foreach (var c in name)
234 | {
235 | // Remove whitespace
236 | if (char.IsWhiteSpace(c))
237 | {
238 | continue;
239 | }
240 |
241 | // Append letters or digits
242 | if (char.IsLetterOrDigit(c))
243 | {
244 | nameBuilder.Append(c);
245 | }
246 | else
247 | {
248 | // Otherwise, append underscore
249 | nameBuilder.Append('_');
250 | }
251 | }
252 |
253 | // It must not begin with a number.
254 | if (char.IsNumber(nameBuilder[0]))
255 | {
256 | nameBuilder.Insert(0, '_');
257 | }
258 |
259 | return nameBuilder.ToString();
260 | }
261 |
262 | private static string GetXmlSafeNodeValue(string input)
263 | {
264 | if (string.IsNullOrWhiteSpace(input))
265 | {
266 | return input ?? string.Empty;
267 | }
268 |
269 | // Probably a better way to do this. Maybe new System.Xml.Linq.XText(input).ToString()?
270 | return new System.Xml.Linq.XElement("D", input)
271 | .ToString()
272 | .Replace("", string.Empty)
273 | .Replace("", string.Empty);
274 | }
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Hosting;
2 | using Microsoft.Extensions.Configuration;
3 | using Microsoft.Extensions.Hosting;
4 | using Microsoft.Extensions.Logging;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Threading.Tasks;
9 |
10 | namespace Microsoft.PWABuilder.IOS.Web
11 | {
12 | public class Program
13 | {
14 | public static void Main(string[] args)
15 | {
16 | CreateHostBuilder(args).Build().Run();
17 | }
18 |
19 | public static IHostBuilder CreateHostBuilder(string[] args) =>
20 | Host.CreateDefaultBuilder(args)
21 | .ConfigureWebHostDefaults(webBuilder =>
22 | {
23 | webBuilder.UseStartup();
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Properties/ServiceDependencies/pwabuilder-ios - Web Deploy/profile.arm.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
3 | "contentVersion": "1.0.0.0",
4 | "metadata": {
5 | "_dependencyType": "appService.windows"
6 | },
7 | "parameters": {
8 | "resourceGroupName": {
9 | "type": "string",
10 | "defaultValue": "pwabuilder-ios",
11 | "metadata": {
12 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
13 | }
14 | },
15 | "resourceGroupLocation": {
16 | "type": "string",
17 | "defaultValue": "westus",
18 | "metadata": {
19 | "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
20 | }
21 | },
22 | "resourceName": {
23 | "type": "string",
24 | "defaultValue": "pwabuilder-ios",
25 | "metadata": {
26 | "description": "Name of the main resource to be created by this template."
27 | }
28 | },
29 | "resourceLocation": {
30 | "type": "string",
31 | "defaultValue": "[parameters('resourceGroupLocation')]",
32 | "metadata": {
33 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
34 | }
35 | }
36 | },
37 | "variables": {
38 | "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
39 | "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]"
40 | },
41 | "resources": [
42 | {
43 | "type": "Microsoft.Resources/resourceGroups",
44 | "name": "[parameters('resourceGroupName')]",
45 | "location": "[parameters('resourceGroupLocation')]",
46 | "apiVersion": "2019-10-01"
47 | },
48 | {
49 | "type": "Microsoft.Resources/deployments",
50 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
51 | "resourceGroup": "[parameters('resourceGroupName')]",
52 | "apiVersion": "2019-10-01",
53 | "dependsOn": [
54 | "[parameters('resourceGroupName')]"
55 | ],
56 | "properties": {
57 | "mode": "Incremental",
58 | "template": {
59 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
60 | "contentVersion": "1.0.0.0",
61 | "resources": [
62 | {
63 | "location": "[parameters('resourceLocation')]",
64 | "name": "[parameters('resourceName')]",
65 | "type": "Microsoft.Web/sites",
66 | "apiVersion": "2015-08-01",
67 | "tags": {
68 | "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
69 | },
70 | "dependsOn": [
71 | "[variables('appServicePlan_ResourceId')]"
72 | ],
73 | "kind": "app",
74 | "properties": {
75 | "name": "[parameters('resourceName')]",
76 | "kind": "app",
77 | "httpsOnly": true,
78 | "reserved": false,
79 | "serverFarmId": "[variables('appServicePlan_ResourceId')]",
80 | "siteConfig": {
81 | "metadata": [
82 | {
83 | "name": "CURRENT_STACK",
84 | "value": "dotnetcore"
85 | }
86 | ]
87 | }
88 | },
89 | "identity": {
90 | "type": "SystemAssigned"
91 | }
92 | },
93 | {
94 | "location": "[parameters('resourceLocation')]",
95 | "name": "[variables('appServicePlan_name')]",
96 | "type": "Microsoft.Web/serverFarms",
97 | "apiVersion": "2015-08-01",
98 | "sku": {
99 | "name": "S1",
100 | "tier": "Standard",
101 | "family": "S",
102 | "size": "S1"
103 | },
104 | "properties": {
105 | "name": "[variables('appServicePlan_name')]"
106 | }
107 | }
108 | ]
109 | }
110 | }
111 | }
112 | ]
113 | }
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:10214",
8 | "sslPort": 44314
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "environmentVariables": {
16 | "ASPNETCORE_ENVIRONMENT": "Development"
17 | }
18 | },
19 | "Microsoft.PWABuilder.IOS.Web": {
20 | "commandName": "Project",
21 | "dotnetRunMessages": "true",
22 | "launchBrowser": true,
23 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
24 | "environmentVariables": {
25 | "ASPNETCORE_ENVIRONMENT": "Development"
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/.DS_Store
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 | /Podfile.lock
92 | /Pods
93 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | platform :ios, '15.0'
3 |
4 | target 'pwa-shell' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 |
8 | # Add the pod for Firebase Cloud Messaging
9 | pod 'Firebase/Messaging'
10 |
11 | end
12 |
13 | post_install do |installer|
14 | installer.pods_project.targets.each do |target|
15 | target.build_configurations.each do |config|
16 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
17 | end
18 | end
19 | end
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/launch-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/launch-128.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/launch-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/launch-192.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/launch-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/launch-64.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell.xcodeproj/xcshareddata/xcschemes/pwa-shell.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import FirebaseCore
3 | import FirebaseMessaging
4 |
5 |
6 | @UIApplicationMain
7 | class AppDelegate: UIResponder, UIApplicationDelegate {
8 |
9 | var window : UIWindow?
10 |
11 | func application(_ application: UIApplication,
12 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
13 |
14 | // TODO: if we're using Firebase, uncomment next string
15 | //FirebaseApp.configure()
16 |
17 | // [START set_messaging_delegate]
18 | Messaging.messaging().delegate = self
19 | // [END set_messaging_delegate]
20 | // Register for remote notifications. This shows a permission dialog on first run, to
21 | // show the dialog at a more appropriate time move this registration accordingly.
22 | // [START register_for_notifications]
23 |
24 | UNUserNotificationCenter.current().delegate = self
25 |
26 | // let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
27 | // UNUserNotificationCenter.current().requestAuthorization(
28 | // options: authOptions,
29 | // completionHandler: {_, _ in })
30 |
31 | // TODO: if we're using Firebase, uncomment next string
32 | // application.registerForRemoteNotifications()
33 |
34 | // [END register_for_notifications]
35 | return true
36 | }
37 |
38 | // [START receive_message]
39 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
40 | // If you are receiving a notification message while your app is in the background,
41 | // this callback will not be fired till the user taps on the notification launching the application.
42 | // With swizzling disabled you must let Messaging know about the message, for Analytics
43 | // Messaging.messaging().appDidReceiveMessage(userInfo)
44 | // Print message ID.
45 | if let messageID = userInfo[gcmMessageIDKey] {
46 | print("Message ID 1: \(messageID)")
47 | }
48 |
49 | // Print full message.
50 | print("push userInfo 1:", userInfo)
51 | sendPushToWebView(userInfo: userInfo)
52 | }
53 |
54 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
55 | fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
56 | // If you are receiving a notification message while your app is in the background,
57 | // this callback will not be fired till the user taps on the notification launching the application.
58 | // With swizzling disabled you must let Messaging know about the message, for Analytics
59 | // Messaging.messaging().appDidReceiveMessage(userInfo)
60 | // Print message ID.
61 | if let messageID = userInfo[gcmMessageIDKey] {
62 | print("Message ID 2: \(messageID)")
63 | }
64 |
65 | // Print full message. **
66 | print("push userInfo 2:", userInfo)
67 | sendPushToWebView(userInfo: userInfo)
68 |
69 | completionHandler(UIBackgroundFetchResult.newData)
70 | }
71 |
72 | // [END receive_message]
73 | func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
74 | print("Unable to register for remote notifications: \(error.localizedDescription)")
75 | }
76 |
77 | // This function is added here only for debugging purposes, and can be removed if swizzling is enabled.
78 | // If swizzling is disabled then this function must be implemented so that the APNs token can be paired to
79 | // the FCM registration token.
80 | // func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
81 | // print("APNs token retrieved: \(deviceToken)")
82 | //
83 | // // With swizzling disabled you must set the APNs token here.
84 | // // Messaging.messaging().apnsToken = deviceToken
85 | // }
86 | }
87 |
88 | // [START ios_10_message_handling]
89 | extension AppDelegate : UNUserNotificationCenterDelegate {
90 |
91 | func userNotificationCenter(_ center: UNUserNotificationCenter,
92 | willPresent notification: UNNotification,
93 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
94 | let userInfo = notification.request.content.userInfo
95 |
96 | // With swizzling disabled you must let Messaging know about the message, for Analytics
97 | // Messaging.messaging().appDidReceiveMessage(userInfo)
98 | // Print message ID.
99 | if let messageID = userInfo[gcmMessageIDKey] {
100 | print("Message ID: 3 \(messageID)")
101 | }
102 |
103 | // Print full message.
104 | print("push userInfo 3:", userInfo)
105 | sendPushToWebView(userInfo: userInfo)
106 |
107 | // Change this to your preferred presentation option
108 | completionHandler([[.banner, .list, .sound]])
109 | }
110 |
111 | func userNotificationCenter(_ center: UNUserNotificationCenter,
112 | didReceive response: UNNotificationResponse,
113 | withCompletionHandler completionHandler: @escaping () -> Void) {
114 | let userInfo = response.notification.request.content.userInfo
115 | // Print message ID.
116 | if let messageID = userInfo[gcmMessageIDKey] {
117 | print("Message ID 4: \(messageID)")
118 | }
119 |
120 | // With swizzling disabled you must let Messaging know about the message, for Analytics
121 | // Messaging.messaging().appDidReceiveMessage(userInfo)
122 | // Print full message.
123 | print("push userInfo 4:", userInfo)
124 | sendPushClickToWebView(userInfo: userInfo)
125 |
126 | completionHandler()
127 | }
128 | }
129 | // [END ios_10_message_handling]
130 |
131 | extension AppDelegate : MessagingDelegate {
132 | // [START refresh_token]
133 | func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
134 | print("Firebase registration token: \(String(describing: fcmToken))")
135 |
136 | let dataDict:[String: String] = ["token": fcmToken ?? ""]
137 | NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict)
138 | handleFCMToken()
139 | // TODO: If necessary send token to application server.
140 | // Note: This callback is fired at each app startup and whenever a new token is generated.
141 | }
142 | // [END refresh_token]
143 | }
144 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "29.png",
17 | "idiom" : "iphone",
18 | "scale" : "1x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "58.png",
23 | "idiom" : "iphone",
24 | "scale" : "2x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "87.png",
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "29x29"
32 | },
33 | {
34 | "filename" : "80.png",
35 | "idiom" : "iphone",
36 | "scale" : "2x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "120.png",
41 | "idiom" : "iphone",
42 | "scale" : "3x",
43 | "size" : "40x40"
44 | },
45 | {
46 | "filename" : "57.png",
47 | "idiom" : "iphone",
48 | "scale" : "1x",
49 | "size" : "57x57"
50 | },
51 | {
52 | "filename" : "114.png",
53 | "idiom" : "iphone",
54 | "scale" : "2x",
55 | "size" : "57x57"
56 | },
57 | {
58 | "filename" : "120.png",
59 | "idiom" : "iphone",
60 | "scale" : "2x",
61 | "size" : "60x60"
62 | },
63 | {
64 | "filename" : "180.png",
65 | "idiom" : "iphone",
66 | "scale" : "3x",
67 | "size" : "60x60"
68 | },
69 | {
70 | "filename" : "20.png",
71 | "idiom" : "ipad",
72 | "scale" : "1x",
73 | "size" : "20x20"
74 | },
75 | {
76 | "filename" : "40.png",
77 | "idiom" : "ipad",
78 | "scale" : "2x",
79 | "size" : "20x20"
80 | },
81 | {
82 | "filename" : "29.png",
83 | "idiom" : "ipad",
84 | "scale" : "1x",
85 | "size" : "29x29"
86 | },
87 | {
88 | "filename" : "58.png",
89 | "idiom" : "ipad",
90 | "scale" : "2x",
91 | "size" : "29x29"
92 | },
93 | {
94 | "filename" : "40.png",
95 | "idiom" : "ipad",
96 | "scale" : "1x",
97 | "size" : "40x40"
98 | },
99 | {
100 | "filename" : "80.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "40x40"
104 | },
105 | {
106 | "filename" : "50.png",
107 | "idiom" : "ipad",
108 | "scale" : "1x",
109 | "size" : "50x50"
110 | },
111 | {
112 | "filename" : "100.png",
113 | "idiom" : "ipad",
114 | "scale" : "2x",
115 | "size" : "50x50"
116 | },
117 | {
118 | "filename" : "72.png",
119 | "idiom" : "ipad",
120 | "scale" : "1x",
121 | "size" : "72x72"
122 | },
123 | {
124 | "filename" : "144.png",
125 | "idiom" : "ipad",
126 | "scale" : "2x",
127 | "size" : "72x72"
128 | },
129 | {
130 | "filename" : "76.png",
131 | "idiom" : "ipad",
132 | "scale" : "1x",
133 | "size" : "76x76"
134 | },
135 | {
136 | "filename" : "152.png",
137 | "idiom" : "ipad",
138 | "scale" : "2x",
139 | "size" : "76x76"
140 | },
141 | {
142 | "filename" : "167.png",
143 | "idiom" : "ipad",
144 | "scale" : "2x",
145 | "size" : "83.5x83.5"
146 | },
147 | {
148 | "filename" : "1024.png",
149 | "idiom" : "ios-marketing",
150 | "scale" : "1x",
151 | "size" : "1024x1024"
152 | },
153 | {
154 | "filename" : "AppIcon-16.png",
155 | "idiom" : "mac",
156 | "scale" : "1x",
157 | "size" : "16x16"
158 | },
159 | {
160 | "filename" : "AppIcon-16@2x.png",
161 | "idiom" : "mac",
162 | "scale" : "2x",
163 | "size" : "16x16"
164 | },
165 | {
166 | "filename" : "AppIcon-32.png",
167 | "idiom" : "mac",
168 | "scale" : "1x",
169 | "size" : "32x32"
170 | },
171 | {
172 | "filename" : "AppIcon-32@2x.png",
173 | "idiom" : "mac",
174 | "scale" : "2x",
175 | "size" : "32x32"
176 | },
177 | {
178 | "filename" : "AppIcon-128.png",
179 | "idiom" : "mac",
180 | "scale" : "1x",
181 | "size" : "128x128"
182 | },
183 | {
184 | "filename" : "AppIcon-128@2x.png",
185 | "idiom" : "mac",
186 | "scale" : "2x",
187 | "size" : "128x128"
188 | },
189 | {
190 | "filename" : "AppIcon-256.png",
191 | "idiom" : "mac",
192 | "scale" : "1x",
193 | "size" : "256x256"
194 | },
195 | {
196 | "filename" : "AppIcon-256@2x.png",
197 | "idiom" : "mac",
198 | "scale" : "2x",
199 | "size" : "256x256"
200 | },
201 | {
202 | "filename" : "AppIcon-512.png",
203 | "idiom" : "mac",
204 | "scale" : "1x",
205 | "size" : "512x512"
206 | },
207 | {
208 | "filename" : "AppIcon-512@2x.png",
209 | "idiom" : "mac",
210 | "scale" : "2x",
211 | "size" : "512x512"
212 | },
213 | {
214 | "filename" : "192.png",
215 | "idiom" : "mac",
216 | "scale" : "1x",
217 | "size" : "192x192"
218 | }
219 | ],
220 | "info" : {
221 | "author" : "xcode",
222 | "version" : 1
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "properties" : {
7 | "compression-type" : "gpu-optimized-smallest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "compression-type" : "automatic",
5 | "filename" : "launch-64.png",
6 | "idiom" : "universal",
7 | "scale" : "1x"
8 | },
9 | {
10 | "compression-type" : "automatic",
11 | "filename" : "launch-128.png",
12 | "idiom" : "universal",
13 | "scale" : "2x"
14 | },
15 | {
16 | "compression-type" : "automatic",
17 | "filename" : "launch-192.png",
18 | "idiom" : "universal",
19 | "scale" : "3x"
20 | },
21 | {
22 | "filename" : "launch-256.png",
23 | "idiom" : "universal",
24 | "scale" : "4x"
25 | },
26 | {
27 | "filename" : "launch-512.png",
28 | "idiom" : "universal",
29 | "scale" : "8x"
30 | }
31 | ],
32 | "info" : {
33 | "author" : "xcode",
34 | "version" : 1
35 | },
36 | "properties" : {
37 | "compression-type" : "automatic"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-128.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-192.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-256.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-512.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pwa-builder/pwabuilder-ios-app-store/e0d3c9008cd1332d099dd665a543ffb4d9744161/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Assets.xcassets/LaunchIcon.imageset/launch-64.png
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {{PWABuilder.iOS.progressBarColor}}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Entitlements/.gitignore:
--------------------------------------------------------------------------------
1 | /GoogleService-Info.plist
2 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Entitlements/Entitlements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | production
7 | com.apple.developer.associated-domains
8 |
9 | {{PWABuilder.iOS.universalLinksHost}}
10 | {{PWABuilder.iOS.sharedCredentialsHost}}
11 |
12 | com.apple.security.app-sandbox
13 |
14 | com.apple.security.device.audio-input
15 |
16 | com.apple.security.device.camera
17 |
18 | com.apple.security.files.user-selected.read-write
19 |
20 | com.apple.security.network.client
21 |
22 | com.apple.security.personal-information.location
23 |
24 | com.apple.security.print
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/GoogleService-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CLIENT_ID
6 | 000000000000-000000000000000000000000000000.apps.googleusercontent.com
7 | REVERSED_CLIENT_ID
8 | com.googleusercontent.apps.0000000000-00000000000000000000000
9 | API_KEY
10 | 0000000000000000000000000
11 | GCM_SENDER_ID
12 | 000000000000
13 | PLIST_VERSION
14 | 1
15 | BUNDLE_ID
16 | com.microsoft.pwabuilder-ios
17 | PROJECT_ID
18 | pwabuilder-ios-template
19 | STORAGE_BUCKET
20 | pwabuilder-ios-template.appspot.com
21 | IS_ADS_ENABLED
22 |
23 | IS_ANALYTICS_ENABLED
24 |
25 | IS_APPINVITE_ENABLED
26 |
27 | IS_GCM_ENABLED
28 |
29 | IS_SIGNIN_ENABLED
30 |
31 | GOOGLE_APP_ID
32 | 1:619930292029:ios:f6737372189b8ee9123f54
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSMicrophoneUsageDescription
6 | Capture Audio by user request
7 | NSLocationWhenInUseUsageDescription
8 | Track current location by user request
9 | NSCameraUsageDescription
10 | Capture Video by user request
11 | CFBundleDevelopmentRegion
12 | $(DEVELOPMENT_LANGUAGE)
13 | CFBundleDisplayName
14 | {{PWABuilder.iOS.appName}}
15 | CFBundleExecutable
16 | $(EXECUTABLE_NAME)
17 | CFBundleIdentifier
18 | $(PRODUCT_BUNDLE_IDENTIFIER)
19 | CFBundleInfoDictionaryVersion
20 | 6.0
21 | CFBundleName
22 | $(PRODUCT_NAME)
23 | CFBundlePackageType
24 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
25 | CFBundleShortVersionString
26 | $(MARKETING_VERSION)
27 | CFBundleURLTypes
28 |
29 |
30 | CFBundleTypeRole
31 | Editor
32 | CFBundleURLName
33 |
34 | CFBundleURLSchemes
35 |
36 |
37 |
38 | CFBundleVersion
39 | $(CURRENT_PROJECT_VERSION)
40 | LSRequiresIPhoneOS
41 |
42 | LSApplicationCategoryType
43 | public.app-category.productivity
44 | NSAppTransportSecurity
45 |
46 | NSAllowsArbitraryLoads
47 |
48 |
49 | UIApplicationSceneManifest
50 |
51 | UIApplicationSupportsMultipleScenes
52 |
53 | UISceneConfigurations
54 |
55 | UIWindowSceneSessionRoleApplication
56 |
57 |
58 | UISceneConfigurationName
59 | Default Configuration
60 | UISceneDelegateClassName
61 | $(PRODUCT_MODULE_NAME).SceneDelegate
62 | UISceneStoryboardFile
63 | Main
64 |
65 |
66 |
67 |
68 | UIBackgroundModes
69 |
70 | processing
71 | remote-notification
72 |
73 | UILaunchStoryboardName
74 | LaunchScreen
75 | UIMainStoryboardFile
76 | Main
77 | UIRequiredDeviceCapabilities
78 |
79 | armv7
80 |
81 | UIStatusBarStyle
82 | UIStatusBarStyleDefault
83 | UISupportedInterfaceOrientations
84 |
85 | UIInterfaceOrientationPortrait
86 | UIInterfaceOrientationLandscapeLeft
87 | UIInterfaceOrientationLandscapeRight
88 | UIInterfaceOrientationPortraitUpsideDown
89 |
90 | UISupportedInterfaceOrientations~ipad
91 |
92 | UIInterfaceOrientationPortrait
93 | UIInterfaceOrientationPortraitUpsideDown
94 | UIInterfaceOrientationLandscapeLeft
95 | UIInterfaceOrientationLandscapeRight
96 |
97 | UIViewControllerBasedStatusBarAppearance
98 |
99 | WKAppBoundDomains
100 |
101 | {{PWABuilder.iOS.permittedUrls}}
102 |
103 | ITSAppUsesNonExemptEncryption
104 |
105 | BGTaskSchedulerPermittedIdentifiers
106 |
107 | $(PRODUCT_BUNDLE_IDENTIFIER)
108 |
109 | {{PWABuilder.iOS.shortcuts}}
110 |
111 |
112 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Printer.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import WebKit
3 |
4 | func printView(webView: WKWebView){
5 | let printController = UIPrintInteractionController.shared
6 |
7 | let printInfo = UIPrintInfo(dictionary:nil)
8 | printInfo.outputType = UIPrintInfo.OutputType.general
9 | printInfo.jobName = (webView.url?.absoluteString)!
10 | printInfo.duplex = UIPrintInfo.Duplex.none
11 | printInfo.orientation = UIPrintInfo.Orientation.portrait
12 |
13 | printController.printPageRenderer = UIPrintPageRenderer()
14 |
15 | printController.printPageRenderer?.addPrintFormatter(webView.viewPrintFormatter(), startingAtPageAt: 0)
16 |
17 | printController.printInfo = printInfo
18 | printController.showsNumberOfCopies = true
19 | printController.present(animated: true)
20 | }
21 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/PushNotifications.swift:
--------------------------------------------------------------------------------
1 | import WebKit
2 | import FirebaseMessaging
3 |
4 | class SubscribeMessage {
5 | var topic = ""
6 | var eventValue = ""
7 | var unsubscribe = false
8 | struct Keys {
9 | static var TOPIC = "topic"
10 | static var UNSUBSCRIBE = "unsubscribe"
11 | static var EVENTVALUE = "eventValue"
12 | }
13 | convenience init(dict: Dictionary) {
14 | self.init()
15 | if let topic = dict[Keys.TOPIC] as? String {
16 | self.topic = topic
17 | }
18 | if let unsubscribe = dict[Keys.UNSUBSCRIBE] as? Bool {
19 | self.unsubscribe = unsubscribe
20 | }
21 | if let eventValue = dict[Keys.EVENTVALUE] as? String {
22 | self.eventValue = eventValue
23 | }
24 | }
25 | }
26 |
27 | func handleSubscribeTouch(message: WKScriptMessage) {
28 | // [START subscribe_topic]
29 | let subscribeMessages = parseSubscribeMessage(message: message)
30 | if (subscribeMessages.count > 0){
31 | let _message = subscribeMessages[0]
32 | if (_message.unsubscribe) {
33 | Messaging.messaging().unsubscribe(fromTopic: _message.topic) { error in }
34 | }
35 | else {
36 | Messaging.messaging().subscribe(toTopic: _message.topic) { error in }
37 | }
38 | }
39 |
40 |
41 | // [END subscribe_topic]
42 | }
43 |
44 | func parseSubscribeMessage(message: WKScriptMessage) -> [SubscribeMessage] {
45 | var subscribeMessages = [SubscribeMessage]()
46 | if let objStr = message.body as? String {
47 |
48 | let data: Data = objStr.data(using: .utf8)!
49 | do {
50 | let jsObj = try JSONSerialization.jsonObject(with: data, options: .init(rawValue: 0))
51 | if let jsonObjDict = jsObj as? Dictionary {
52 | let subscribeMessage = SubscribeMessage(dict: jsonObjDict)
53 | subscribeMessages.append(subscribeMessage)
54 | } else if let jsonArr = jsObj as? [Dictionary] {
55 | for jsonObj in jsonArr {
56 | let sMessage = SubscribeMessage(dict: jsonObj)
57 | subscribeMessages.append(sMessage)
58 | }
59 | }
60 | } catch _ {
61 |
62 | }
63 | }
64 | return subscribeMessages
65 | }
66 |
67 | func returnPermissionResult(isGranted: Bool){
68 | DispatchQueue.main.async(execute: {
69 | if (isGranted){
70 | PWAShell.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('push-permission-request', { detail: 'granted' }))")
71 | }
72 | else {
73 | PWAShell.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('push-permission-request', { detail: 'denied' }))")
74 | }
75 | })
76 | }
77 | func returnPermissionState(state: String){
78 | DispatchQueue.main.async(execute: {
79 | PWAShell.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('push-permission-state', { detail: '\(state)' }))")
80 | })
81 | }
82 |
83 | func handlePushPermission() {
84 | UNUserNotificationCenter.current().getNotificationSettings () { settings in
85 | switch settings.authorizationStatus {
86 | case .notDetermined:
87 | let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
88 | UNUserNotificationCenter.current().requestAuthorization(
89 | options: authOptions,
90 | completionHandler: { (success, error) in
91 | if error == nil {
92 | if success == true {
93 | returnPermissionResult(isGranted: true)
94 | DispatchQueue.main.async {
95 | UIApplication.shared.registerForRemoteNotifications()
96 | }
97 | }
98 | else {
99 | returnPermissionResult(isGranted: false)
100 | }
101 | }
102 | else {
103 | returnPermissionResult(isGranted: false)
104 | }
105 | }
106 | )
107 | case .denied:
108 | returnPermissionResult(isGranted: false)
109 | case .authorized, .ephemeral, .provisional:
110 | returnPermissionResult(isGranted: true)
111 | @unknown default:
112 | return;
113 | }
114 | }
115 | }
116 | func handlePushState() {
117 | UNUserNotificationCenter.current().getNotificationSettings () { settings in
118 | switch settings.authorizationStatus {
119 | case .notDetermined:
120 | returnPermissionState(state: "notDetermined")
121 | case .denied:
122 | returnPermissionState(state: "denied")
123 | case .authorized:
124 | returnPermissionState(state: "authorized")
125 | case .ephemeral:
126 | returnPermissionState(state: "ephemeral")
127 | case .provisional:
128 | returnPermissionState(state: "provisional")
129 | @unknown default:
130 | returnPermissionState(state: "unknown")
131 | return;
132 | }
133 | }
134 | }
135 |
136 | func checkViewAndEvaluate(event: String, detail: String) {
137 | if (!PWAShell.webView.isHidden && !PWAShell.webView.isLoading ) {
138 | DispatchQueue.main.async(execute: {
139 | PWAShell.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('\(event)', { detail: \(detail) }))")
140 | })
141 | }
142 | else {
143 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
144 | checkViewAndEvaluate(event: event, detail: detail)
145 | }
146 | }
147 | }
148 |
149 | func handleFCMToken(){
150 | DispatchQueue.main.async(execute: {
151 | Messaging.messaging().token { token, error in
152 | if let error = error {
153 | print("Error fetching FCM registration token: \(error)")
154 | checkViewAndEvaluate(event: "push-token", detail: "ERROR GET TOKEN")
155 | } else if let token = token {
156 | print("FCM registration token: \(token)")
157 | checkViewAndEvaluate(event: "push-token", detail: "'\(token)'")
158 | }
159 | }
160 | })
161 | }
162 |
163 | func sendPushToWebView(userInfo: [AnyHashable: Any]){
164 | var json = "";
165 | do {
166 | let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
167 | json = String(data: jsonData, encoding: .utf8)!
168 | } catch {
169 | print("ERROR: userInfo parsing problem")
170 | return
171 | }
172 | checkViewAndEvaluate(event: "push-notification", detail: json)
173 | }
174 |
175 | func sendPushClickToWebView(userInfo: [AnyHashable: Any]){
176 | var json = "";
177 | do {
178 | let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
179 | json = String(data: jsonData, encoding: .utf8)!
180 | } catch {
181 | print("ERROR: userInfo parsing problem")
182 | return
183 | }
184 | checkViewAndEvaluate(event: "push-notification-click", detail: json)
185 | }
186 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @available(iOS 13.0, *)
4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
5 |
6 | var window: UIWindow?
7 |
8 | // If our app is launched with a universal link, we'll store it in this variable
9 | static var universalLinkToLaunch: URL? = nil;
10 | static var shortcutLinkToLaunch: URL? = nil
11 |
12 |
13 | // This function is called when your app launches.
14 | // Check to see if we were launched via a universal link or a shortcut.
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // See if our app is being launched via universal link.
17 | // If so, store that link so we can navigate to it once our webView is initialized.
18 | for userActivity in connectionOptions.userActivities {
19 | if let universalLink = userActivity.webpageURL {
20 | SceneDelegate.universalLinkToLaunch = universalLink;
21 | break
22 | }
23 | }
24 |
25 | // See if we were launched via shortcut
26 | if let shortcutUrl = connectionOptions.shortcutItem?.type {
27 | SceneDelegate.shortcutLinkToLaunch = URL.init(string: shortcutUrl)
28 | }
29 |
30 | // See if we were launched via scheme URL
31 | if let schemeUrl = connectionOptions.urlContexts.first?.url {
32 | // Convert scheme://url to a https://url
33 | var comps = URLComponents(url: schemeUrl, resolvingAgainstBaseURL: false)
34 | comps?.scheme = "https"
35 |
36 | if let url = comps?.url {
37 | SceneDelegate.universalLinkToLaunch = url;
38 | }
39 | }
40 | }
41 |
42 | // This function is called when our app is already running and the user clicks a custom scheme URL
43 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) {
44 | if let scheme = URLContexts.first?.url {
45 | // Convert scheme://url to a https://url and navigate to it
46 | var comps = URLComponents(url: scheme, resolvingAgainstBaseURL: false)
47 | comps?.scheme = "https"
48 |
49 | if let url = comps?.url {
50 | // Handle it inside our web view in a SPA-friendly way.
51 | PWAShell.webView.evaluateJavaScript("location.href = '\(url)'")
52 | }
53 | }
54 | }
55 |
56 | // This function is called when our app is already running and the user clicks a universal link.
57 | func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
58 | // Handle universal links into our app when the app is already running.
59 | // This allows your PWA to open links to your domain, rather than opening in a browser tab.
60 | // For more info about universal links, see https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app
61 |
62 | // Ensure we're trying to launch a link.
63 | guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
64 | let universalLink = userActivity.webpageURL else {
65 | return
66 | }
67 |
68 | // Handle it inside our web view in a SPA-friendly way.
69 | PWAShell.webView.evaluateJavaScript("location.href = '\(universalLink)'")
70 | }
71 |
72 | // This function is called if our app is already loaded and the user activates the app via shortcut
73 | func windowScene(_ windowScene: UIWindowScene,
74 | performActionFor shortcutItem: UIApplicationShortcutItem,
75 | completionHandler: @escaping (Bool) -> Void) {
76 | if let shortcutUrl = URL.init(string: shortcutItem.type) {
77 | PWAShell.webView.evaluateJavaScript("location.href = '\(shortcutUrl)'");
78 | }
79 | }
80 |
81 | func sceneDidDisconnect(_ scene: UIScene) {
82 | // Called as the scene is being released by the system.
83 | // This occurs shortly after the scene enters the background, or when its session is discarded.
84 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
85 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
86 | }
87 |
88 | func sceneDidBecomeActive(_ scene: UIScene) {
89 | // Called when the scene has moved from an inactive state to an active state.
90 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
91 | }
92 |
93 | func sceneWillResignActive(_ scene: UIScene) {
94 | // Called when the scene will move from an active state to an inactive state.
95 | // This may occur due to temporary interruptions (ex. an incoming phone call).
96 | }
97 |
98 | func sceneWillEnterForeground(_ scene: UIScene) {
99 | // Called as the scene transitions from the background to the foreground.
100 | // Use this method to undo the changes made on entering the background.
101 | }
102 |
103 | func sceneDidEnterBackground(_ scene: UIScene) {
104 | // Called as the scene transitions from the foreground to the background.
105 | // Use this method to save data, release shared resources, and store enough scene-specific state information
106 | // to restore the scene back to its current state.
107 | }
108 |
109 |
110 | }
111 |
112 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/Settings.swift:
--------------------------------------------------------------------------------
1 | import WebKit
2 |
3 | struct Cookie {
4 | var name: String
5 | var value: String
6 | }
7 |
8 | let gcmMessageIDKey = "00000000000" // update this with actual ID if using Firebase
9 |
10 | // URL for first launch
11 | let rootUrl = URL(string: "{{PWABuilder.iOS.url}}")!
12 |
13 | // allowed origin is for what we are sticking to pwa domain
14 | // This should also appear in Info.plist
15 | let allowedOrigins: [String] = ["{{PWABuilder.iOS.urlHost}}"]
16 |
17 | // auth origins will open in modal and show toolbar for back into the main origin.
18 | // These should also appear in Info.plist
19 | let authOrigins: [String] = ["{{PWABuilder.iOS.permittedHosts}}"]
20 | // allowedOrigins + authOrigins <= 10
21 |
22 | let platformCookie = Cookie(name: "app-platform", value: "iOS App Store")
23 |
24 | // UI options
25 | let displayMode = "standalone" // standalone / fullscreen.
26 | let adaptiveUIStyle = true // iOS 15+ only. Change app theme on the fly to dark/light related to WebView background color.
27 | let overrideStatusBar = false // iOS 13-14 only. if you don't support dark/light system theme.
28 | let statusBarTheme = "dark" // dark / light, related to override option.
29 | let pullToRefresh = true // Enable/disable pull down to refresh page
30 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/ios-project-src/pwa-shell/ViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import WebKit
3 |
4 | var webView: WKWebView! = nil
5 |
6 | class ViewController: UIViewController, WKNavigationDelegate, UIDocumentInteractionControllerDelegate {
7 |
8 | var documentController: UIDocumentInteractionController?
9 | func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
10 | return self
11 | }
12 |
13 | @IBOutlet weak var loadingView: UIView!
14 | @IBOutlet weak var progressView: UIProgressView!
15 | @IBOutlet weak var connectionProblemView: UIImageView!
16 | @IBOutlet weak var webviewView: UIView!
17 | var toolbarView: UIToolbar!
18 |
19 | var htmlIsLoaded = false;
20 |
21 | private var themeObservation: NSKeyValueObservation?
22 | var currentWebViewTheme: UIUserInterfaceStyle = .unspecified
23 | override var preferredStatusBarStyle : UIStatusBarStyle {
24 | if #available(iOS 13, *), overrideStatusBar{
25 | if #available(iOS 15, *) {
26 | return .default
27 | } else {
28 | return statusBarTheme == "dark" ? .lightContent : .darkContent
29 | }
30 | }
31 | return .default
32 | }
33 |
34 | override func viewDidLoad() {
35 | super.viewDidLoad()
36 | initWebView()
37 | initToolbarView()
38 | loadRootUrl()
39 |
40 | NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification , object: nil)
41 |
42 | }
43 |
44 | override func viewDidLayoutSubviews() {
45 | super.viewDidLayoutSubviews()
46 | PWAShell.webView.frame = calcWebviewFrame(webviewView: webviewView, toolbarView: nil)
47 | }
48 |
49 | @objc func keyboardWillHide(_ notification: NSNotification) {
50 | PWAShell.webView.setNeedsLayout()
51 | }
52 |
53 | func initWebView() {
54 | PWAShell.webView = createWebView(container: webviewView, WKSMH: self, WKND: self, NSO: self, VC: self)
55 | webviewView.addSubview(PWAShell.webView);
56 |
57 | PWAShell.webView.uiDelegate = self;
58 |
59 | PWAShell.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
60 |
61 | if(pullToRefresh){
62 | let refreshControl = UIRefreshControl()
63 | refreshControl.addTarget(self, action: #selector(refreshWebView(_:)), for: UIControl.Event.valueChanged)
64 | PWAShell.webView.scrollView.addSubview(refreshControl)
65 | PWAShell.webView.scrollView.bounces = true
66 | }
67 |
68 | if #available(iOS 15.0, *), adaptiveUIStyle {
69 | themeObservation = PWAShell.webView.observe(\.themeColor) { [unowned self] webView, _ in
70 | let backgroundColor = PWAShell.webView.underPageBackgroundColor;
71 | let themeColor = PWAShell.webView.themeColor;
72 | currentWebViewTheme = themeColor?.isLight() ?? backgroundColor?.isLight() ?? true ? .light : .dark
73 | self.overrideUIStyle()
74 | view.backgroundColor = themeColor ?? backgroundColor;
75 | }
76 | }
77 | }
78 |
79 | @objc func refreshWebView(_ sender: UIRefreshControl) {
80 | PWAShell.webView?.reload()
81 | sender.endRefreshing()
82 | }
83 |
84 | func createToolbarView() -> UIToolbar{
85 | let winScene = UIApplication.shared.connectedScenes.first
86 | let windowScene = winScene as! UIWindowScene
87 | var statusBarHeight = windowScene.statusBarManager?.statusBarFrame.height ?? 60
88 |
89 | #if targetEnvironment(macCatalyst)
90 | if (statusBarHeight == 0){
91 | statusBarHeight = 30
92 | }
93 | #endif
94 |
95 | let toolbarView = UIToolbar(frame: CGRect(x: 0, y: 0, width: webviewView.frame.width, height: 0))
96 | toolbarView.sizeToFit()
97 | toolbarView.frame = CGRect(x: 0, y: 0, width: webviewView.frame.width, height: toolbarView.frame.height + statusBarHeight)
98 | // toolbarView.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin, .flexibleWidth]
99 |
100 | let flex = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
101 | let close = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(loadRootUrl))
102 | toolbarView.setItems([close,flex], animated: true)
103 |
104 | toolbarView.isHidden = true
105 |
106 | return toolbarView
107 | }
108 |
109 | func overrideUIStyle(toDefault: Bool = false) {
110 | if #available(iOS 15.0, *), adaptiveUIStyle {
111 | if (((htmlIsLoaded && !PWAShell.webView.isHidden) || toDefault) && self.currentWebViewTheme != .unspecified) {
112 | UIApplication
113 | .shared
114 | .connectedScenes
115 | .flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
116 | .first { $0.isKeyWindow }?.overrideUserInterfaceStyle = toDefault ? .unspecified : self.currentWebViewTheme;
117 | }
118 | }
119 | }
120 |
121 | func initToolbarView() {
122 | toolbarView = createToolbarView()
123 |
124 | webviewView.addSubview(toolbarView)
125 | }
126 |
127 | @objc func loadRootUrl() {
128 | PWAShell.webView.load(URLRequest(url: SceneDelegate.universalLinkToLaunch ?? SceneDelegate.shortcutLinkToLaunch ?? rootUrl))
129 | }
130 |
131 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){
132 | htmlIsLoaded = true
133 |
134 | self.setProgress(1.0, true)
135 | self.animateConnectionProblem(false)
136 |
137 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
138 | PWAShell.webView.isHidden = false
139 | self.loadingView.isHidden = true
140 |
141 | self.setProgress(0.0, false)
142 |
143 | self.overrideUIStyle()
144 | }
145 | }
146 |
147 | func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
148 | htmlIsLoaded = false;
149 |
150 | if (error as NSError)._code != (-999) {
151 | self.overrideUIStyle(toDefault: true);
152 |
153 | webView.isHidden = true;
154 | loadingView.isHidden = false;
155 | animateConnectionProblem(true);
156 |
157 | setProgress(0.05, true);
158 |
159 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
160 | self.setProgress(0.1, true);
161 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
162 | self.loadRootUrl();
163 | }
164 | }
165 | }
166 | }
167 |
168 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
169 |
170 | if (keyPath == #keyPath(WKWebView.estimatedProgress) &&
171 | PWAShell.webView.isLoading &&
172 | !self.loadingView.isHidden &&
173 | !self.htmlIsLoaded) {
174 | var progress = Float(PWAShell.webView.estimatedProgress);
175 |
176 | if (progress >= 0.8) { progress = 1.0; };
177 | if (progress >= 0.3) { self.animateConnectionProblem(false); }
178 |
179 | self.setProgress(progress, true);
180 | }
181 | }
182 |
183 | func setProgress(_ progress: Float, _ animated: Bool) {
184 | self.progressView.setProgress(progress, animated: animated);
185 | }
186 |
187 |
188 | func animateConnectionProblem(_ show: Bool) {
189 | if (show) {
190 | self.connectionProblemView.isHidden = false;
191 | self.connectionProblemView.alpha = 0
192 | UIView.animate(withDuration: 0.7, delay: 0, options: [.repeat, .autoreverse], animations: {
193 | self.connectionProblemView.alpha = 1
194 | })
195 | }
196 | else {
197 | UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: {
198 | self.connectionProblemView.alpha = 0 // Here you will get the animation you want
199 | }, completion: { _ in
200 | self.connectionProblemView.isHidden = true;
201 | self.connectionProblemView.layer.removeAllAnimations();
202 | })
203 | }
204 | }
205 |
206 | deinit {
207 | PWAShell.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
208 | }
209 | }
210 |
211 | extension UIColor {
212 | // Check if the color is light or dark, as defined by the injected lightness threshold.
213 | // Some people report that 0.7 is best. I suggest to find out for yourself.
214 | // A nil value is returned if the lightness couldn't be determined.
215 | func isLight(threshold: Float = 0.5) -> Bool? {
216 | let originalCGColor = self.cgColor
217 |
218 | // Now we need to convert it to the RGB colorspace. UIColor.white / UIColor.black are greyscale and not RGB.
219 | // If you don't do this then you will crash when accessing components index 2 below when evaluating greyscale colors.
220 | let RGBCGColor = originalCGColor.converted(to: CGColorSpaceCreateDeviceRGB(), intent: .defaultIntent, options: nil)
221 | guard let components = RGBCGColor?.components else {
222 | return nil
223 | }
224 | guard components.count >= 3 else {
225 | return nil
226 | }
227 |
228 | let brightness = Float(((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000)
229 | return (brightness > threshold)
230 | }
231 | }
232 |
233 | extension ViewController: WKScriptMessageHandler {
234 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
235 | if message.name == "print" {
236 | printView(webView: PWAShell.webView)
237 | }
238 | if message.name == "push-subscribe" {
239 | handleSubscribeTouch(message: message)
240 | }
241 | if message.name == "push-permission-request" {
242 | handlePushPermission()
243 | }
244 | if message.name == "push-permission-state" {
245 | handlePushState()
246 | }
247 | if message.name == "push-token" {
248 | handleFCMToken()
249 | }
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Resources/next-steps.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Services/AnalyticsService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.ApplicationInsights;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.Extensions.Options;
4 | using Microsoft.PWABuilder.IOS.Web.Models;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Net.Http;
9 | using System.Threading.Tasks;
10 |
11 | namespace Microsoft.PWABuilder.IOS.Web.Services
12 | {
13 | ///
14 | /// Reports iOS package generation to the PWABuilder analytics backend service.
15 | ///
16 | public class AnalyticsService
17 | {
18 | private readonly IOptions settings;
19 | private readonly ILogger logger;
20 | private readonly HttpClient http;
21 | private readonly TelemetryClient telemetryClient;
22 | private readonly bool isAppInsightsEnabled;
23 |
24 | public AnalyticsService(
25 | IOptions settings,
26 | IHttpClientFactory httpClientFactory,
27 | ILogger logger,
28 | TelemetryClient telemetryClient)
29 | {
30 | this.settings = settings;
31 | this.http = httpClientFactory.CreateClient();
32 | this.logger = logger;
33 | this.telemetryClient = telemetryClient;
34 | this.isAppInsightsEnabled = !string.IsNullOrEmpty(this.settings.Value.ApplicationInsightsConnectionString);
35 | }
36 |
37 | public void Record(string url, bool success, IOSAppPackageOptions.Validated? packageOptions, AnalyticsInfo? analyticsInfo, string? error)
38 | {
39 | //Code to remove starts here (in the future when we don't need RavenDB)
40 | if (!string.IsNullOrEmpty(this.settings.Value.AnalyticsUrl))
41 | {
42 | LogToRavenDB(url, success, error);
43 | }
44 | else
45 | {
46 | this.logger.LogWarning("Skipping analytics event recording in RavenDB due to no analytics URL in app settings. For development, this should be expected.");
47 | }
48 | //Code to remove ends here
49 |
50 | if (!this.isAppInsightsEnabled)
51 | {
52 | this.logger.LogWarning("Skipping analytics event recording in App insights due to no connection string. For development, this should be expected.");
53 | return;
54 | }
55 |
56 | this.telemetryClient.Context.Operation.Id = analyticsInfo?.correlationId != null ? analyticsInfo.correlationId : System.Guid.NewGuid().ToString();
57 |
58 | Dictionary record;
59 | var name = "";
60 | if (success && packageOptions != null)
61 | {
62 | record = new() { { "URL", url.ToString() }, { "IOSBundleID", packageOptions.BundleId ?? "" }, { "IOSAppName", packageOptions.Name ?? ""} };
63 | name = "IOSPackageEvent";
64 | }
65 | else
66 | {
67 | record = new() { { "URL", url.ToString() }, { "IOSPackageError", error ?? "" } };
68 | name = "IOSPackageFailureEvent";
69 | }
70 | if (analyticsInfo?.platformId != null)
71 | {
72 | record.Add("PlatformId", analyticsInfo.platformId);
73 | if (analyticsInfo?.platformIdVersion != null)
74 | {
75 | record.Add("PlatformVersion", analyticsInfo.platformIdVersion);
76 | }
77 | }
78 | if(analyticsInfo?.referrer != null)
79 | {
80 | record.Add("referrer", analyticsInfo.referrer);
81 | }
82 | telemetryClient.TrackEvent(name, record);
83 | ;
84 | }
85 |
86 | private void LogToRavenDB(string url, bool success, string? error)
87 | {
88 | var args = System.Text.Json.JsonSerializer.Serialize(new
89 | {
90 | Url = url,
91 | IOSPackage = success,
92 | IOSPackageError = error
93 | });
94 | this.http.PostAsync(this.settings.Value.AnalyticsUrl, new StringContent(args))
95 | .ContinueWith(_ => logger.LogInformation("Successfully sent {url} to URL logging service. Success = {success}, Error = {error}", url, success, error), TaskContinuationOptions.OnlyOnRanToCompletion)
96 | .ContinueWith(task => logger.LogError(task.Exception ?? new Exception("Unable to send URL to logging service"), "Unable to send {url} to logging service due to an error", url), TaskContinuationOptions.OnlyOnFaulted);
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Services/IOSPackageCreator.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using Microsoft.Extensions.Options;
3 | using Microsoft.PWABuilder.IOS.Web.Models;
4 | using Microsoft.PWABuilder.IOS.Web.Common;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.IO;
8 | using System.IO.Compression;
9 | using System.Linq;
10 | using System.Threading.Tasks;
11 |
12 | namespace Microsoft.PWABuilder.IOS.Web.Services
13 | {
14 | public class IOSPackageCreator
15 | {
16 | private readonly ImageGenerator imageGenerator;
17 | private readonly TempDirectory temp;
18 | private readonly AppSettings appSettings;
19 | private readonly ILogger logger;
20 |
21 | public IOSPackageCreator(
22 | ImageGenerator imageGenerator,
23 | IOptions appSettings,
24 | TempDirectory temp,
25 | ILogger logger)
26 | {
27 | this.imageGenerator = imageGenerator;
28 | this.appSettings = appSettings.Value;
29 | this.temp = temp;
30 | this.logger = logger;
31 | }
32 |
33 | ///
34 | /// Generates an iOS package.
35 | ///
36 | /// The package creation options.
37 | /// The path to a zip file.
38 | public async Task Create(IOSAppPackageOptions.Validated options)
39 | {
40 | try
41 | {
42 | var outputDir = temp.CreateDirectory($"ios-package-{Guid.NewGuid()}");
43 |
44 | // Make a copy of the iOS source code.
45 | new DirectoryInfo(appSettings.IOSSourceCodePath).CopyContents(new DirectoryInfo(outputDir));
46 |
47 | // Create any missing images for the iOS template.
48 | // This should be done before project.ApplyChanges(). Otherwise, it'll attempt to write the images to the "pwa-shell" directory, which no longer exists after ApplyChanges().
49 | await this.imageGenerator.Generate(options, WebAppManifestContext.From(options.Manifest, options.ManifestUri), outputDir);
50 |
51 | // Update the source files with the real values from the requested PWA
52 | var project = new XcodePwaShellProject(options, outputDir);
53 | project.Load();
54 | await project.ApplyChanges();
55 |
56 | // Zip it all up.
57 | var zipFile = CreateZip(outputDir);
58 | return await File.ReadAllBytesAsync(zipFile);
59 | }
60 | catch (Exception error)
61 | {
62 | logger.LogError(error, "Error generating iOS package");
63 | throw;
64 | }
65 | finally
66 | {
67 | temp.CleanUp();
68 | }
69 | }
70 |
71 | private string CreateZip(string outputDir)
72 | {
73 | var zipFilePath = temp.CreateFile();
74 | using var zipFile = File.Create(zipFilePath);
75 | using var zipArchive = new ZipArchive(zipFile, ZipArchiveMode.Create);
76 | zipArchive.CreateEntryFromFile(appSettings.NextStepsPath, "next-steps.html");
77 | zipArchive.CreateEntryFromDirectory(outputDir, "src");
78 | return zipFilePath;
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Services/TempDirectory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using Microsoft.Extensions.Options;
3 | using Microsoft.PWABuilder.IOS.Web.Models;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Threading.Tasks;
9 |
10 | namespace Microsoft.PWABuilder.IOS.Web.Services
11 | {
12 | ///
13 | /// Creates and tracks temporary files and directories and deletes them when CleanUp() is called.
14 | ///
15 | public class TempDirectory : IDisposable
16 | {
17 | private readonly List directoriesToCleanUp = new();
18 | private readonly List filesToCleanUp = new();
19 | private readonly ILogger logger;
20 |
21 | public TempDirectory(ILogger logger)
22 | {
23 | this.logger = logger;
24 | }
25 |
26 | public string CreateDirectory(string dirName)
27 | {
28 | var outputFolder = Path.Combine(Path.GetTempPath(), dirName);
29 | Directory.CreateDirectory(outputFolder);
30 | directoriesToCleanUp.Add(outputFolder);
31 | return outputFolder;
32 | }
33 |
34 | public string CreateFile()
35 | {
36 | var tempFileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".tmp");
37 | this.filesToCleanUp.Add(tempFileName);
38 | return tempFileName;
39 | }
40 |
41 | public void CleanUp()
42 | {
43 | foreach (var file in this.filesToCleanUp)
44 | {
45 | if (!string.IsNullOrWhiteSpace(file))
46 | {
47 | try
48 | {
49 | File.Delete(file);
50 | }
51 | catch (Exception fileDeleteError)
52 | {
53 | logger.LogWarning(fileDeleteError, "Unable to cleanup {zipFile}", file);
54 | }
55 | }
56 | }
57 |
58 | foreach (var directory in this.directoriesToCleanUp)
59 | {
60 | if (!string.IsNullOrWhiteSpace(directory))
61 | {
62 | try
63 | {
64 | Directory.Delete(directory, recursive: true);
65 | }
66 | catch (Exception directoryDeleteError)
67 | {
68 | logger.LogWarning(directoryDeleteError, "Unable to cleanup {directory}", directory);
69 | }
70 | }
71 | }
72 | }
73 |
74 | public void Dispose()
75 | {
76 | this.CleanUp();
77 | GC.SuppressFinalize(this);
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.ApplicationInsights.AspNetCore.Extensions;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.AspNetCore.HttpsPolicy;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Hosting;
9 | using Microsoft.Extensions.Logging;
10 | using Microsoft.OpenApi.Models;
11 | using Microsoft.PWABuilder.IOS.Web.Models;
12 | using Microsoft.PWABuilder.IOS.Web.Services;
13 | using System;
14 | using System.Collections.Generic;
15 | using System.Linq;
16 | using System.Threading.Tasks;
17 |
18 | namespace Microsoft.PWABuilder.IOS.Web
19 | {
20 | public class Startup
21 | {
22 | private readonly string AllowedOriginsPolicyName = "allowedOrigins";
23 |
24 | public Startup(IConfiguration configuration)
25 | {
26 | Configuration = configuration;
27 | }
28 |
29 | public IConfiguration Configuration { get; }
30 |
31 | // This method gets called by the runtime. Use this method to add services to the container.
32 | public void ConfigureServices(IServiceCollection services)
33 | {
34 | var appSettings = Configuration.GetSection("AppSettings");
35 | var aiOptions = setUpAppInsights(appSettings);
36 | services.Configure(Configuration.GetSection("AppSettings"));
37 | services.AddCors(options =>
38 | {
39 | options.AddPolicy(name: AllowedOriginsPolicyName, builder => builder
40 | .SetIsOriginAllowed(CheckAllowedOriginCors)
41 | .AllowAnyHeader()
42 | .AllowAnyMethod());
43 | });
44 |
45 | services.AddTransient();
46 | services.AddTransient();
47 | services.AddTransient();
48 | services.AddTransient();
49 | services.AddHttpClient();
50 | services.AddControllers();
51 | services.AddSwaggerGen(c =>
52 | {
53 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "Microsoft.PWABuilder.IOS.Web", Version = "v1" });
54 | });
55 | services.AddApplicationInsightsTelemetry(aiOptions);
56 | }
57 |
58 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
59 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
60 | {
61 | if (env.IsDevelopment())
62 | {
63 | app.UseSwagger();
64 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Microsoft.PWABuilder.IOS.Web v1"));
65 | }
66 |
67 | app.UseDeveloperExceptionPage();
68 | app.UseHttpsRedirection();
69 | app.UseRouting();
70 | app.UseStaticFiles();
71 | app.UseAuthorization();
72 |
73 | app.UseEndpoints(endpoints =>
74 | {
75 | endpoints.MapControllers();
76 | });
77 | }
78 |
79 | private bool CheckAllowedOriginCors(string origin)
80 | {
81 | var allowedOrigins = new[]
82 | {
83 | "https://www.pwabuilder.com",
84 | "https://pwabuilder.com",
85 | "https://preview.pwabuilder.com",
86 | "https://localhost:3333",
87 | "https://localhost:3000",
88 | "http://localhost:3333",
89 | "http://localhost:3000",
90 | "https://localhost:8000",
91 | "http://localhost:8000",
92 | "https://nice-field-047c1420f.azurestaticapps.net"
93 | };
94 | return allowedOrigins.Any(o => origin.Contains(o, StringComparison.OrdinalIgnoreCase));
95 | }
96 |
97 | static ApplicationInsightsServiceOptions setUpAppInsights(IConfigurationSection appSettings)
98 | {
99 | var connectionString = appSettings["ApplicationInsightsConnectionString"];
100 | var aiOptions = new ApplicationInsightsServiceOptions();
101 | aiOptions.EnableRequestTrackingTelemetryModule = false;
102 | aiOptions.EnableDependencyTrackingTelemetryModule = true;
103 | aiOptions.EnableHeartbeat = false;
104 | aiOptions.EnableAzureInstanceMetadataTelemetryModule = false;
105 | aiOptions.EnableActiveTelemetryConfigurationSetup = false;
106 | aiOptions.EnableAdaptiveSampling = false;
107 | aiOptions.EnableAppServicesHeartbeatTelemetryModule = false;
108 | aiOptions.EnableAuthenticationTrackingJavaScript = false;
109 | aiOptions.ConnectionString = connectionString;
110 | return aiOptions;
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AppSettings": {
10 | "IOSSourceCodePath": "bin/Debug/net7.0/Resources/ios-project-src",
11 | "NextStepsPath": "bin/Debug/net7.0/Resources/next-steps.html",
12 | "ImageGeneratorApiUrl": "https://appimagegenerator-pre.azurewebsites.net/api/image",
13 | "ApplicationInsightsConnectionString": ""
14 | }
15 | }
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/appsettings.Production.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppSettings": {
3 | "IOSSourceCodePath": "Resources/ios-project-src",
4 | "NextStepsPath": "Resources/next-steps.html",
5 | "ImageGeneratorApiUrl": "https://appimagegenerator-prod.azurewebsites.net/api/image",
6 | "ApplicationInsightsConnectionString": ""
7 | }
8 | }
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*",
10 | "AppSettings": {
11 | "IOSSourceCodePath": "",
12 | "NextStepsPath": "",
13 | "ImageGeneratorApiUrl": "",
14 | "AnalyticsUrl": "",
15 | "ApplicationInsightsConnectionString": ""
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | PWABuilder.iOS
12 |
13 |
14 |
15 | PWABuilder.iOS
16 |
17 | Test PWABuilder's iOS app package generation service using the JSON below
18 |
19 |
20 |
34 |
35 |
43 |
44 | dotnet 7
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Microsoft.PWABuilder.IOS.Web/wwwroot/index.js:
--------------------------------------------------------------------------------
1 | const codeArea = document.querySelector("textarea");
2 | const submitBtn = document.querySelector("#submitBtn");
3 | const resultsDiv = document.querySelector("#results");
4 | const spinner = document.querySelector(".spinner-border");
5 |
6 | submitBtn.addEventListener("click", () => submit());
7 |
8 | setCode(getDefaultJson());
9 | codeArea.scrollTop = 0;
10 |
11 | function setCode(options) {
12 | const code = JSON.stringify(options, undefined, 4);
13 | codeArea.value = code;
14 | codeArea.scrollTop = 1000000;
15 | }
16 |
17 | function getDefaultJson() {
18 | // This creates an unsigned package. Should be considered the bare minimum.
19 | return {
20 | name: "Sad Chonks",
21 | bundleId: "com.sadchonks",
22 | url: "https://sadchonks.com",
23 | imageUrl: "https://sadchonks.com/kitteh-512.png",
24 | splashColor: "#f5f5f5",
25 | progressBarColor: "#3f51b5",
26 | statusBarColor: "#f5f5f5",
27 | permittedUrls: [],
28 | manifestUrl: "https://sadchonks.com/manifest.json",
29 | manifest: getManifest()
30 | };
31 | }
32 |
33 | function getManifest() {
34 | return {
35 | "short_name": "Chonks",
36 | "name": "Sad Chonks",
37 | "description": "Your daily source for Sad Chonks",
38 | "categories": ["cats", "memes"],
39 | "screenshots": [
40 | {
41 | "src": "/chonkscreenshot1.jpeg",
42 | "type": "image/jpeg",
43 | "sizes": "728x409",
44 | "label": "App on homescreen with shortcuts",
45 | "platform": "play"
46 | },
47 | {
48 | "src": "/chonkscreenshot2.jpg",
49 | "type": "image/jpeg",
50 | "sizes": "551x541",
51 | "label": "Really long text describing the screenshot above which is basically a picture showing the app being long pressed on Android and the WebShortcuts popping out",
52 | "platform": "xbox"
53 | }
54 | ],
55 | "icons": [
56 | {
57 | "src": "/favicon.png",
58 | "type": "image/png",
59 | "sizes": "128x128"
60 | },
61 | {
62 | "src": "/kitteh-192.png",
63 | "type": "image/png",
64 | "sizes": "192x192"
65 | },
66 | {
67 | "src": "/kitteh-512.png",
68 | "type": "image/png",
69 | "sizes": "512x512"
70 | }
71 | ],
72 | "start_url": "/saved",
73 | "background_color": "#3f51b5",
74 | "display": "standalone",
75 | "scope": "/",
76 | "theme_color": "#3f51b5",
77 | "shortcuts": [
78 | {
79 | "name": "New Chonks",
80 | "short_name": "New",
81 | "url": "/?shortcut",
82 | "icons": [{ "src": "/favicon.png", "sizes": "128x128" }]
83 | },
84 | {
85 | "name": "Saved Chonks",
86 | "short_name": "Saved",
87 | "url": "/saved?shortcut",
88 | "icons": [{ "src": "/favicon.png", "sizes": "128x128" }]
89 | }
90 | ]
91 | }
92 | }
93 |
94 | async function submit() {
95 | resultsDiv.textContent = "";
96 |
97 | setLoading(true);
98 | try {
99 | // Convert the JSON to an object and back to a string to ensure proper formatting.
100 | const options = JSON.stringify(JSON.parse(codeArea.value));
101 | const response = await fetch("/packages/create", {
102 | method: "POST",
103 | body: options,
104 | headers: new Headers({ 'content-type': 'application/json', 'platform-identifier': 'ServerUI', 'platform-identifier-version': '1.0.0' }),
105 | });
106 | if (response.status === 200) {
107 | const data = await response.blob();
108 | const url = window.URL.createObjectURL(data);
109 | window.location.assign(url);
110 |
111 | resultsDiv.textContent = "Success, download started 😎";
112 | } else {
113 | const responseText = await response.text();
114 | resultsDiv.textContent = `Failed. Status code ${response.status}, Error: ${response.statusText}, Details: ${responseText}`;
115 | }
116 | } catch (err) {
117 | resultsDiv.textContent = "Failed. Error: " + err;
118 | }
119 | finally {
120 | setLoading(false);
121 | }
122 | }
123 |
124 | function setLoading(state) {
125 | submitBtn.disabled = state;
126 | if (state) {
127 | spinner.classList.remove("d-none");
128 | } else {
129 | spinner.classList.add("d-none");
130 | }
131 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PWABuilder iOS Platform
2 | Hey folks! Just wanted give the community an update on iOS support on PWABuilder and support within the communication channels. Due to the fact that iOS has very limited support for PWAs we will be supporting the iOS in a community driven fashion. This means that there will not be devs from the PWABuilder team maintaining and building out iOS functionality on the site or Discord. Responses will be 100% community driven. If you are interested in being an iOS champion DM me, would be happy to chat 🙂
3 |
4 | This is PWABuilder's iOS platform that generates an iOS app that loads your PWA in a WKWebView. The platform generates a zip file containing an Xcode project that you can compile on your Mac and publish to the App Store.
5 |
6 | # Documentation
7 | If you're looking for more info on how to use PWABuilder to package for iOS, check out the documentation [here.](https://docs.pwabuilder.com/#/builder/app-store)
8 |
9 | There is also an [iOS FAQ](https://docs.pwabuilder.com/#/builder/faq?id=ios) available.
10 |
11 | # Architecture
12 |
13 | This is a C# web app that listens for requests to generate a PWA.
14 |
15 | When a request comes in, it creates a copy of the iOS PWA template code, modifies the template with the desired PWA values and zips up the result.
16 |
17 | The iOS PWA template code is located in [/Microsoft.PWABuilder.IOS.Web/Resources](https://github.com/pwa-builder/pwabuilder-ios/tree/main/Microsoft.PWABuilder.IOS.Web/Resources).
18 |
19 | The code is a fork of https://github.com/khmyznikov/ios-pwa-wrap, licensed under [The Unlicense](https://unlicense.org/). A big thanks to Gleb for permitting PWABuilder to use, fork, and improve on his PWA template.
20 |
21 | # Running Locally
22 |
23 | You will need [Docker](https://www.docker.com/products/docker-desktop/) and the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) to run this service locally.
24 |
25 | Steps:
26 |
27 | 1. Run `az acr login -n pwabuilder` to authenticate with our Azure Container Registry.
28 |
29 | 2. Run `docker-compose up` to start the service.
30 |
31 | 3. Visit `localhost:5000` to see the iOS packaging testing interface.
32 |
33 | Alternately, you can POST to `/packages/create` with the following JSON body:
34 |
35 | ```json
36 | {
37 | "name": "Sad Chonks",
38 | "bundleId": "com.sadchonks",
39 | "url": "https://sadchonks.com",
40 | "imageUrl": "https://sadchonks.com/kitteh-512.png",
41 | "splashColor": "#f5f5f5",
42 | "progressBarColor": "#3f51b5",
43 | "statusBarColor": "#f5f5f5",
44 | "permittedUrls": [],
45 | "manifestUrl": "https://sadchonks.com/manifest.json",
46 | "manifest": {
47 | "short_name": "Chonks",
48 | "name": "Sad Chonks",
49 | "description": "Your daily source for Sad Chonks",
50 | "categories": [ "entertainment" ],
51 | "screenshots": [
52 | {
53 | "src": "/chonkscreenshot1.jpeg",
54 | "type": "image/jpeg",
55 | "sizes": "728x409",
56 | "label": "App on homescreen with shortcuts",
57 | "platform": "play"
58 | },
59 | {
60 | "src": "/chonkscreenshot2.jpg",
61 | "type": "image/jpeg",
62 | "sizes": "551x541",
63 | "label": "Really long text describing the screenshot above which is basically a picture showing the app being long pressed on Android and the WebShortcuts popping out",
64 | "platform": "xbox"
65 | }
66 | ],
67 | "icons": [
68 | {
69 | "src": "/favicon.png",
70 | "type": "image/png",
71 | "sizes": "128x128"
72 | },
73 | {
74 | "src": "/kitteh-192.png",
75 | "type": "image/png",
76 | "sizes": "192x192"
77 | },
78 | {
79 | "src": "/kitteh-512.png",
80 | "type": "image/png",
81 | "sizes": "512x512"
82 | }
83 | ],
84 | "start_url": "/saved",
85 | "background_color": "#3f51b5",
86 | "display": "standalone",
87 | "scope": "/",
88 | "theme_color": "#3f51b5",
89 | "shortcuts": [
90 | {
91 | "name": "New Chonks",
92 | "short_name": "New",
93 | "url": "/?shortcut",
94 | "icons": [
95 | {
96 | "src": "/favicon.png",
97 | "sizes": "128x128"
98 | }
99 | ]
100 | },
101 | {
102 | "name": "Saved Chonks",
103 | "short_name": "Saved",
104 | "url": "/saved?shortcut",
105 | "icons": [
106 | {
107 | "src": "/favicon.png",
108 | "sizes": "128x128"
109 | }
110 | ]
111 | }
112 | ]
113 | }
114 | }
115 | ```
116 |
117 | For more information about the JSON arguments, see [IOSPackageOptions](https://github.com/pwa-builder/pwabuilder-ios/blob/main/Microsoft.PWABuilder.IOS.Web/Models/IOSAppPackageOptions.cs).
118 |
119 | The response will be a zip file containing the generated app solution, which can be compiled in Xcode.
120 |
121 | # Deployment
122 |
123 | Checkins to main branch will trigger automatic deployment to [pwabuilder-ios staging](pwabuilder-ios-staging.azurewebsites.net).
124 |
125 | To deploy to production, swap staging and production slots.
--------------------------------------------------------------------------------
/docker-compose.debug.yml:
--------------------------------------------------------------------------------
1 | # Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP .NET Core service.
2 |
3 | version: '3.4'
4 |
5 | services:
6 | microsoftpwabuilderiosweb:
7 | image: microsoftpwabuilderiosweb
8 | build:
9 | context: .
10 | dockerfile: Microsoft.PWABuilder.IOS.Web/Dockerfile
11 | ports:
12 | - 5000:5000
13 | environment:
14 | - ASPNETCORE_ENVIRONMENT=Development
15 | volumes:
16 | - ~/.vsdbg:/remote_debugger:rw
17 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP .NET Core service.
2 |
3 | version: '3.4'
4 |
5 | services:
6 | microsoftpwabuilderiosweb:
7 | image: microsoftpwabuilderiosweb
8 | build:
9 | context: .
10 | dockerfile: Microsoft.PWABuilder.IOS.Web/Dockerfile
11 | ports:
12 | - 5000:5000
13 |
--------------------------------------------------------------------------------