├── .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 | 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 |
21 |
22 | 24 |
25 | 33 |
34 | 35 |
36 |
37 | Results 38 |
39 |
40 |

41 |         
42 |
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 | --------------------------------------------------------------------------------