├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── PushnotificationsDemo.sln ├── PushnotificationsDemo ├── Controllers │ ├── HomeController.cs │ └── PushController.cs ├── Migrations │ ├── 20180925204906_Initial.Designer.cs │ ├── 20180925204906_Initial.cs │ └── DemoDbContextModelSnapshot.cs ├── Models │ ├── DemoDbContext.cs │ ├── ErrorViewModel.cs │ ├── Notification.cs │ └── PushSubscription.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── PushnotificationsDemo.csproj ├── Services │ ├── IPushService.cs │ └── PushService.cs ├── Startup.cs ├── Views │ ├── Home │ │ └── Index.cshtml │ └── Shared │ │ └── Error.cshtml ├── appsettings.json └── wwwroot │ ├── css │ ├── demo-template.css │ └── push-notifications.css │ ├── images │ ├── astrology-demo_chart-1.svg │ ├── astrology-demo_chart-2.svg │ ├── astrology-demo_chart-3.svg │ ├── astrology-demo_chart-4.svg │ ├── astrology-demo_chart-5.svg │ ├── client-side.svg │ ├── cron-job.svg │ ├── earth-detail.svg │ ├── favicon.png │ ├── header-embellishment.svg │ ├── hero-background-stars.svg │ ├── hero-image.svg │ ├── leo-server-side.svg │ ├── moon-detail.svg │ ├── notifications-permission.svg │ ├── side-signs.svg │ ├── side-star-chart.svg │ ├── social-image.jpg │ ├── star-background.svg │ ├── toast-image.jpg │ ├── top-star-chart.svg │ ├── triangle-down.svg │ ├── triangle-up.svg │ ├── try-it-out.svg │ └── x.svg │ ├── js │ ├── demo-template.js │ ├── push-notifications.js │ ├── script.js │ └── util.js │ └── service-worker.js ├── PushnotificationsDemoFunction ├── .gitignore ├── Models │ ├── DemoDbContext.cs │ ├── Notification.cs │ └── PushSubscription.cs ├── Notify.cs ├── PushnotificationsDemoFunction.csproj ├── Services │ ├── IPushService.cs │ └── PushService.cs ├── host.json └── settings.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | PushnotificationsDemo/appsettings\.Development\.json 263 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/PushnotificationsDemo/bin/Debug/netcoreapp2.1/PushnotificationsDemo.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/PushnotificationsDemo", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "${auto-detect-url}", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start ${auto-detect-url}" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "sourceFileMap": { 36 | "/Views": "${workspaceFolder}/Views" 37 | } 38 | }, 39 | { 40 | "name": ".NET Core Attach", 41 | "type": "coreclr", 42 | "request": "attach", 43 | "processId": "${command:pickProcess}" 44 | } 45 | ,] 46 | } -------------------------------------------------------------------------------- /.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}/PushnotificationsDemo/PushnotificationsDemo.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to this documentation 2 | 3 | Thank you for your interest in our documentation! 4 | 5 | - [Ways to contribute](#ways-to-contribute) 6 | - [Contribute using GitHub](#contribute-using-github) 7 | - [Contribute using Git](#contribute-using-git) 8 | - [How to use Markdown to format your topic](#how-to-use-markdown-to-format-your-topic) 9 | - [FAQ](#faq) 10 | - [More resources](#more-resources) 11 | 12 | ## Ways to contribute 13 | 14 | Here are some ways you can contribute to this documentation: 15 | 16 | - To make small changes to an article, [Contribute using GitHub](#contribute-using-github). 17 | - To make large changes, or changes that involve code, [Contribute using Git](#contribute-using-git). 18 | - Report documentation bugs via GitHub Issues 19 | - Request new documentation at the [Office Developer Platform UserVoice](http://officespdev.uservoice.com) site. 20 | 21 | ## Contribute using GitHub 22 | 23 | Use GitHub to contribute to this documentation without having to clone the repo to your desktop. This is the easiest way to create a pull request in this repository. Use this method to make a minor change that doesn't involve code changes. 24 | 25 | **Note** Using this method allows you to contribute to one article at a time. 26 | 27 | ### To Contribute using GitHub 28 | 29 | 1. Find the article you want to contribute to on GitHub. 30 | 31 | If the article is in MSDN, choose the **suggest and submit changes** link in the **Contribute to this content** section and you'll be taken to the same article on GitHub. 32 | 33 | 2. Once you are on the article in GitHub, sign in to GitHub (get a free account [Join GitHub](https://github.com/join). 34 | 3. Choose the **pencil icon** (edit the file in your fork of this project) and make your changes in the **<>Edit file** window. 35 | 4. Scroll to the bottom and enter a description. 36 | 5. Choose **Propose file change**>**Create pull request**. 37 | 38 | You now have successfully submitted a pull request. Pull requests are typically reviewed within 10 business days. 39 | 40 | ## Contribute using Git 41 | 42 | Use Git to contribute substantive changes, such as: 43 | 44 | - Contributing code. 45 | - Contributing changes that affect meaning. 46 | - Contributing large changes to text. 47 | - Adding new topics. 48 | 49 | ### To Contribute using Git 50 | 51 | 1. If you don't have a GitHub account, set one up at [GitHub](https://github.com/join). 52 | 2. After you have an account, install Git on your computer. Follow the steps in [Setting up Git Tutorial](https://help.github.com/articles/set-up-git/). 53 | 3. To submit a pull request using Git, follow the steps in [Use GitHub, Git, and this repository](#use-github-git-and-this-repository). 54 | 4. You will be asked to sign the Contributor's License Agreement if you are: 55 | 56 | - A member of the Microsoft Open Technologies group. 57 | - A contributors who doesn't work for Microsoft. 58 | 59 | As a community member, you must sign the Contribution License Agreement (CLA) before you can contribute large submissions to a project. You only need to complete and submit the documentation once. Carefully review the document. You may be required to have your employer sign the document. 60 | 61 | Signing the CLA does not grant you rights to commit to the main repository, but it does mean that the Office Developer and Office Developer Content Publishing teams will be able to review and approve your contributions. You will be credited for your submissions. 62 | 63 | Pull requests are typically reviewed within 10 business days. 64 | 65 | ## Use GitHub, Git, and this repository 66 | 67 | **Note:** Most of the information in this section can be found in [GitHub Help] articles. If you're familiar with Git and GitHub, skip to the **Contribute and edit content** section for the specifics of the code/content flow of this repository. 68 | 69 | ### To set up your fork of the repository 70 | 71 | 1. Set up a GitHub account so you can contribute to this project. If you haven't done this, go to [GitHub](https://github.com/join) and do it now. 72 | 2. Install Git on your computer. Follow the steps in the [Setting up Git Tutorial][set up git]. 73 | 3. Create your own fork of this repository. To do this, at the top of the page, choose the **Fork** button. 74 | 4. Copy your fork to your computer. To do this, open Git Bash. At the command prompt enter: 75 | 76 | git clone https://github.com//.git 77 | 78 | Next, create a reference to the root repository by entering these commands: 79 | 80 | cd 81 | git remote add upstream https://github.com/MicrosoftEdge/.git 82 | git fetch upstream 83 | 84 | Congratulations! You've now set up your repository. You won't need to repeat these steps again. 85 | 86 | ### Contribute and edit content 87 | 88 | To make the contribution process as seamless as possible, follow these steps. 89 | 90 | #### To contribute and edit content 91 | 92 | 1. Create a new branch. 93 | 2. Add new content or edit existing content. 94 | 3. Submit a pull request to the main repository. 95 | 4. Delete the branch. 96 | 97 | **Important** Limit each branch to a single concept/article to streamline the work flow and reduce the chance of merge conflicts. Content appropriate for a new branch includes: 98 | 99 | - A new article. 100 | - Spelling and grammar edits. 101 | - Applying a single formatting change across a large set of articles (for example, applying a new copyright footer). 102 | 103 | #### To create a new branch 104 | 105 | 1. Open Git Bash. 106 | 2. At the Git Bash command prompt, type `git pull upstream master:`. This creates a new branch locally that is copied from the latest MicrosoftEdge master branch. 107 | 3. At the Git Bash command prompt, type `git push origin `. This alerts GitHub to the new branch. You should now see the new branch in your fork of the repository on GitHub. 108 | 4. At the Git Bash command prompt, type `git checkout ` to switch to your new branch. 109 | 110 | #### Add new content or edit existing content 111 | 112 | You navigate to the repository on your computer by using File Explorer. The repository files are in `C:\Users\\`. 113 | 114 | To edit files, open them in an editor of your choice and modify them. To create a new file, use the editor of your choice and save the new file in the appropriate location in your local copy of the repository. While working, save your work frequently. 115 | 116 | The files in `C:\Users\\` are a working copy of the new branch that you created in your local repository. Changing anything in this folder doesn't affect the local repository until you commit a change. To commit a change to the local repository, type the following commands in GitBash: 117 | 118 | git add . 119 | git commit -v -a -m "" 120 | 121 | The `add` command adds your changes to a staging area in preparation for committing them to the repository. The period after the `add` command specifies that you want to stage all of the files that you added or modified, checking subfolders recursively. (If you don't want to commit all of the changes, you can add specific files. You can also undo a commit. For help, type `git add -help` or `git status`.) 122 | 123 | The `commit` command applies the staged changes to the repository. The switch `-m` means you are providing the commit comment in the command line. The -v and -a switches can be omitted. The -v switch is for verbose output from the command, and -a does what you already did with the add command. 124 | 125 | You can commit multiple times while you are doing your work, or you can commit once when you're done. 126 | 127 | #### Submit a pull request to the main repository 128 | 129 | When you're finished with your work and are ready to have it merged into the main repository, follow these steps. 130 | 131 | #### To submit a pull request to the main repository 132 | 133 | 1. In the Git Bash command prompt, type `git push origin `. In your local repository, `origin` refers to your GitHub repository that you cloned the local repository from. This command pushes the current state of your new branch, including all commits made in the previous steps, to your GitHub fork. 134 | 2. On the GitHub site, navigate in your fork to the new branch. 135 | 3. Choose the **Pull Request** button at the top of the page. 136 | 4. Verify the Base branch is `MicrosoftEdge/@master` and the Head branch is `/@`. 137 | 5. Choose the **Update Commit Range** button. 138 | 6. Add a title to your pull request, and describe all the changes you're making. 139 | 7. Submit the pull request. 140 | 141 | One of the site administrators will process your pull request. Your pull request will surface on the MicrosoftEdge/ site under Issues. When the pull request is accepted, the issue will be resolved. 142 | 143 | #### Create a new branch after merge 144 | 145 | After a branch is successfully merged (that is, your pull request is accepted), don't continue working in that local branch. This can lead to merge conflicts if you submit another pull request. To do another update, create a new local branch from the successfully merged upstream branch, and then delete your initial local branch. 146 | 147 | For example, if your local branch X was successfully merged into the OfficeDev/microsoft-graph-docs master branch and you want to make additional updates to the content that was merged. Create a new local branch, X2, from the OfficeDev/microsoft-graph-docs master branch. To do this, open GitBash and execute the following commands: 148 | 149 | cd microsoft-graph-docs 150 | git pull upstream master:X2 151 | git push origin X2 152 | 153 | You now have local copies (in a new local branch) of the work that you submitted in branch X. The X2 branch also contains all the work other writers have merged, so if your work depends on others' work (for example, shared images), it is available in the new branch. You can verify that your previous work (and others' work) is in the branch by checking out the new branch... 154 | 155 | git checkout X2 156 | 157 | ...and verifying the content. (The `checkout` command updates the files in `C:\Users\\microsoft-graph-docs` to the current state of the X2 branch.) Once you check out the new branch, you can make updates to the content and commit them as usual. However, to avoid working in the merged branch (X) by mistake, it's best to delete it (see the following **Delete a branch** section). 158 | 159 | #### Delete a branch 160 | 161 | Once your changes are successfully merged into the main repository, delete the branch you used because you no longer need it. Any additional work should be done in a new branch. 162 | 163 | #### To delete a branch 164 | 165 | 1. In the Git Bash command prompt, type `git checkout master`. This ensures that you aren't in the branch to be deleted (which isn't allowed). 166 | 2. Next, at the command prompt, type `git branch -d `. This deletes the branch on your computer only if it has been successfully merged to the upstream repository. (You can override this behavior with the `–D` flag, but first be sure you want to do this.) 167 | 3. Finally, type `git push origin :` at the command prompt (a space before the colon and no space after it). This will delete the branch on your github fork. 168 | 169 | Congratulations, you have successfully contributed to the project! 170 | 171 | ## How to use Markdown to format your topic 172 | 173 | All of the articles in this repository use Markdown. A complete introduction (and listing of all the syntax) can be found at [Markdown Home]. 174 | 175 | ## FAQ 176 | 177 | ### How do I get a GitHub account? 178 | 179 | Fill out the form at [Join GitHub](https://github.com/join) to open a free GitHub account. 180 | 181 | ### Where do I get a Contributor's License Agreement? 182 | 183 | You will automatically be sent a notice that you need to sign the Contributor's License Agreement (CLA) if your pull request requires one. 184 | 185 | As a community member, **you must sign the Contribution License Agreement (CLA) before you can contribute large submissions to this project**. You only need complete and submit the documentation once. Carefully review the document. You may be required to have your employer sign the document. 186 | 187 | ### What happens with my contributions? 188 | 189 | When you submit your changes, via a pull request, our team will be notified and will review your pull request. You will receive notifications about your pull request from GitHub; you may also be notified by someone from our team if we need more information. We reserve the right to edit your submission for legal, style, clarity, or other issues. 190 | 191 | ### Can I become an approver for this repository's GitHub pull requests? 192 | 193 | Currently, we are not allowing external contributors to approve pull requests in this repository. 194 | 195 | ### How soon will I get a response about my change request or issue? 196 | 197 | We typically review pull requests and respond to issues within 10 business days. 198 | 199 | ## More resources 200 | 201 | - To learn more about Markdown, go to the Git creator's site [Daring Fireball]. 202 | - To learn more about using Git and GitHub, first check out the [GitHub Help section][github help]. 203 | 204 | [github home]: http://github.com 205 | [github help]: http://help.github.com/ 206 | [set up git]: http://help.github.com/win-set-up-git/ 207 | [markdown home]: http://daringfireball.net/projects/markdown/ 208 | [daring fireball]: http://daringfireball.net/ 209 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PushnotificationsDemo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PushnotificationsDemo", "PushnotificationsDemo\PushnotificationsDemo.csproj", "{69754450-0E82-4B41-AA83-B63568188DD3}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PushnotificationsDemoFunction", "PushnotificationsDemoFunction\PushnotificationsDemoFunction.csproj", "{2815385C-3CDF-47C9-8608-AFB0E4273739}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {69754450-0E82-4B41-AA83-B63568188DD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {69754450-0E82-4B41-AA83-B63568188DD3}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {69754450-0E82-4B41-AA83-B63568188DD3}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {69754450-0E82-4B41-AA83-B63568188DD3}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {2815385C-3CDF-47C9-8608-AFB0E4273739}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2815385C-3CDF-47C9-8608-AFB0E4273739}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2815385C-3CDF-47C9-8608-AFB0E4273739}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2815385C-3CDF-47C9-8608-AFB0E4273739}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {19707769-8A98-4855-A59E-BB18BCAEA1FB} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using PushnotificationsDemo.Models; 8 | 9 | namespace PushnotificationsDemo.Controllers 10 | { 11 | public class HomeController : Controller 12 | { 13 | public IActionResult Index() 14 | { 15 | return View(); 16 | } 17 | 18 | public IActionResult About() 19 | { 20 | ViewData["Message"] = "Your application description page."; 21 | 22 | return View(); 23 | } 24 | 25 | public IActionResult Contact() 26 | { 27 | ViewData["Message"] = "Your contact page."; 28 | 29 | return View(); 30 | } 31 | 32 | public IActionResult Privacy() 33 | { 34 | return View(); 35 | } 36 | 37 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 38 | public IActionResult Error() 39 | { 40 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Controllers/PushController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc; 3 | using PushnotificationsDemo.Models; 4 | using PushnotificationsDemo.Services; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace PushnotificationsDemo.Controllers 10 | { 11 | /// 12 | /// VAPID Push Notification API 13 | /// 14 | [Produces("application/json")] 15 | [Route("api/[controller]")] 16 | [ApiController] 17 | public class PushController : ControllerBase 18 | { 19 | private readonly IHostingEnvironment _env; 20 | private readonly IPushService _pushService; 21 | 22 | /// 23 | public PushController(IHostingEnvironment hostingEnvironment, IPushService pushService) 24 | { 25 | _env = hostingEnvironment; 26 | _pushService = pushService; 27 | } 28 | 29 | // GET: api/push/vapidpublickey 30 | /// 31 | /// Get VAPID Public Key 32 | /// 33 | /// VAPID Public Key 34 | /// OK 35 | /// Unauthorized 36 | [HttpGet, Route("vapidpublickey")] 37 | public ActionResult GetVapidPublicKey() 38 | { 39 | return Ok(_pushService.GetVapidPublicKey()); 40 | } 41 | 42 | // POST: api/push/subscribe 43 | /// 44 | /// Subscribe for push notifications 45 | /// 46 | /// No content 47 | /// NoContent 48 | /// BadRequest if subscription is null or invalid. 49 | /// Unauthorized 50 | [HttpPost("subscribe")] 51 | public async Task> Subscribe([FromBody] PushSubscriptionViewModel model) 52 | { 53 | var subscription = new PushSubscription 54 | { 55 | UserId = Guid.NewGuid().ToString(), // You'd use your existing user id here 56 | Endpoint = model.Subscription.Endpoint, 57 | ExpirationTime = model.Subscription.ExpirationTime, 58 | Auth = model.Subscription.Keys.Auth, 59 | P256Dh = model.Subscription.Keys.P256Dh 60 | }; 61 | 62 | return await _pushService.Subscribe(subscription); 63 | } 64 | 65 | // POST: api/push/unsubscribe 66 | /// 67 | /// Unsubscribe for push notifications 68 | /// 69 | /// No content 70 | /// NoContent 71 | /// BadRequest if subscription is null or invalid. 72 | /// Unauthorized 73 | [HttpPost("unsubscribe")] 74 | public async Task> Unsubscribe([FromBody] PushSubscriptionViewModel model) 75 | { 76 | var subscription = new PushSubscription 77 | { 78 | Endpoint = model.Subscription.Endpoint, 79 | ExpirationTime = model.Subscription.ExpirationTime, 80 | Auth = model.Subscription.Keys.Auth, 81 | P256Dh = model.Subscription.Keys.P256Dh 82 | }; 83 | 84 | await _pushService.Unsubscribe(subscription); 85 | 86 | return subscription; 87 | } 88 | 89 | // POST: api/push/send 90 | /// 91 | /// Send a push notifications to a specific user's every device (for development only!) 92 | /// 93 | /// No content 94 | /// Accepted 95 | /// BadRequest if subscription is null or invalid. 96 | /// Unauthorized 97 | [HttpPost("send/{userId}")] 98 | public async Task> Send([FromRoute] string userId, [FromBody] Notification notification, [FromQuery] int? delay) 99 | { 100 | if (!_env.IsDevelopment()) return Forbid(); 101 | 102 | if (delay != null) Thread.Sleep((int)delay); 103 | 104 | await _pushService.Send(userId, notification); 105 | 106 | return Accepted(); 107 | } 108 | } 109 | 110 | /// 111 | /// Request body model for Push registration 112 | /// 113 | public class PushSubscriptionViewModel 114 | { 115 | /// 116 | public Subscription Subscription { get; set; } 117 | 118 | /// 119 | /// Other attributes, like device id for example. 120 | /// 121 | public string DeviceId { get; set; } 122 | } 123 | 124 | /// 125 | /// Representation of the Web Standard Push API's PushSubscription 126 | /// 127 | public class Subscription 128 | { 129 | /// 130 | /// The endpoint associated with the push subscription. 131 | /// 132 | public string Endpoint { get; set; } 133 | 134 | /// 135 | /// The subscription expiration time associated with the push subscription, if there is one, or null otherwise. 136 | /// 137 | public double? ExpirationTime { get; set; } 138 | 139 | /// 140 | public Keys Keys { get; set; } 141 | 142 | /// 143 | /// Converts the push subscription to the format of the library WebPush 144 | /// 145 | /// WebPush subscription 146 | public WebPush.PushSubscription ToWebPushSubscription() => new WebPush.PushSubscription(Endpoint, Keys.P256Dh, Keys.Auth); 147 | } 148 | 149 | /// 150 | /// Contains the client's public key and authentication secret to be used in encrypting push message data. 151 | /// 152 | public class Keys 153 | { 154 | /// 155 | /// An Elliptic curve Diffie–Hellman public key on the P-256 curve (that is, the NIST secp256r1 elliptic curve). 156 | /// The resulting key is an uncompressed point in ANSI X9.62 format. 157 | /// 158 | public string P256Dh { get; set; } 159 | 160 | /// 161 | /// An authentication secret, as described in Message Encryption for Web Push. 162 | /// 163 | public string Auth { get; set; } 164 | } 165 | } -------------------------------------------------------------------------------- /PushnotificationsDemo/Migrations/20180925204906_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using PushnotificationsDemo.Models; 9 | 10 | namespace PushnotificationsDemo.Migrations 11 | { 12 | [DbContext(typeof(DemoDbContext))] 13 | [Migration("20180925204906_Initial")] 14 | partial class Initial 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("PushnotificationsDemo.Models.PushSubscription", b => 25 | { 26 | b.Property("P256Dh") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("Auth") 30 | .IsRequired(); 31 | 32 | b.Property("Endpoint") 33 | .IsRequired(); 34 | 35 | b.Property("ExpirationTime"); 36 | 37 | b.Property("UserId") 38 | .IsRequired(); 39 | 40 | b.HasKey("P256Dh"); 41 | 42 | b.ToTable("PushSubscription"); 43 | }); 44 | #pragma warning restore 612, 618 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Migrations/20180925204906_Initial.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace PushnotificationsDemo.Migrations 4 | { 5 | public partial class Initial : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.CreateTable( 10 | name: "PushSubscription", 11 | columns: table => new 12 | { 13 | UserId = table.Column(nullable: false), 14 | Endpoint = table.Column(nullable: false), 15 | ExpirationTime = table.Column(nullable: true), 16 | P256Dh = table.Column(nullable: false), 17 | Auth = table.Column(nullable: false) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_PushSubscription", x => x.P256Dh); 22 | }); 23 | } 24 | 25 | protected override void Down(MigrationBuilder migrationBuilder) 26 | { 27 | migrationBuilder.DropTable( 28 | name: "PushSubscription"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Migrations/DemoDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using PushnotificationsDemo.Models; 8 | 9 | namespace PushnotificationsDemo.Migrations 10 | { 11 | [DbContext(typeof(DemoDbContext))] 12 | partial class DemoDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("PushnotificationsDemo.Models.PushSubscription", b => 23 | { 24 | b.Property("P256Dh") 25 | .ValueGeneratedOnAdd(); 26 | 27 | b.Property("Auth") 28 | .IsRequired(); 29 | 30 | b.Property("Endpoint") 31 | .IsRequired(); 32 | 33 | b.Property("ExpirationTime"); 34 | 35 | b.Property("UserId") 36 | .IsRequired(); 37 | 38 | b.HasKey("P256Dh"); 39 | 40 | b.ToTable("PushSubscription"); 41 | }); 42 | #pragma warning restore 612, 618 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Models/DemoDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace PushnotificationsDemo.Models 4 | { 5 | public class DemoDbContext : DbContext 6 | { 7 | public DemoDbContext(DbContextOptions options) : base(options) { } 8 | 9 | public DbSet PushSubscription { get; set; } 10 | 11 | 12 | protected override void OnModelCreating(ModelBuilder builder) 13 | { 14 | base.OnModelCreating(builder); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PushnotificationsDemo.Models 4 | { 5 | public class ErrorViewModel 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 10 | } 11 | } -------------------------------------------------------------------------------- /PushnotificationsDemo/Models/Notification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace PushnotificationsDemo.Models 6 | { 7 | /// 8 | /// Notification API Standard 9 | /// 10 | public class Notification 11 | { 12 | public Notification() { } 13 | 14 | public Notification(string text) 15 | { 16 | Body = text; 17 | } 18 | 19 | [JsonProperty("title")] 20 | public string Title { get; set; } = "Push Demo"; 21 | 22 | [JsonProperty("lang")] 23 | public string Lang { get; set; } = "en"; 24 | 25 | [JsonProperty("body")] 26 | public string Body { get; set; } 27 | 28 | [JsonProperty("tag")] 29 | public string Tag { get; set; } 30 | 31 | [JsonProperty("image")] 32 | public string Image { get; set; } 33 | 34 | [JsonProperty("icon")] 35 | public string Icon { get; set; } 36 | 37 | [JsonProperty("badge")] 38 | public string Badge { get; set; } 39 | 40 | [JsonProperty("timestamp")] 41 | public DateTime Timestamp { get; set; } = DateTime.Now; 42 | 43 | [JsonProperty("requireInteraction")] 44 | public bool RequireInteraction { get; set; } = false; 45 | 46 | [JsonProperty("actions")] 47 | public List Actions { get; set; } = new List(); 48 | } 49 | 50 | /// 51 | /// Notification API Standard 52 | /// 53 | public class NotificationAction 54 | { 55 | 56 | [JsonProperty("action")] 57 | public string Action { get; set; } 58 | 59 | [JsonProperty("title")] 60 | public string Title { get; set; } 61 | } 62 | 63 | public class NotificationTag 64 | { 65 | public const string Notify = "demo_testmessage"; 66 | public const string Trivia = "demo_trivia"; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Models/PushSubscription.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace PushnotificationsDemo.Models 5 | { 6 | /// 7 | /// Database representation of a push subscription 8 | /// 9 | public class PushSubscription 10 | { 11 | /// 12 | public PushSubscription() { } 13 | 14 | /// 15 | public PushSubscription(string userId, WebPush.PushSubscription subscription) 16 | { 17 | UserId = userId; 18 | Endpoint = subscription.Endpoint; 19 | ExpirationTime = null; 20 | P256Dh = subscription.P256DH; 21 | Auth = subscription.Auth; 22 | } 23 | 24 | /// 25 | /// User id associated with the push subscription. 26 | /// 27 | [Required] 28 | [ForeignKey("User")] 29 | public string UserId { get; set; } 30 | 31 | /// 32 | /// The endpoint associated with the push subscription. 33 | /// 34 | [Required] 35 | public string Endpoint { get; set; } 36 | 37 | /// 38 | /// The subscription expiration time associated with the push subscription, if there is one, or null otherwise. 39 | /// 40 | public double? ExpirationTime { get; set; } 41 | 42 | /// 43 | /// An 44 | /// Elliptic curve Diffie–Hellman 45 | /// public key on the P-256 curve (that is, the NIST secp256r1 elliptic curve). 46 | /// The resulting key is an uncompressed point in ANSI X9.62 format. 47 | /// 48 | [Required] 49 | [Key] 50 | public string P256Dh { get; set; } 51 | 52 | /// 53 | /// An authentication secret, as described in 54 | /// Message Encryption for Web Push. 55 | /// 56 | [Required] 57 | public string Auth { get; set; } 58 | 59 | /// 60 | /// Converts the push subscription to the format of the library WebPush 61 | /// 62 | /// WebPush subscription 63 | public WebPush.PushSubscription ToWebPushSubscription() 64 | { 65 | return new WebPush.PushSubscription(Endpoint, P256Dh, Auth); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace PushnotificationsDemo 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateWebHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:1058", 7 | "sslPort": 44376 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "PushnotificationsDemo": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /PushnotificationsDemo/PushnotificationsDemo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Services/IPushService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PushnotificationsDemo.Models; 3 | 4 | namespace PushnotificationsDemo.Services 5 | { 6 | /// 7 | /// Defines a service to manage Push subscriptions and send Push notifications 8 | /// 9 | public interface IPushService 10 | { 11 | /// 12 | /// Checks VAPID info and if invalid generates new keys and throws exception 13 | /// 14 | /// This should be a URL or a 'mailto:' email address 15 | /// The VAPID public key as a base64 encoded string 16 | /// The VAPID private key as a base64 encoded string 17 | void CheckOrGenerateVapidDetails(string subject, string vapidPublicKey, string vapidPrivateKey); 18 | 19 | /// 20 | /// Get the server's saved VAPID public key 21 | /// 22 | /// VAPID public key 23 | string GetVapidPublicKey(); 24 | 25 | /// 26 | /// Register a push subscription (save to the database for later use) 27 | /// 28 | /// push subscription 29 | Task Subscribe(PushSubscription subscription); 30 | 31 | /// 32 | /// Un-register a push subscription (delete from the database) 33 | /// 34 | /// push subscription 35 | Task Unsubscribe(PushSubscription subscription); 36 | 37 | /// 38 | /// Send a plain text push notification to a user without any special option 39 | /// 40 | /// user id the push should be sent to 41 | /// text of the notification 42 | Task Send(string userId, string text); 43 | 44 | /// 45 | /// Send a push notification to a user 46 | /// 47 | /// user id the push should be sent to 48 | /// the notification to be sent 49 | Task Send(string userId, Notification notification); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Services/PushService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using Microsoft.EntityFrameworkCore; 9 | using PushnotificationsDemo.Models; 10 | using WebPush; 11 | using PushSubscription = PushnotificationsDemo.Models.PushSubscription; 12 | 13 | namespace PushnotificationsDemo.Services 14 | { 15 | /// 16 | public class PushService : IPushService 17 | { 18 | private readonly WebPushClient _client; 19 | private readonly DemoDbContext _context; 20 | private readonly VapidDetails _vapidDetails; 21 | 22 | /// 23 | public PushService(DemoDbContext context, string vapidSubject, string vapidPublicKey, string vapidPrivateKey) 24 | { 25 | _context = context; 26 | _client = new WebPushClient(); 27 | 28 | CheckOrGenerateVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey); 29 | 30 | _vapidDetails = new VapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey); 31 | } 32 | 33 | /// 34 | public PushService(DemoDbContext context, IConfiguration configuration) 35 | { 36 | _context = context; 37 | _client = new WebPushClient(); 38 | 39 | var vapidSubject = configuration.GetValue("Vapid:Subject"); 40 | var vapidPublicKey = configuration.GetValue("Vapid:PublicKey"); 41 | var vapidPrivateKey = configuration.GetValue("Vapid:PrivateKey"); 42 | 43 | CheckOrGenerateVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey); 44 | 45 | _vapidDetails = new VapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey); 46 | } 47 | 48 | /// 49 | public void CheckOrGenerateVapidDetails(string vapidSubject, string vapidPublicKey, string vapidPrivateKey) 50 | { 51 | if (string.IsNullOrEmpty(vapidSubject) || 52 | string.IsNullOrEmpty(vapidPublicKey) || 53 | string.IsNullOrEmpty(vapidPrivateKey)) 54 | { 55 | var vapidKeys = VapidHelper.GenerateVapidKeys(); 56 | 57 | // Prints 2 URL Safe Base64 Encoded Strings 58 | Debug.WriteLine($"Public {vapidKeys.PublicKey}"); 59 | Debug.WriteLine($"Private {vapidKeys.PrivateKey}"); 60 | 61 | throw new Exception( 62 | "You must set the Vapid:Subject, Vapid:PublicKey and Vapid:PrivateKey application settings or pass them to the service in the constructor. You can use the ones just printed to the debug console."); 63 | } 64 | } 65 | 66 | /// 67 | public string GetVapidPublicKey() => _vapidDetails.PublicKey; 68 | 69 | /// 70 | public async Task Subscribe(PushSubscription subscription) 71 | { 72 | if (await _context.PushSubscription.AnyAsync(s => s.P256Dh == subscription.P256Dh)) 73 | return await _context.PushSubscription.FindAsync(subscription.P256Dh); 74 | 75 | await _context.PushSubscription.AddAsync(subscription); 76 | await _context.SaveChangesAsync(); 77 | 78 | return subscription; 79 | } 80 | 81 | /// 82 | public async Task Unsubscribe(PushSubscription subscription) 83 | { 84 | if (!await _context.PushSubscription.AnyAsync(s => s.P256Dh == subscription.P256Dh)) return; 85 | 86 | _context.PushSubscription.Remove(subscription); 87 | await _context.SaveChangesAsync(); 88 | } 89 | 90 | /// 91 | public async Task Send(string userId, Notification notification) 92 | { 93 | foreach (var subscription in await GetUserSubscriptions(userId)) 94 | try 95 | { 96 | _client.SendNotification(subscription.ToWebPushSubscription(), JsonConvert.SerializeObject(notification), _vapidDetails); 97 | } 98 | catch (WebPushException e) 99 | { 100 | if (e.Message == "Subscription no longer valid") 101 | { 102 | _context.PushSubscription.Remove(subscription); 103 | await _context.SaveChangesAsync(); 104 | } 105 | else 106 | { 107 | // Track exception with eg. AppInsights 108 | } 109 | } 110 | } 111 | 112 | /// 113 | public async Task Send(string userId, string text) 114 | { 115 | await Send(userId, new Notification(text)); 116 | } 117 | 118 | /// 119 | /// Loads a list of user subscriptions from the database 120 | /// 121 | /// user id 122 | /// List of subscriptions 123 | private async Task> GetUserSubscriptions(string userId) => 124 | await _context.PushSubscription.Where(s => s.UserId == userId).ToListAsync(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.HttpsPolicy; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.ResponseCompression; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Net.Http.Headers; 11 | using PushnotificationsDemo.Models; 12 | using PushnotificationsDemo.Services; 13 | using System; 14 | using System.Diagnostics; 15 | using System.IO.Compression; 16 | 17 | namespace PushnotificationsDemo 18 | { 19 | public class Startup 20 | { 21 | public Startup(IConfiguration configuration) 22 | { 23 | Configuration = configuration; 24 | } 25 | 26 | public IConfiguration Configuration { get; } 27 | 28 | // This method gets called by the runtime. Use this method to add services to the container. 29 | public void ConfigureServices(IServiceCollection services) 30 | { 31 | services.AddSingleton(); 32 | services.AddScoped(); 33 | 34 | services.AddDbContextPool(options => options.UseSqlServer(Configuration.GetConnectionString("Database"))); 35 | 36 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 37 | 38 | // Add gzip compression 39 | services.Configure(options => options.Level = CompressionLevel.Optimal); 40 | services.AddResponseCompression(options => 41 | { 42 | options.Providers.Add(); 43 | //options.EnableForHttps = true; 44 | options.MimeTypes = new[] 45 | { 46 | // Default 47 | "text/plain", 48 | "text/css", 49 | "application/javascript", 50 | "text/html", 51 | "application/xml", 52 | "text/xml", 53 | "application/json", 54 | "text/json", 55 | 56 | // Custom 57 | "image/svg+xml", 58 | "application/font-woff2" 59 | }; 60 | }); 61 | 62 | services.Configure(options => 63 | { 64 | options.IncludeSubDomains = true; 65 | options.MaxAge = TimeSpan.FromDays(365); 66 | }); 67 | } 68 | 69 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 70 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, DemoDbContext dbContext) 71 | { 72 | if (env.IsDevelopment()) 73 | { 74 | app.UseDeveloperExceptionPage(); 75 | } 76 | else 77 | { 78 | app.UseExceptionHandler("/Home/Error"); 79 | app.UseHsts(); 80 | } 81 | 82 | try 83 | { 84 | dbContext.Database.Migrate(); 85 | } 86 | catch (Exception e) 87 | { 88 | Debug.WriteLine($"An error occurred seeding the DB: {e}"); 89 | } 90 | 91 | app.UseHttpsRedirection(); 92 | 93 | app.Use(async (context, next) => 94 | { 95 | context.Response.Headers.Add("X-Frame-Options", new[] { "SAMEORIGIN" }); 96 | context.Response.Headers.Add("Expect-CT", new[] { "expect-ct: max-age=604800, report-uri=https://example.com" }); 97 | context.Response.Headers.Add("X-XSS-Protection", new[] { "1; mode=block; report=https://example.com" }); 98 | context.Response.Headers.Add("X-Content-Type-Options", new[] { "nosniff" }); 99 | context.Response.Headers.Add("Referrer-Policy", new[] { "strict-origin-when-cross-origin" }); 100 | context.Response.Headers.Add("Feature-Policy", new[] { "accelerometer 'none'; camera 'none'; geolocation 'self'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'" }); 101 | context.Response.Headers.Add("Content-Security-Policy", new[] { "default-src 'self'; script-src 'self'; style-src 'self' *.msecnd.net; img-src 'self' data:; connect-src https: wss: 'self'; font-src 'self' c.s-microsoft.com; frame-src 'self'; form-action 'self'; upgrade-insecure-requests; report-uri https://example.com" }); 102 | context.Response.Headers.Remove(HeaderNames.Server); 103 | context.Response.Headers.Remove("X-Powered-By"); 104 | await next(); 105 | }); 106 | 107 | app.UseResponseCompression(); 108 | 109 | app.UseStaticFiles(new StaticFileOptions 110 | { 111 | OnPrepareResponse = ctx => 112 | { 113 | const int cacheExpirationInSeconds = 60 * 60 * 24 * 30; //one month 114 | ctx.Context.Response.Headers[HeaderNames.CacheControl] = 115 | "public,max-age=" + cacheExpirationInSeconds; 116 | } 117 | }); 118 | 119 | app.UseMvc(routes => 120 | { 121 | routes.MapRoute( 122 | name: "default", 123 | template: "{controller=Home}/{action=Index}/{id?}"); 124 | }); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /PushnotificationsDemo/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model PushnotificationsDemo.Models.ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | -------------------------------------------------------------------------------- /PushnotificationsDemo/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Vapid": { 3 | "Subject": "mailto:email@outlook.com", 4 | "PublicKey": "", 5 | "PrivateKey": "" 6 | }, 7 | "ConnectionStrings": { 8 | "Database": "Server=(localdb)\\mssqllocaldb;Database=PushDemoInMemoryDb;Trusted_Connection=True;ConnectRetryCount=0" 9 | }, 10 | "Logging": { 11 | "LogLevel": { 12 | "Default": "Warning" 13 | } 14 | }, 15 | "AllowedHosts": "*" 16 | } 17 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/css/demo-template.css: -------------------------------------------------------------------------------- 1 | /* 2 | * DEMO TEMPLATE STYLES 3 | * ============================================= 4 | */ 5 | 6 | * { 7 | margin: 0; 8 | padding: 0; 9 | box-sizing: border-box; 10 | } 11 | 12 | html { 13 | font: 400 100%/1.4 base, "Segoe UI", Segoe, "Segoe WP", "Lucida Grande", "Lucida Sans", Verdana, sans-serif; 14 | } 15 | 16 | :focus { 17 | outline-offset: 1px; 18 | } 19 | 20 | a:link, a:visited { 21 | text-underline-offset: 1px; 22 | text-decoration-skip: ink; 23 | } 24 | 25 | a:hover { 26 | text-decoration: none; 27 | } 28 | 29 | @media (min-width: 80em) { 30 | html { 31 | font-size: 112.5%; 32 | } 33 | } 34 | 35 | @media (min-width: 120em) { 36 | html { 37 | font-size: 118.75%; 38 | line-height: 1.5; 39 | } 40 | } 41 | 42 | /* 43 | * LAYOUT 44 | * --------------------------------------------- 45 | * @NOTE: The layout margins and paddings are in 46 | * rem (mostly) so that changing the font size 47 | * of a layout element doesn't change those 48 | * measurements. Vertical measurements in the 49 | * mobile-first experience are in em. 50 | * --------------------------------------------- 51 | */ 52 | 53 | .l-contain { 54 | margin-right: auto; 55 | margin-left: auto; 56 | padding-right: 1.25rem; 57 | padding-left: 1.25rem; 58 | max-width: 960px; 59 | max-width: 100rem; 60 | } 61 | 62 | .l-section { 63 | margin-top: 3.5em; 64 | } 65 | 66 | .l-section--banner, 67 | .l-section--banner--dark, 68 | .l-section--banner--dark-black, 69 | .c-intro { 70 | padding-top: 3.5em; 71 | padding-bottom: 3.5em; 72 | } 73 | 74 | .l-subsection, 75 | .l-subsection--banner--dark { 76 | margin-top: 2em; 77 | } 78 | 79 | .l-subsection--banner, 80 | .l-subsection--banner--dark { 81 | padding-top: 2em; 82 | padding-bottom: 2em; 83 | } 84 | 85 | .l-section--banner + .l-section--banner, 86 | .l-subsection--banner + .l-subsection--banner, 87 | .l-section--banner--dark + .l-section--banner--dark, 88 | .l-subsection--banner--dark + .l-subsection--banner--dark, 89 | .l-section--banner--dark-black + .l-section--banner--dark-black { 90 | margin-top: 0; 91 | } 92 | 93 | @media (min-width: 36em) { 94 | .l-contain { 95 | padding-right: 3rem; 96 | padding-left: 3rem; 97 | } 98 | 99 | .l-section { 100 | margin-top: 6rem; 101 | } 102 | 103 | .l-section--banner, 104 | .l-section--banner--dark, 105 | .c-intro { 106 | padding-top: 6rem; 107 | padding-bottom: 6rem; 108 | } 109 | 110 | .l-subsection { 111 | margin-top: 3rem; 112 | } 113 | 114 | .l-subsection--banner--dark, 115 | .l-subsection--banner { 116 | padding-top: 3rem; 117 | padding-bottom: 3rem; 118 | } 119 | 120 | .l-section--banner + .l-section--banner, 121 | .l-subsection--banner + .l-subsection--banner, 122 | .l-section--banner--dark + .l-section--banner--dark, 123 | .l-subsection--banner--dark + .l-subsection--banner--dark, 124 | .l-section--banner--dark-black + .l-section--banner--dark-black { 125 | margin-top: 0; 126 | } 127 | } 128 | 129 | @media (min-width: 60em) { 130 | .l-contain { 131 | padding-right: 6rem; 132 | padding-left: 6rem; 133 | } 134 | } 135 | 136 | /* 137 | * BODY TYPE 138 | * --------------------------------------------- 139 | */ 140 | 141 | p, ul, ol { 142 | margin-top: 1em; 143 | max-width: 44rem; 144 | } 145 | 146 | ul, ol { 147 | padding-left: 1.25em; 148 | } 149 | 150 | p:first-child, ul:first-child, ol:first-child { 151 | margin-top: 0; 152 | } 153 | 154 | li + li { 155 | margin-top: .625em; 156 | } 157 | 158 | pre { 159 | white-space: pre-wrap; 160 | } 161 | 162 | /* 163 | * UTILITIES 164 | * --------------------------------------------- 165 | */ 166 | 167 | /* Clear floated elements */ 168 | .u-clear::before, 169 | .u-clear::after { 170 | content: ""; 171 | display: table; 172 | } 173 | 174 | .u-clear::after { 175 | clear: both; 176 | } 177 | 178 | /* Hide element visually (not from screen reader) */ 179 | .u-sr-only { 180 | position: absolute; 181 | clip: rect(0, 0, 0, 0); 182 | } 183 | 184 | /* Simple list */ 185 | .u-simple-list { 186 | margin: 0; 187 | padding: 0; 188 | max-width: 100%; 189 | list-style: none; 190 | } 191 | 192 | /* 193 | * COMPONENT: NAV BAR 194 | * @NOTE: These styles are not intended to be 195 | * changed by demo authors. For those styles, 196 | * see demo.css 197 | * --------------------------------------------- 198 | */ 199 | 200 | .c-nav-bar { 201 | position: fixed; 202 | position: sticky; 203 | top: 0; 204 | width: 100%; 205 | font-size: .813em; 206 | line-height: 1.5; 207 | } 208 | 209 | .c-nav-bar__breadcrumb { 210 | position: relative; 211 | z-index: 9000; /* Higher than demo TOC */ 212 | margin-right: 9em; 213 | max-width: none; 214 | padding: .75em 1.25rem; 215 | } 216 | 217 | .c-nav-bar__breadcrumb li { 218 | display: block; 219 | margin-top: 0; 220 | white-space: nowrap; 221 | overflow: hidden; 222 | text-overflow: ellipsis; 223 | } 224 | 225 | .c-nav-bar__breadcrumb li + li { 226 | margin-top: 0; 227 | } 228 | 229 | .c-nav-bar__title { 230 | font-weight: 700; 231 | } 232 | 233 | .c-toc { 234 | position: absolute; 235 | z-index: 8890; /* Unnecessarily high just in case… */ 236 | top: 0; 237 | right: 0; 238 | width: 100%; 239 | } 240 | 241 | /* If alert present above nav, change layout */ 242 | .has-alert .c-toc { 243 | position: absolute; 244 | } 245 | 246 | .c-toc__btn { 247 | float: right; 248 | padding: 1.5em 1.25rem; 249 | text-align: left; 250 | } 251 | 252 | /* Focus styles are in your-demo-name.css */ 253 | .c-toc__btn:hover, 254 | .c-toc__btn:focus, 255 | .c-toc__item a:focus { 256 | outline: 0; 257 | } 258 | 259 | .c-toc__btn:hover { 260 | cursor: pointer; 261 | } 262 | 263 | .c-toc__arrow { 264 | margin-left: .5em; 265 | transform: translateY(.1em); 266 | } 267 | 268 | .c-toc__items { 269 | margin: 0; 270 | padding-left: 0; 271 | width: 100%; 272 | max-height: calc(100vh - 4.5em); 273 | overflow: auto; 274 | clear: both; 275 | list-style: none; 276 | } 277 | 278 | .c-toc__items[aria-hidden="true"] { 279 | display: none; 280 | } 281 | 282 | .c-toc__item { 283 | margin-top: 0; 284 | } 285 | 286 | .c-toc__item a { 287 | display: block; 288 | padding: 1em 1.25rem; 289 | text-decoration: none; 290 | transition: background-color ease-out 400ms; 291 | } 292 | 293 | .c-toc__item a:hover, 294 | .c-toc__item a:focus { 295 | text-decoration: underline; 296 | } 297 | 298 | @media (max-width: 36em) { 299 | .c-nav-bar__contain { 300 | padding: 0; 301 | } 302 | } 303 | 304 | @media (min-width: 36em) { 305 | .c-nav-bar { 306 | position: relative; 307 | } 308 | 309 | .c-nav-bar__breadcrumb { 310 | margin-right: 13em; 311 | padding: .75rem 0; 312 | white-space: nowrap; 313 | overflow: hidden; 314 | text-overflow: ellipsis; 315 | } 316 | 317 | .c-nav-bar__breadcrumb li { 318 | display: inline; 319 | } 320 | 321 | .c-nav-bar__index::after { 322 | padding: .5em; 323 | content: "\005C"; /* Backslash */ 324 | } 325 | 326 | .c-toc { 327 | position: fixed; 328 | right: 3rem; 329 | width: 12em; 330 | } 331 | 332 | .c-toc__btn, 333 | .c-toc__items { 334 | width: 100%; 335 | } 336 | 337 | .c-toc__btn, 338 | .c-toc__item a { 339 | padding: .75rem; 340 | } 341 | 342 | .c-toc__btn { 343 | float: none; 344 | } 345 | 346 | .c-toc__items { 347 | max-height: calc(100vh - 3em); 348 | } 349 | } 350 | 351 | @media (min-width: 48em) { 352 | .c-nav-bar { 353 | font-size: .938em; 354 | } 355 | } 356 | 357 | @media (min-width: 60em) { 358 | .c-toc { 359 | right: 6rem; 360 | } 361 | } 362 | 363 | @media (min-width: 112em) { 364 | /* Align toc to edge of contents whenever .l-contain maxes out */ 365 | .c-toc { 366 | right: calc(50vw - 44rem); 367 | } 368 | } 369 | 370 | /* 371 | * COMPONENT: ALERT 372 | * Only used for the feature-detection area for 373 | * now, but built as a component in case we want 374 | * to allow for reuse elsewhere 375 | * --------------------------------------------- 376 | */ 377 | 378 | .c-alert { 379 | position: relative; 380 | z-index: 9000; 381 | background: #f2f2f2; 382 | } 383 | 384 | .c-alert__contain { 385 | display: flex; 386 | justify-content: space-between; 387 | align-items: center; 388 | margin: 0 auto; 389 | max-width: 100rem; 390 | } 391 | 392 | /* Add side padding if not already on the element from .l-contain */ 393 | .c-alert__contain:not(.l-contain) { 394 | padding: 0 1em; 395 | } 396 | 397 | .c-alert a:link, 398 | .c-alert a:visited { 399 | display: inline-block; 400 | color: #0067b8; 401 | } 402 | 403 | .c-alert__message { 404 | margin: 0; 405 | max-width: 44rem; 406 | padding: 1em 0; 407 | padding: 1rem 0; 408 | font-size: .813em; 409 | } 410 | 411 | .c-alert--error a:link, 412 | .c-alert--error a:visited { 413 | color: #005da6; 414 | } 415 | 416 | .c-alert__dismiss { 417 | margin-left: 1em; 418 | width: 3em; 419 | height: 3em; 420 | background: url("../images/x.svg") no-repeat center; 421 | cursor: pointer; 422 | } 423 | 424 | .c-alert__dismiss:hover, 425 | .c-alert__dismiss:focus { 426 | outline: 2px solid hsla(0, 0%, 0%, .4); 427 | outline-offset: -2px; 428 | } 429 | 430 | .c-alert__dismiss:active { 431 | background-color: hsla(0, 0%, 0%, .2); 432 | } 433 | 434 | @media (min-width: 36em) { 435 | .c-alert__dismiss { 436 | margin-right: -1em; 437 | } 438 | } 439 | 440 | /* 441 | * COMPONENT: TOGGLE ANIMATIONS BUTTON 442 | * --------------------------------------------- 443 | */ 444 | 445 | .c-toggle-anim { 446 | display: none; 447 | position: fixed; 448 | z-index: 8880; 449 | right: 1em; 450 | bottom: 1em; 451 | font-size: .875em; 452 | } 453 | 454 | .c-toggle-anim__state { 455 | font-weight: 700; 456 | } 457 | 458 | body.has-js .c-toggle-anim { 459 | display: block; 460 | } 461 | 462 | /* 463 | * COMPONENT: OUTRO 464 | * --------------------------------------------- 465 | */ 466 | 467 | .c-outro { 468 | text-align: center; 469 | } 470 | 471 | .c-outro__byline p { 472 | margin-right: auto; 473 | margin-left: auto; 474 | max-width: 24em; 475 | font-size: .875em; 476 | } 477 | 478 | .c-outro__byline p + p { 479 | margin-top: .5em; 480 | } 481 | 482 | .c-outro__github { 483 | display: inline-block; 484 | margin-top: 2em; 485 | padding: .5em .75em; 486 | border: 1px solid transparent; 487 | line-height: 1; 488 | text-decoration: none; 489 | } 490 | 491 | .c-outro__github:hover, 492 | .c-outro__github:focus { 493 | border-color: hsla(0, 0%, 0%, .4); 494 | } 495 | 496 | .c-outro__github svg, 497 | .c-outro__github span { 498 | display: inline-block; 499 | vertical-align: middle; 500 | } 501 | 502 | .c-outro__github svg { 503 | width: 20px; 504 | height: 20px; 505 | } 506 | 507 | .c-outro__github span { 508 | margin-left: .5em; 509 | } -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/css/push-notifications.css: -------------------------------------------------------------------------------- 1 | /* 2 | * PUSH NOTIFICATIONS 3 | * ============================================= 4 | * @Dependencies: 5 | * @Note: 6 | * @TODO: 7 | */ 8 | 9 | body { 10 | background-color: #fff; 11 | } 12 | 13 | /* DO NOT remove focus altogether, just style to your colors */ 14 | :focus { 15 | outline: 1px dotted #000; 16 | } 17 | 18 | ::-moz-selection { 19 | background: #a5802a; 20 | color: #000; 21 | } 22 | 23 | ::selection { 24 | background: #a5802a; 25 | color: #000; 26 | } 27 | 28 | a:link, a:visited { 29 | color: #a5802a; 30 | } 31 | 32 | a:hover { 33 | color: #666; 34 | } 35 | 36 | .l-section--banner--dark a:link, 37 | .l-section--banner--dark a:visited, 38 | .l-section--banner--dark-black a:link, 39 | .l-section--banner--dark-black a:visited { 40 | color: #dbccaa; 41 | } 42 | 43 | .l-section--banner--dark a:hover, 44 | .l-section--banner--dark-black a:hover { 45 | color: #c3a86f; 46 | } 47 | 48 | /* 49 | * FORM ELEMENTS 50 | * --------------------------------------------- 51 | */ 52 | 53 | button { 54 | padding: .5em .75em; 55 | border: 0; 56 | border-radius: 0; 57 | font: inherit; 58 | -webkit-appearance: none; 59 | background-color: #a5802a; 60 | color: #000; 61 | } 62 | 63 | button:hover, 64 | button:focus { 65 | outline: 2px solid hsla(0, 0%, 0%, .4); 66 | outline-offset: 0; 67 | } 68 | 69 | button:hover { 70 | cursor: pointer; 71 | } 72 | 73 | button:active { 74 | background-color: #999; 75 | } 76 | 77 | /* 78 | * UNIVERSAL DEMO-SPECIFIC STYLES 79 | * ============================================= 80 | */ 81 | 82 | h1, 83 | h2, 84 | h3, 85 | button, 86 | .highlight, 87 | .card--caption { 88 | font-family: Georgia, 'Times New Roman', Times, serif; 89 | font-weight: normal; 90 | } 91 | 92 | h1, 93 | h2, 94 | h3 { 95 | text-align: center; 96 | } 97 | 98 | h1, 99 | .h1 { 100 | font-size: 3.4rem; 101 | letter-spacing: .1rem; 102 | } 103 | 104 | h2, 105 | .h2 { 106 | /* for spans */ 107 | position: relative; 108 | font-size: 2.6rem; 109 | font-style: italic; 110 | letter-spacing: .1rem; 111 | } 112 | 113 | h2::after { 114 | content: ""; 115 | position: relative; 116 | display: block; 117 | height: .5rem; 118 | width: 7rem; 119 | border-bottom: 3px solid; 120 | text-align: center; 121 | margin: .2rem auto; 122 | } 123 | 124 | h3, 125 | .h3 { 126 | font-size: 1.3rem; 127 | letter-spacing: .06rem; 128 | } 129 | 130 | .title-step { 131 | font-family: "Segoe UI", Segoe, "Segoe WP", "Lucida Grande", "Lucida Sans", Verdana, sans-serif; 132 | position: absolute; 133 | top: -1rem; 134 | left: 0; 135 | right: 0; 136 | 137 | margin: 0 auto; 138 | max-width: 6rem; 139 | 140 | font-size: .9rem; 141 | font-style: normal; 142 | font-weight: bold; 143 | text-transform: uppercase; 144 | letter-spacing: .01rem; 145 | 146 | color: #9b8e6e; 147 | } 148 | 149 | .c-nav-bar { 150 | z-index: 30; 151 | } 152 | 153 | .highlight { 154 | margin-top: 1.2rem; 155 | } 156 | 157 | .btn--action { 158 | margin: 2rem auto 1.2rem auto; 159 | } 160 | 161 | /* Only apply dark background style to subsection */ 162 | .subsection-dark { 163 | padding-top: 3rem; 164 | background-color: #202020; 165 | } 166 | 167 | .wrap { 168 | flex-wrap: wrap; 169 | } 170 | 171 | @media (max-width: 24em) { 172 | 173 | h1 { 174 | font-size: 3rem; 175 | } 176 | } 177 | 178 | /* 179 | * DEMO SECTION STYLES 180 | * ============================================= 181 | */ 182 | 183 | /* 184 | * INTRO 185 | * --------------------------------------------- 186 | */ 187 | 188 | .intro__heading { 189 | position: relative; 190 | max-width: 40rem; 191 | margin: 0 auto; 192 | padding: .5rem 1rem; 193 | background-color: #000; 194 | border: 1px solid #a5802a; 195 | z-index: 10; 196 | border-left: none; 197 | border-right: none 198 | } 199 | 200 | .heading__image { 201 | position: absolute; 202 | width: 11rem; 203 | left: 0; 204 | right: 0; 205 | margin: auto; 206 | top: -2.6rem; 207 | z-index: 0; 208 | } 209 | 210 | .heading__image-background { 211 | position: absolute; 212 | left: -3rem; 213 | top: 15rem; 214 | width: 65%; 215 | z-index: 0; 216 | } 217 | 218 | .intro__section { 219 | position: relative; 220 | z-index: 20; 221 | margin-top: 3rem 222 | } 223 | 224 | .intro-image { 225 | position: absolute; 226 | top: 4.8rem; 227 | left: 0; 228 | right: 0; 229 | margin: auto; 230 | width: 75%; 231 | z-index: 0; 232 | } 233 | 234 | .intro__section__cards { 235 | background-image: url("../images/star-background.svg"); 236 | background-repeat: no-repeat; 237 | background-position: center 108%; 238 | background-attachment: fixed; 239 | } 240 | 241 | .intro__section__overview > p:first-child { 242 | max-width: 34rem; 243 | } 244 | 245 | .intro__section__overview .highlight, 246 | .intro__section__cards .highlight, 247 | .outro__highlight .highlight { 248 | margin-left: auto; 249 | margin-right: auto; 250 | text-align: center; 251 | } 252 | 253 | /* 254 | * LAYOUT: FLEX 255 | * --------------------------------------------- 256 | */ 257 | 258 | .l-flex--guide, 259 | .l-flex--halves, 260 | .l-flex--guide--callout { 261 | display: flex; 262 | position: relative; 263 | z-index: 20; 264 | } 265 | 266 | .l-flex--halves, 267 | .l-flex--guide--callout { 268 | flex-wrap: wrap; 269 | } 270 | 271 | .l-flex-center { 272 | justify-content: center; 273 | } 274 | 275 | 276 | @media (min-width: 34em) { 277 | 278 | .l-flex--guide--callout { 279 | margin-left: -1.5rem; 280 | margin-right: -1.5rem; 281 | } 282 | 283 | } 284 | 285 | /* 286 | * GUIDE 287 | * --------------------------------------------- 288 | */ 289 | 290 | .guide-section { 291 | position: relative; 292 | } 293 | 294 | .side-image { 295 | background-image: url("../images/side-star-chart.svg"); 296 | background-repeat: no-repeat; 297 | background-position: right 0; 298 | background-attachment: fixed; 299 | background-size: 49%; 300 | } 301 | 302 | .side-image-left { 303 | background-image: url("../images/side-signs.svg"); 304 | background-repeat: no-repeat; 305 | background-position: left 0; 306 | background-attachment: fixed; 307 | background-size: 49%; 308 | } 309 | 310 | .moon-background { 311 | background-image: url("../images/try-it-out.svg"); 312 | background-repeat: no-repeat; 313 | background-position: center left; 314 | background-attachment: fixed; 315 | background-size: 100%; 316 | } 317 | 318 | .guide-col-main { 319 | width: 100%; 320 | margin-left: auto; 321 | margin-right: auto; 322 | } 323 | 324 | .guide-col-side { 325 | width: 100%; 326 | margin: 0 1.5rem; 327 | } 328 | 329 | /* Use for pullout/highlight */ 330 | .guide-col-main--alt { 331 | width: 100%; 332 | margin: 0 1.5rem; 333 | } 334 | 335 | .callout { 336 | position: relative; 337 | max-width: 30rem; 338 | font-size: 1.4rem; 339 | margin: 0 auto; 340 | padding: 1rem; 341 | text-align: center; 342 | } 343 | 344 | .callout::before { 345 | content: url("../images/triangle-up.svg"); 346 | display: block; 347 | position: absolute; 348 | margin: auto; 349 | left: 0; 350 | right: 0; 351 | top: -1.2rem; 352 | max-width: 2rem; 353 | background-color: #202020; 354 | } 355 | 356 | .callout-light, 357 | .callout-dark { 358 | position: relative; 359 | padding: 1rem; 360 | font-size: 1.2rem; 361 | text-align: center; 362 | } 363 | 364 | .callout-light { 365 | border: 2px solid #9b8e6e; 366 | } 367 | 368 | .callout, 369 | .callout-dark { 370 | border: 2px solid #a5802a; 371 | } 372 | 373 | .callout-text, 374 | .callout-text-dark { 375 | order: 1; 376 | margin-bottom: 1.5rem; 377 | } 378 | 379 | .callout-text::before { 380 | content: url("../images/moon-detail.svg"); 381 | display: block; 382 | position: absolute; 383 | margin: auto; 384 | left: 0; 385 | right: 0; 386 | top: -.8rem; 387 | max-width: 4rem; 388 | } 389 | 390 | .callout-text-dark::before { 391 | content: url("../images/earth-detail.svg"); 392 | display: block; 393 | position: absolute; 394 | margin: auto; 395 | left: 0; 396 | right: 0; 397 | top: -1rem; 398 | max-width: 2rem; 399 | } 400 | 401 | .callout-img { 402 | order: 2; 403 | } 404 | 405 | .callout-img--vertical { 406 | position: relative; 407 | display: block; 408 | top: 50%; 409 | transform: translateY(-50%); 410 | } 411 | 412 | code { 413 | padding: .25em; 414 | background: #dbccaa; 415 | color: #000; 416 | } 417 | 418 | pre { 419 | margin-top: 1.5em; 420 | margin-right: auto; 421 | max-width: 44rem; 422 | } 423 | 424 | pre code { 425 | display: block; 426 | padding: 1.25em; 427 | tab-size: 3; 428 | background: #dbccaa; 429 | color: #000; 430 | word-break: break-all; 431 | } 432 | 433 | @media (min-width: 47em) { 434 | 435 | .guide-col-main { 436 | max-width: 44rem; 437 | } 438 | 439 | .guide-col-main--alt { 440 | max-width: 38rem; 441 | flex: 1 1 20rem; 442 | } 443 | 444 | .guide-col-side { 445 | max-width: 24rem; 446 | flex: 1 1 15rem; 447 | } 448 | 449 | .callout-text, 450 | .callout-text-dark { 451 | margin-bottom: 0; 452 | } 453 | } 454 | 455 | /* 456 | * COMPONENT: CARDS 457 | * --------------------------------------------- 458 | */ 459 | 460 | .card { 461 | position: relative; 462 | flex: 1 1 20rem; 463 | margin: 1.5rem 0; 464 | max-width: 23rem; 465 | background-color: #202020; 466 | border: 2px solid #a5802a; 467 | } 468 | 469 | .card--right { 470 | order: 1; 471 | } 472 | 473 | .card--left { 474 | order: 2; 475 | } 476 | 477 | .card--bottom { 478 | border-top: 2px solid #a5802a; 479 | } 480 | 481 | .card h3 { 482 | position: absolute; 483 | max-width: 9rem; 484 | margin: auto; 485 | left: 0; 486 | right: 0; 487 | top: -1rem; 488 | background: #202020; 489 | } 490 | 491 | .card--illustration { 492 | max-width: 15rem; 493 | margin: 1rem auto 0 auto; 494 | } 495 | 496 | .card--caption { 497 | font-size: .8rem; 498 | padding: 1rem; 499 | letter-spacing: .02rem; 500 | line-height: 1.2rem; 501 | text-align: center; 502 | } 503 | 504 | @media (min-width: 34em) { 505 | 506 | .card { 507 | margin-right: 1.5rem; 508 | margin-left: 1.5rem; 509 | } 510 | 511 | } 512 | 513 | /* 514 | * LAYOUT: CHARTS 515 | * --------------------------------------------- 516 | */ 517 | 518 | .chart { 519 | position: relative; 520 | flex: 1 1 20rem; 521 | margin: 1.5rem 0; 522 | max-width: 35rem; 523 | background-color: #202020; 524 | } 525 | 526 | .non-chart { 527 | position: relative; 528 | flex: 1 1 20rem; 529 | max-height: 20rem; 530 | margin: 1.5rem 0; 531 | align-self: center; 532 | } 533 | 534 | @media (max-width: 34em) { 535 | 536 | .non-chart, 537 | .chart { 538 | width: 100%; 539 | margin: 1.5rem auto; 540 | } 541 | } 542 | 543 | @media (min-width: 34.1em) { 544 | 545 | .non-chart, 546 | .chart { 547 | margin-right: 1.5rem; 548 | margin-left: 1.5rem; 549 | } 550 | 551 | } 552 | 553 | /* 554 | * UNIVERSAL COMPONENTS 555 | * ============================================= 556 | */ 557 | 558 | /* 559 | * COMPONENT: NAV BAR 560 | * --------------------------------------------- 561 | */ 562 | 563 | .c-nav-bar { 564 | background: #000; 565 | color: #ccc; 566 | } 567 | 568 | .c-nav-bar a { 569 | color: #fff; 570 | border-bottom-color: rgba(255, 255, 255, .66); 571 | } 572 | 573 | /* DO NOT remove focus altogether, just style to your colors */ 574 | .c-nav-bar__breadcrumb a:focus { 575 | outline: 1px dotted #fff; 576 | } 577 | 578 | /* Only used in mobile-first design */ 579 | .c-nav-bar__breadcrumb { 580 | border-bottom-color: #444; 581 | } 582 | 583 | .c-nav-bar__title { 584 | color: #fff; 585 | } 586 | 587 | /* TABLE OF CONTENTS DROPDOWN */ 588 | 589 | .c-toc__btn { 590 | background: #000; 591 | color: #fff; 592 | } 593 | 594 | .c-toc__btn:hover, 595 | .c-toc__btn:focus { 596 | background: #222; 597 | } 598 | 599 | .c-toc__btn:active { 600 | background: #444; 601 | } 602 | 603 | .c-toc__arrow path { 604 | stroke: #fff; 605 | } 606 | 607 | .c-toc__items { 608 | background: #444; 609 | } 610 | 611 | .c-toc__item a:hover, 612 | .c-toc__item a:focus { 613 | background: #222; 614 | } 615 | 616 | /* 617 | * COMPONENT: INTRO 618 | * --------------------------------------------- 619 | */ 620 | 621 | .c-intro { 622 | background: #f9f9f9; 623 | text-align: center; 624 | } 625 | 626 | /* 627 | * LAYOUT: "BANNER" SECTIONS 628 | * --------------------------------------------- 629 | */ 630 | 631 | .l-section--banner, 632 | .l-subsection--banner { 633 | background-color: #f2f2f2; 634 | } 635 | 636 | .l-section--banner--dark, 637 | .l-subsection--banner--dark { 638 | background-color: #202020; 639 | color: #fff; 640 | } 641 | 642 | .l-section--banner--dark-black { 643 | background-color: #0c0c0c; 644 | color: #fff; 645 | } 646 | 647 | .l-section--banner a:link, 648 | .l-section--banner a:visited, 649 | .l-subsection--banner a:link, 650 | .l-subsection--banner a:visited { 651 | color: #000; 652 | } 653 | 654 | /* 655 | * COMPONENT: OUTRO 656 | * --------------------------------------------- 657 | */ 658 | 659 | .c-outro a { 660 | color: #000; 661 | } 662 | 663 | .c-outro a:hover { 664 | color: #555; 665 | } 666 | 667 | a.c-outro__github { 668 | background: #a5802a; 669 | color: #000; 670 | } 671 | 672 | a.c-outro__github:hover { 673 | background: #86671d; 674 | color: #fff; 675 | } 676 | 677 | a.c-outro__github:active { 678 | background: #86671d; 679 | } -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/astrology-demo_chart-5.svg: -------------------------------------------------------------------------------- 1 | View today’s astrology reading for Aries -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/client-side.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/earth-detail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/pushnotifications-demo-aspnetcore/957b08133a2d0a2339ce6aa233a5238a26a243e8/PushnotificationsDemo/wwwroot/images/favicon.png -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/header-embellishment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/hero-background-stars.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/leo-server-side.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/moon-detail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/social-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/pushnotifications-demo-aspnetcore/957b08133a2d0a2339ce6aa233a5238a26a243e8/PushnotificationsDemo/wwwroot/images/social-image.jpg -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/star-background.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/toast-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/pushnotifications-demo-aspnetcore/957b08133a2d0a2339ce6aa233a5238a26a243e8/PushnotificationsDemo/wwwroot/images/toast-image.jpg -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/triangle-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/triangle-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/try-it-out.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/images/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/js/demo-template.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | /* eslint-disable no-var, prefer-template, strict, prefer-arrow-callback, object-shorthand, no-continue */ 3 | /*globals event*/ 4 | 5 | /* 6 | * DEMO TEMPLATE SCRIPTS 7 | * ============================================= 8 | */ 9 | 10 | /* 11 | * GENERIC JS-ENABLED CLASS ON BODY 12 | * --------------------------------------------- 13 | */ 14 | (function () { 15 | 'use strict'; 16 | 17 | document.body.classList.add('has-js'); 18 | }()); 19 | 20 | /* 21 | * FEATURE SUPPORT ALERT 22 | * --------------------------------------------- 23 | */ 24 | 25 | (function () { 26 | 'use strict'; 27 | 28 | /* These scripts are written old-school so that they work on older browsers */ 29 | var closeAlert = function(e) { 30 | if (e.target.id === 'dismissFeatureAlert') { 31 | var featureAlert = document.getElementById('featureAlert'); 32 | document.body.removeChild(featureAlert); 33 | document.body.classList.remove('has-alert'); 34 | document.querySelector('.c-nav-bar').focus(); 35 | } 36 | }; 37 | 38 | var insertAlert = function() { 39 | var featureAlertMsg = 'Notice: This page demonstrates web push notifications, which is not supported in your browser version. For the full experience, please view in Microsoft Edge build 17603+ or any browser that supports web push notifications.', 40 | featureAlert = document.createElement('div'); 41 | 42 | featureAlert.className = 'c-alert c-alert--error'; 43 | featureAlert.setAttribute('role', 'alert'); 44 | featureAlert.setAttribute('aria-live', 'polite'); 45 | featureAlert.setAttribute('id', 'featureAlert'); 46 | 47 | document.body.insertBefore(featureAlert, document.body.querySelector(':first-child')); 48 | document.body.classList.add('has-alert'); 49 | 50 | /* Set trivial timeout to trigger aria-live readout cross-browser */ 51 | setTimeout(function(){ 52 | featureAlert.innerHTML = '

' + featureAlertMsg + '

'; 53 | }, 10); 54 | 55 | /* Makes heading focusable by JS, for when alert is cleared */ 56 | document.querySelector('.c-nav-bar').setAttribute('tabindex', '-1'); 57 | window.addEventListener('click', closeAlert, false); 58 | }; 59 | 60 | /* Add your own feature query conditions here, run insertAlert() only if false */ 61 | if (!(navigator.serviceWorker && 'PushManager' in window)) { 62 | insertAlert(); 63 | } 64 | }()); 65 | 66 | /* 67 | * COMPONENT: DEMO NAV 68 | * --------------------------------------------- 69 | */ 70 | 71 | /* Generates nav items for each section on the page */ 72 | (function () { 73 | 'use strict'; 74 | 75 | const demoNavItems = document.getElementById('js-nav-items'); 76 | const pageSections = document.querySelectorAll('[data-nav-label]'); 77 | 78 | for (let i = 0; i < pageSections.length; i++) { 79 | const section = pageSections[i]; 80 | const newLink = document.createElement('li'); 81 | newLink.className = 'c-toc__item'; 82 | newLink.innerHTML = '' + section.getAttribute('data-nav-label') + ''; 83 | demoNavItems.appendChild(newLink); 84 | 85 | // Smooth scroll TOC links 86 | newLink.addEventListener('click', function(e) { 87 | const thisHash = e.target.hash, 88 | thisID = thisHash.replace('#', ''); 89 | if (thisID) { 90 | e.preventDefault(); 91 | document.getElementById(thisID).scrollIntoView({block: 'start', behavior: 'smooth'}); 92 | history.replaceState({}, '', window.location.pathname + thisHash); 93 | return false; 94 | } 95 | }); 96 | } 97 | }()); 98 | 99 | //nav menu 100 | (function () { 101 | 'use strict'; 102 | 103 | const menu = document.querySelector('.c-toc__btn'); 104 | const items = menu.parentElement.querySelector('.c-toc__items'); 105 | 106 | const collapse = function () { 107 | items.setAttribute('aria-hidden', 'true'); 108 | menu.setAttribute('aria-expanded', 'false'); 109 | }; 110 | 111 | 112 | const toggleSection = function (evt) { 113 | evt.preventDefault(); 114 | evt.stopPropagation(); 115 | const expanded = evt.currentTarget.getAttribute('aria-expanded') === 'true'; 116 | 117 | if (expanded) { 118 | collapse(); 119 | } else { 120 | items.removeAttribute('aria-hidden'); 121 | menu.setAttribute('aria-expanded', 'true'); 122 | } 123 | }; 124 | 125 | const toggleKeydownSection = function (evt) { 126 | const key = evt.which || evt.keyCode; 127 | 128 | if (key !== 32 && key !== 13) { 129 | return; 130 | } 131 | 132 | toggleSection(evt); 133 | }; 134 | 135 | 136 | menu.addEventListener('click', toggleSection, false); 137 | menu.addEventListener('keydown', toggleKeydownSection, false); 138 | 139 | document.addEventListener('click', function () { 140 | collapse(); 141 | }); 142 | 143 | const insideContainer = function (item, container) { 144 | let result = false; 145 | 146 | while (item) { 147 | if (item === container) { 148 | result = true; 149 | break; 150 | } 151 | 152 | item = item.parentElement; //eslint-disable-line no-param-reassign 153 | } 154 | 155 | return result; 156 | }; 157 | 158 | document.addEventListener('focus', function (evt) { 159 | const target = evt.target; 160 | const expandedMenus = document.querySelectorAll('.navbar__submenu:not([aria-hidden="true"])'); 161 | 162 | if (expandedMenus.length === 0) { 163 | return; 164 | } 165 | 166 | for (let j = 0, lj = expandedMenus.length; j < lj; j++) { 167 | const expandedMenu = expandedMenus[j]; 168 | 169 | if (!insideContainer(target, expandedMenu)) { 170 | expandedMenu.setAttribute('aria-hidden', 'true'); 171 | expandedMenu.parentElement.querySelector('[aria-expanded="true"]').removeAttribute('aria-expanded'); 172 | } 173 | } 174 | }, true); 175 | }()); 176 | 177 | (function () { 178 | 'use strict'; 179 | 180 | var menus = document.querySelectorAll('[data-menu]'); 181 | 182 | if (menus.length === 0) { 183 | return; 184 | } 185 | 186 | var findIndex = function (element, elements) { 187 | var index, l; 188 | 189 | for (index = 0, l = elements.length; index < l; index++) { 190 | if (elements[index] === element) { 191 | return index; 192 | } 193 | } 194 | 195 | return null; 196 | }; 197 | 198 | var next = function (elements) { 199 | return function (index) { 200 | if (typeof index === 'number') { 201 | var i = index + 1; 202 | var element = elements[i]; 203 | var current = document.activeElement; 204 | 205 | while (element) { 206 | element.focus(); 207 | if (current !== document.activeElement) { 208 | break; 209 | } else { 210 | i++; 211 | element = elements[i]; 212 | } 213 | } 214 | } 215 | }; 216 | }; 217 | 218 | var previous = function (elements) { 219 | return function (index) { 220 | if (typeof index === 'number') { 221 | var i = index - 1; 222 | var element = elements[i]; 223 | var current = document.activeElement; 224 | 225 | while (element) { 226 | element.focus(); 227 | if (current !== document.activeElement) { 228 | break; 229 | } else { 230 | i--; 231 | element = elements[i]; 232 | } 233 | } 234 | } 235 | }; 236 | }; 237 | 238 | var findSiblings = function (source, type, topParent) { 239 | if (source === topParent) { 240 | return []; 241 | } 242 | 243 | var parent = source.parentElement; 244 | 245 | var elements = topParent.querySelectorAll(type); 246 | 247 | if (elements.length === 1) { 248 | return findSiblings(parent, type, topParent); 249 | } 250 | 251 | return elements; 252 | }; 253 | 254 | var arrowAction = function (action, container) { 255 | var activeElement = document.activeElement; 256 | var elements = findSiblings(activeElement, activeElement.tagName.toLowerCase(), container); 257 | var nextElementTo = action(elements); 258 | 259 | nextElementTo(findIndex(activeElement, elements)); 260 | }; 261 | 262 | var keydown = function (container) { 263 | return function () { 264 | var key = event.keyCode; 265 | var handled = true; 266 | 267 | //right or down 268 | if (key === 39 || key === 40) { 269 | arrowAction(next, container); 270 | //up or left 271 | } else if (key === 38 || key === 37) { 272 | arrowAction(previous, container); 273 | } else { 274 | handled = false; 275 | } 276 | 277 | if (handled) { 278 | event.stopPropagation(); 279 | event.preventDefault(); 280 | } 281 | }; 282 | }; 283 | 284 | for (var i = 0, l = menus.length; i < l; i++) { 285 | var menu = menus[i]; 286 | 287 | menu.addEventListener('keydown', keydown(menu), false); 288 | } 289 | }()); -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/js/push-notifications.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NAME OF YOUR DEMO 3 | * ============================================= 4 | */ 5 | 6 | /* 7 | * COMPONENT: PAUSE ANIMATIONS BUTTON 8 | * Toggles "has-anim" class; CSS selectors and JS 9 | * should be written so that animations only run 10 | * when this class is present 11 | * ---------------------------------------------- 12 | */ 13 | 14 | (function () { 15 | 'use strict'; 16 | 17 | const animButton = document.getElementById('toggle-anim'); 18 | 19 | if (animButton) { 20 | const toggleAnim = function() { 21 | const stateName = animButton.querySelector('.c-toggle-anim__state'); 22 | if (animButton.getAttribute('aria-pressed') === 'true') { 23 | console.log('animations were off'); 24 | animButton.setAttribute('aria-pressed', 'false'); 25 | stateName.innerText = animButton.getAttribute('data-unpressed-text'); 26 | document.body.classList.add('has-anim'); 27 | } else { 28 | console.log('animations were on'); 29 | animButton.setAttribute('aria-pressed', 'true'); 30 | stateName.innerText = animButton.getAttribute('data-pressed-text'); 31 | document.body.classList.remove('has-anim'); 32 | } 33 | }; 34 | 35 | const showAnimButton = function() { 36 | document.body.classList.add('has-anim'); 37 | animButton.removeAttribute('aria-hidden'); 38 | animButton.setAttribute('aria-pressed', 'false'); 39 | animButton.addEventListener('click', toggleAnim, false); 40 | }; 41 | 42 | showAnimButton(); 43 | } 44 | }()); -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/js/script.js: -------------------------------------------------------------------------------- 1 | function registerServiceWorker() { 2 | return navigator.serviceWorker.register('service-worker.js'); 3 | } 4 | 5 | function resetServiceWorkerAndPush() { 6 | return navigator.serviceWorker.getRegistration() 7 | .then(function (registration) { 8 | if (registration) { 9 | return registration.unregister(); 10 | } 11 | }) 12 | .then(function () { 13 | return registerServiceWorker().then(function (registration) { 14 | return registerPush(); 15 | }); 16 | }); 17 | } 18 | 19 | function subscribePushAndUpdateButtons(registration) { 20 | return subscribePush(registration).then(function (subscription) { 21 | updateUnsubscribeButtons(); 22 | return subscription; 23 | }); 24 | } 25 | 26 | function registerPush() { 27 | return navigator.serviceWorker.ready 28 | .then(function (registration) { 29 | return registration.pushManager.getSubscription().then(function (subscription) { 30 | if (subscription) { 31 | // renew subscription if we're within 5 days of expiration 32 | if (subscription.expirationTime && Date.now() > subscription.expirationTime - 432000000) { 33 | return unsubscribePush().then(function () { 34 | updateUnsubscribeButtons(); 35 | return subscribePushAndUpdateButtons(registration); 36 | }); 37 | } 38 | 39 | return subscription; 40 | } 41 | 42 | return subscribePushAndUpdateButtons(registration); 43 | }); 44 | }) 45 | .then(function (subscription) { 46 | return saveSubscription(subscription); 47 | }); 48 | } 49 | 50 | function sendMessage(title, message, delay) { 51 | const notification = { 52 | title: title, 53 | body: message, 54 | //tag: 'demo_testmessage' //we don't want a tag, as it would cause every notification after the first one to appear in the notification drawer only 55 | }; 56 | 57 | const userId = localStorage.getItem('userId'); 58 | let apiUrl = `./api/push/send/${userId}`; 59 | if (delay) apiUrl += `?delay=${delay}`; 60 | 61 | return fetch(apiUrl, { 62 | method: 'post', 63 | headers: { 64 | 'Content-type': 'application/json' 65 | }, 66 | body: JSON.stringify(notification) 67 | }); 68 | } 69 | 70 | function getPushSubscription() { 71 | return navigator.serviceWorker.ready 72 | .then(function (registration) { 73 | return registration.pushManager.getSubscription(); 74 | }); 75 | } 76 | 77 | function unsubscribePush() { 78 | return getPushSubscription().then(function (subscription) { 79 | return subscription.unsubscribe().then(function () { 80 | deleteSubscription(subscription); 81 | }); 82 | }); 83 | } 84 | 85 | function updateUnsubscribeButtons() { 86 | const unsubBtn = document.getElementById('unsubscribe-push'); 87 | const unsubBtn2 = document.getElementById('unsubscribe-push-2'); 88 | 89 | if (!(navigator.serviceWorker && 'PushManager' in window)) { 90 | // service worker is not supported, so it won't work! 91 | unsubBtn.innerText = 'SW & Push are Not Supported'; 92 | unsubBtn2.innerText = 'SW & Push are Not Supported'; 93 | return; 94 | } 95 | 96 | const fn = function (event) { 97 | event.preventDefault(); 98 | unsubscribePush().then(function () { 99 | updateUnsubscribeButtons(); 100 | }); 101 | }; 102 | 103 | return getPushSubscription() 104 | .then(function (subscription) { 105 | if (subscription) { 106 | unsubBtn.removeAttribute('disabled'); 107 | unsubBtn.innerText = 'Unsubscribe from push'; 108 | unsubBtn2.removeAttribute('disabled'); 109 | unsubBtn2.innerText = 'Unsubscribe from push'; 110 | 111 | unsubBtn.addEventListener('click', fn); 112 | unsubBtn2.addEventListener('click', fn); 113 | } else { 114 | unsubBtn.setAttribute('disabled', 'disabled'); 115 | unsubBtn.innerText = 'Not subscribed'; 116 | unsubBtn2.setAttribute('disabled', 'disabled'); 117 | unsubBtn2.innerText = 'Not subscribed'; 118 | 119 | unsubBtn.removeEventListener('click', fn); 120 | unsubBtn2.removeEventListener('click', fn); 121 | } 122 | }); 123 | } 124 | 125 | document.addEventListener('DOMContentLoaded', function (event) { 126 | const pushBtn = document.getElementById('initiate-push'); 127 | const pushBtn2 = document.getElementById('initiate-push-2'); 128 | 129 | if (!(navigator.serviceWorker && 'PushManager' in window)) { 130 | // service worker is not supported, so it won't work! 131 | pushBtn.innerText = 'SW & Push are Not Supported'; 132 | pushBtn2.innerText = 'SW & Push are Not Supported'; 133 | return; 134 | } 135 | 136 | registerServiceWorker().then(function () { 137 | pushBtn.removeAttribute('disabled'); 138 | pushBtn2.removeAttribute('disabled'); 139 | pushBtn.innerText = 'Initiate push'; 140 | pushBtn2.innerText = 'Initiate push'; 141 | pushBtn.addEventListener('click', function (event) { 142 | event.preventDefault(); 143 | registerPush().then(function () { 144 | sendMessage('Interested in how to do this?', 145 | 'Click on this notification to get back to the tutorial to learn how to do this!', 5000); 146 | }); 147 | }); 148 | pushBtn2.addEventListener('click', function (event) { 149 | event.preventDefault(); 150 | registerPush().then(function () { 151 | sendMessage('Cool!', 'It works!'); 152 | }); 153 | }); 154 | updateUnsubscribeButtons(); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/js/util.js: -------------------------------------------------------------------------------- 1 | function urlB64ToUint8Array(base64String) { 2 | const padding = '='.repeat((4 - base64String.length % 4) % 4); 3 | const base64 = (base64String + padding) 4 | .replace(/\-/g, '+') 5 | .replace(/_/g, '/'); 6 | const rawData = window.atob(base64); 7 | const outputArray = new Uint8Array(rawData.length); 8 | 9 | for (let i = 0; i < rawData.length; ++i) { 10 | outputArray[i] = rawData.charCodeAt(i); 11 | } 12 | 13 | return outputArray; 14 | } 15 | 16 | function subscribePush(registration) { 17 | return getPublicKey().then(function (key) { 18 | return registration.pushManager.subscribe({ 19 | userVisibleOnly: true, 20 | applicationServerKey: key 21 | }); 22 | }); 23 | } 24 | 25 | function getPublicKey() { 26 | return fetch('./api/push/vapidpublickey') 27 | .then(function (response) { 28 | return response.json(); 29 | }) 30 | .then(function (data) { 31 | return urlB64ToUint8Array(data); 32 | }); 33 | } 34 | 35 | function saveSubscription(subscription) { 36 | return fetch('./api/push/subscribe', { 37 | method: 'post', 38 | headers: { 39 | 'Content-type': 'application/json' 40 | }, 41 | body: JSON.stringify({ 42 | subscription: subscription 43 | }) 44 | }) 45 | .then(response => response.json()) 46 | .then(response => { 47 | localStorage.setItem('userId', response.userId); 48 | }); 49 | } 50 | 51 | function deleteSubscription(subscription) { 52 | return fetch('./api/push/unsubscribe', { 53 | method: 'post', 54 | headers: { 55 | 'Content-type': 'application/json' 56 | }, 57 | body: JSON.stringify({ 58 | subscription: subscription 59 | }) 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /PushnotificationsDemo/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | importScripts('./js/util.js'); 4 | 5 | self.addEventListener('install', function (event) { 6 | self.skipWaiting(); 7 | }); 8 | 9 | self.addEventListener('activate', function (event) { 10 | event.waitUntil(clients.claim()); 11 | }); 12 | 13 | // Respond to a server push with a user notification 14 | self.addEventListener('push', function (event) { 15 | if (event.data) { 16 | const { title, lang = 'en', body, tag, timestamp, requireInteraction, actions, image } = event.data.json(); 17 | 18 | const promiseChain = self.registration.showNotification(title, { 19 | lang, 20 | body, 21 | requireInteraction, 22 | tag: tag || undefined, 23 | timestamp: timestamp ? Date.parse(timestamp) : undefined, 24 | actions: actions || undefined, 25 | image: image || undefined, 26 | badge: '/images/favicon.png', 27 | icon: '/images/toast-image.jpg' 28 | }); 29 | 30 | // Ensure the toast notification is displayed before exiting this function 31 | event.waitUntil(promiseChain); 32 | } 33 | }); 34 | 35 | self.addEventListener('notificationclick', function (event) { 36 | event.notification.close(); 37 | 38 | event.waitUntil( 39 | clients.matchAll({ type: 'window', includeUncontrolled: true }) 40 | .then(function (clientList) { 41 | if (clientList.length > 0) { 42 | let client = clientList[0]; 43 | 44 | for (let i = 0; i < clientList.length; i++) { 45 | if (clientList[i].focused) { 46 | client = clientList[i]; 47 | } 48 | } 49 | 50 | return client.focus(); 51 | } 52 | 53 | return clients.openWindow('/'); 54 | }) 55 | ); 56 | }); 57 | 58 | self.addEventListener('pushsubscriptionchange', function (event) { 59 | event.waitUntil( 60 | Promise.all([ 61 | Promise.resolve(event.oldSubscription ? deleteSubscription(event.oldSubscription) : true), 62 | Promise.resolve(event.newSubscription ? event.newSubscription : subscribePush(registration)) 63 | .then(function (sub) { return saveSubscription(sub); }) 64 | ]) 65 | ); 66 | }); 67 | -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/Models/DemoDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Debug; 5 | 6 | namespace PushnotificationsDemo.Models 7 | { 8 | public class DemoDbContext : DbContext 9 | { 10 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 11 | { 12 | var connectionString = Environment.GetEnvironmentVariable("Database", EnvironmentVariableTarget.Process); 13 | if (string.IsNullOrEmpty(connectionString)) throw new Exception("Application setting 'SqlDatabase' was not found. Add it in settings.json!"); 14 | optionsBuilder.UseSqlServer(connectionString); 15 | var isDevelopment = Environment.GetEnvironmentVariable("Environment", EnvironmentVariableTarget.Process) == "Development"; 16 | if (isDevelopment) optionsBuilder.UseLoggerFactory(new LoggerFactory(new[] { new DebugLoggerProvider() })); 17 | } 18 | 19 | public DbSet PushSubscription { get; set; } 20 | 21 | 22 | protected override void OnModelCreating(ModelBuilder builder) 23 | { 24 | base.OnModelCreating(builder); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/Models/Notification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace PushnotificationsDemo.Models 6 | { 7 | /// 8 | /// Notification API Standard 9 | /// 10 | public class Notification 11 | { 12 | public Notification() { } 13 | 14 | public Notification(string text) 15 | { 16 | Body = text; 17 | } 18 | 19 | [JsonProperty("title")] 20 | public string Title { get; set; } = "Push Demo"; 21 | 22 | [JsonProperty("lang")] 23 | public string Lang { get; set; } = "en"; 24 | 25 | [JsonProperty("body")] 26 | public string Body { get; set; } 27 | 28 | [JsonProperty("tag")] 29 | public string Tag { get; set; } 30 | 31 | [JsonProperty("image")] 32 | public string Image { get; set; } 33 | 34 | [JsonProperty("icon")] 35 | public string Icon { get; set; } 36 | 37 | [JsonProperty("badge")] 38 | public string Badge { get; set; } 39 | 40 | [JsonProperty("timestamp")] 41 | public DateTime Timestamp { get; set; } = DateTime.Now; 42 | 43 | [JsonProperty("requireInteraction")] 44 | public bool RequireInteraction { get; set; } = false; 45 | 46 | [JsonProperty("actions")] 47 | public List Actions { get; set; } = new List(); 48 | } 49 | 50 | /// 51 | /// Notification API Standard 52 | /// 53 | public class NotificationAction 54 | { 55 | 56 | [JsonProperty("action")] 57 | public string Action { get; set; } 58 | 59 | [JsonProperty("title")] 60 | public string Title { get; set; } 61 | } 62 | 63 | public class NotificationTag 64 | { 65 | public const string Notify = "demo_testmessage"; 66 | public const string Trivia = "demo_trivia"; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/Models/PushSubscription.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace PushnotificationsDemo.Models 5 | { 6 | /// 7 | /// Database representation of a push subscription 8 | /// 9 | public class PushSubscription 10 | { 11 | /// 12 | public PushSubscription() { } 13 | 14 | /// 15 | public PushSubscription(string userId, WebPush.PushSubscription subscription) 16 | { 17 | UserId = userId; 18 | Endpoint = subscription.Endpoint; 19 | ExpirationTime = null; 20 | P256Dh = subscription.P256DH; 21 | Auth = subscription.Auth; 22 | } 23 | 24 | /// 25 | /// User id associated with the push subscription. 26 | /// 27 | [Required] 28 | [ForeignKey("User")] 29 | public string UserId { get; set; } 30 | 31 | /// 32 | /// The endpoint associated with the push subscription. 33 | /// 34 | [Required] 35 | public string Endpoint { get; set; } 36 | 37 | /// 38 | /// The subscription expiration time associated with the push subscription, if there is one, or null otherwise. 39 | /// 40 | public double? ExpirationTime { get; set; } 41 | 42 | /// 43 | /// An 44 | /// Elliptic curve Diffie–Hellman 45 | /// public key on the P-256 curve (that is, the NIST secp256r1 elliptic curve). 46 | /// The resulting key is an uncompressed point in ANSI X9.62 format. 47 | /// 48 | [Required] 49 | [Key] 50 | public string P256Dh { get; set; } 51 | 52 | /// 53 | /// An authentication secret, as described in 54 | /// Message Encryption for Web Push. 55 | /// 56 | [Required] 57 | public string Auth { get; set; } 58 | 59 | /// 60 | /// Converts the push subscription to the format of the library WebPush 61 | /// 62 | /// WebPush subscription 63 | public WebPush.PushSubscription ToWebPushSubscription() 64 | { 65 | return new WebPush.PushSubscription(Endpoint, P256Dh, Auth); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/Notify.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.WebJobs; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Logging; 4 | using PushnotificationsDemo.Models; 5 | using PushnotificationsDemo.Services; 6 | using System; 7 | using System.Threading.Tasks; 8 | 9 | namespace PushnotificationsDemoFunction 10 | { 11 | public static class Notify 12 | { 13 | [FunctionName("Notify")] 14 | public static async Task RunAsync([TimerTrigger("0 0 7 * * *")]TimerInfo myTimer, ILogger log) 15 | { 16 | log.LogInformation($"Notify function executed at: {DateTime.Now}"); 17 | 18 | var vapidSubject = Environment.GetEnvironmentVariable("VapidSubject", EnvironmentVariableTarget.Process); 19 | var vapidPublicKey = Environment.GetEnvironmentVariable("VapidPublicKey", EnvironmentVariableTarget.Process); 20 | var vapidPrivateKey = Environment.GetEnvironmentVariable("VapidPrivateKey", EnvironmentVariableTarget.Process); 21 | 22 | // Get subscriptions from SQL database 23 | var context = new DemoDbContext(); 24 | var subscriptions = await context.PushSubscription.ToListAsync(); 25 | 26 | if (subscriptions.Count == 0) 27 | { 28 | log.LogInformation("No subscriptions were found in the database. Exciting."); 29 | return; 30 | } 31 | 32 | log.LogInformation($"{subscriptions.Count} subscriptions was found in the database."); 33 | 34 | var pushService = new PushService(context, vapidSubject, vapidPublicKey, vapidPrivateKey); 35 | 36 | var pushMessage = TriviaList[DateTime.Today.DayOfYear % TriviaList.Length]; 37 | 38 | log.LogInformation($"Trivia of the day: {pushMessage}"); 39 | 40 | var notification = new Notification 41 | { 42 | Title = "Did you know?", 43 | Body = pushMessage, 44 | Tag = NotificationTag.Trivia, 45 | }; 46 | 47 | foreach (var subscription in subscriptions) 48 | { 49 | try { await pushService.Send(subscription.UserId, notification); } 50 | catch (Exception e) 51 | { 52 | throw new Exception($"Failed to send push to user with id: {subscription.UserId}. See inner exception.", e); 53 | } 54 | } 55 | 56 | log.LogInformation("All push notifications were sent successfully. Exciting."); 57 | } 58 | 59 | internal static readonly string[] TriviaList = 60 | { 61 | "Astrology is the study of celestial objects and their position's affect on humans and earth", 62 | "The zodiac is divided into twelve sections with signs that form the celestial sphere", 63 | "Aries is the first astrological sign of the zodiac", 64 | "Aries' symbol is a ram", 65 | "Taurus is the second astrological sign of the zodiac", 66 | "Taurus' symbol is a bull", 67 | "Gemini is the third astrological sign of the zodiac", 68 | "Gemini's symbol is twins", 69 | "Cancer is the fourth astrological sign of the zodiac", 70 | "Cancer's symbol is a crab", 71 | "Leo is the fifth astrological sign of the zodiac", 72 | "Leo's symbol is a lion", 73 | "Virgo is the sixth astrological sign of the zodiac", 74 | "Virgo's symbol is a virgin", 75 | "Libra is the seventh astrological sign of the zodiac", 76 | "Libra's symbol is scales", 77 | "Scorpio is the eighth astrological sign of the zodiac", 78 | "Scorpio's symbol is a scorpion", 79 | "Sagittarius is the ninth astrological sign of the zodiac", 80 | "Sagittarius' symbol is an archer", 81 | "Capricorn is the tenth astrological sign of the zodiac", 82 | "Capricorn's symbol is a goat-fish hybrid", 83 | "Aquarius is the eleventh astrological sign of the zodiac", 84 | "Aquarius' symbol is a water-bearer", 85 | "Pisces is the twelfth astrological sign of the zodiac", 86 | "Pisces' symbol is a fish" 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/PushnotificationsDemoFunction.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | v2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | PreserveNewest 21 | Never 22 | 23 | 24 | -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/Services/IPushService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PushnotificationsDemo.Models; 3 | 4 | namespace PushnotificationsDemo.Services 5 | { 6 | /// 7 | /// Defines a service to manage Push subscriptions and send Push notifications 8 | /// 9 | public interface IPushService 10 | { 11 | /// 12 | /// Checks VAPID info and if invalid generates new keys and throws exception 13 | /// 14 | /// This should be a URL or a 'mailto:' email address 15 | /// The VAPID public key as a base64 encoded string 16 | /// The VAPID private key as a base64 encoded string 17 | void CheckOrGenerateVapidDetails(string subject, string vapidPublicKey, string vapidPrivateKey); 18 | 19 | /// 20 | /// Get the server's saved VAPID public key 21 | /// 22 | /// VAPID public key 23 | string GetVapidPublicKey(); 24 | 25 | /// 26 | /// Register a push subscription (save to the database for later use) 27 | /// 28 | /// push subscription 29 | Task Subscribe(PushSubscription subscription); 30 | 31 | /// 32 | /// Un-register a push subscription (delete from the database) 33 | /// 34 | /// push subscription 35 | Task Unsubscribe(PushSubscription subscription); 36 | 37 | /// 38 | /// Send a plain text push notification to a user without any special option 39 | /// 40 | /// user id the push should be sent to 41 | /// text of the notification 42 | Task Send(string userId, string text); 43 | 44 | /// 45 | /// Send a push notification to a user 46 | /// 47 | /// user id the push should be sent to 48 | /// the notification to be sent 49 | Task Send(string userId, Notification notification); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/Services/PushService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using Microsoft.EntityFrameworkCore; 9 | using PushnotificationsDemo.Models; 10 | using WebPush; 11 | using PushSubscription = PushnotificationsDemo.Models.PushSubscription; 12 | 13 | namespace PushnotificationsDemo.Services 14 | { 15 | /// 16 | public class PushService : IPushService 17 | { 18 | private readonly WebPushClient _client; 19 | private readonly DemoDbContext _context; 20 | private readonly VapidDetails _vapidDetails; 21 | 22 | /// 23 | public PushService(DemoDbContext context, string vapidSubject, string vapidPublicKey, string vapidPrivateKey) 24 | { 25 | _context = context; 26 | _client = new WebPushClient(); 27 | 28 | CheckOrGenerateVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey); 29 | 30 | _vapidDetails = new VapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey); 31 | } 32 | 33 | /// 34 | public PushService(DemoDbContext context, IConfiguration configuration) 35 | { 36 | _context = context; 37 | _client = new WebPushClient(); 38 | 39 | var vapidSubject = configuration.GetValue("Vapid:Subject"); 40 | var vapidPublicKey = configuration.GetValue("Vapid:PublicKey"); 41 | var vapidPrivateKey = configuration.GetValue("Vapid:PrivateKey"); 42 | 43 | CheckOrGenerateVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey); 44 | 45 | _vapidDetails = new VapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey); 46 | } 47 | 48 | /// 49 | public void CheckOrGenerateVapidDetails(string vapidSubject, string vapidPublicKey, string vapidPrivateKey) 50 | { 51 | if (string.IsNullOrEmpty(vapidSubject) || 52 | string.IsNullOrEmpty(vapidPublicKey) || 53 | string.IsNullOrEmpty(vapidPrivateKey)) 54 | { 55 | var vapidKeys = VapidHelper.GenerateVapidKeys(); 56 | 57 | // Prints 2 URL Safe Base64 Encoded Strings 58 | Debug.WriteLine($"Public {vapidKeys.PublicKey}"); 59 | Debug.WriteLine($"Private {vapidKeys.PrivateKey}"); 60 | 61 | throw new Exception( 62 | "You must set the Vapid:Subject, Vapid:PublicKey and Vapid:PrivateKey application settings or pass them to the service in the constructor. You can use the ones just printed to the debug console."); 63 | } 64 | } 65 | 66 | /// 67 | public string GetVapidPublicKey() => _vapidDetails.PublicKey; 68 | 69 | /// 70 | public async Task Subscribe(PushSubscription subscription) 71 | { 72 | if (await _context.PushSubscription.AnyAsync(s => s.P256Dh == subscription.P256Dh)) 73 | return await _context.PushSubscription.FindAsync(subscription.P256Dh); 74 | 75 | await _context.PushSubscription.AddAsync(subscription); 76 | await _context.SaveChangesAsync(); 77 | 78 | return subscription; 79 | } 80 | 81 | /// 82 | public async Task Unsubscribe(PushSubscription subscription) 83 | { 84 | if (!await _context.PushSubscription.AnyAsync(s => s.P256Dh == subscription.P256Dh)) return; 85 | 86 | _context.PushSubscription.Remove(subscription); 87 | await _context.SaveChangesAsync(); 88 | } 89 | 90 | /// 91 | public async Task Send(string userId, Notification notification) 92 | { 93 | foreach (var subscription in await GetUserSubscriptions(userId)) 94 | try 95 | { 96 | _client.SendNotification(subscription.ToWebPushSubscription(), JsonConvert.SerializeObject(notification), _vapidDetails); 97 | } 98 | catch (WebPushException e) 99 | { 100 | if (e.Message == "Subscription no longer valid") 101 | { 102 | _context.PushSubscription.Remove(subscription); 103 | await _context.SaveChangesAsync(); 104 | } 105 | else 106 | { 107 | // Track exception with eg. AppInsights 108 | } 109 | } 110 | } 111 | 112 | /// 113 | public async Task Send(string userId, string text) 114 | { 115 | await Send(userId, new Notification(text)); 116 | } 117 | 118 | /// 119 | /// Loads a list of user subscriptions from the database 120 | /// 121 | /// user id 122 | /// List of subscriptions 123 | private async Task> GetUserSubscriptions(string userId) => 124 | await _context.PushSubscription.Where(s => s.UserId == userId).ToListAsync(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /PushnotificationsDemoFunction/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "Environment": "Production", 5 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 6 | "AzureWebJobsDashboard": "", 7 | "Database": "Server=(localdb)\\mssqllocaldb;Database=PushDemoInMemoryDb;Trusted_Connection=True;ConnectRetryCount=0", 8 | "VapidSubject": "mailto:email@outlook.com", 9 | "VapidPublicKey": "", 10 | "VapidPrivateKey": "", 11 | "WEBSITE_TIME_ZONE": "Pacific Standard Time", 12 | "FUNCTIONS_WORKER_RUNTIME": "dotnet" 13 | } 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Push Notifications Tutorial 2 | 3 | This is a tutorial and demo for web push notifications that work in modern web browsers. Frontend is written in vanilla JavaScript, while the backend is using the ASP.NET Core 2.1 framework. 4 | 5 | > If you are searching the node.js version of this sample, you can find it [here](https://github.com/MicrosoftEdge/pushnotifications-demo). 6 | 7 | ## How to use 8 | 9 | First, install all .NET dependencies via `dotnet restore`. 10 | 11 | This demo uses an in-memory SQL database instance for storing push subscription info to send push updates at some other point in time. It also requires specifying a public and private key for identifying your server to the push service's server. These keys, known as VAPID public/private keys, can be generated and printed to the console when first executing the site. The site can be executed by running `dotnet run` which will start a server on `https://localhost:5001`. You'll need to populate those keys as environment variables and execute `dotnet run` again to ensure that push messages can be configured from your server. 12 | 13 | You should set the environment variables mentioned above in your `appsettings.json` file as follows: 14 | 15 | ```json 16 | { 17 | "Vapid": { 18 | "Subject": "mailto:email@outlook.com", 19 | "PublicKey": "YOUR_PUBLIC_KEY", 20 | "PrivateKey": "YOUR_PRIVATE_KEY" 21 | }, 22 | "ConnectionStrings": { 23 | "Database": "Server=(localdb)\\mssqllocaldb;Database=PushDemoInMemoryDb;Trusted_Connection=True;ConnectRetryCount=0" 24 | }, 25 | "Logging": { 26 | "LogLevel": { 27 | "Default": "Warning" 28 | } 29 | }, 30 | "AllowedHosts": "*" 31 | } 32 | ``` 33 | 34 | ## Key components of the sample 35 | 36 | The following files contain code that's related to generating VAPID keys, registering a push subscription and sending push notifications. 37 | 38 | ### ASP.NET Core backend 39 | 40 | - [`appsettings.json`](/PushnotificationsDemo/appsettings.json) Contains VAPID keys and the database connection string. 41 | - [`Startup.cs`](/PushnotificationsDemo/Startup.cs) Configures the app and the services it uses, including the database connection. 42 | - [`PushController.cs`](/PushnotificationsDemo/Controllers/PushController.cs) Contains the API endpoints. 43 | - [`PushService.cs`](/PushnotificationsDemo/Services/PushService.cs) Contains the Push service which is used to manage saving subscriptions to the database and sending push notifications. 44 | 45 | ### Frontend 46 | 47 | - [`Index.cshtml`](/PushnotificationsDemo/Views/Home/Index.cshtml) Contains the sample's UI. 48 | - [`service-worker.js`](/PushnotificationsDemo/wwwroot/service-worker.js) Contains the sample's service worker which gets registered and will manage the incoming push notifications. 49 | - [`script.js`](/PushnotificationsDemo/wwwroot/js/script.js) Runs after DOM is loaded and contains methods for service worker and push subscription registration. 50 | - [`util.js`](/PushnotificationsDemo/wwwroot/js/util.js) Contains methods for push subscription management. 51 | 52 | ## Running the Azure Function App 53 | 54 | The service which is sending the periodic push notifications (7AM Pacific Standard Time every day) is using Azure Funtions to run periodically. You can run it locally by calling `func host start` in the `PushnotificationsDemoFuntion` folder. You need to create a copy of `settings.json` with the name of `local.settings.json` and fill the VAPID keys. 55 | 56 | If you want to run the Function App more frequently for debugging for example, you can use something like this: `0,15,30,45 * * * * *`. This will run every 15 seconds. 57 | 58 | ## Contributing 59 | 60 | If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). 61 | 62 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 63 | 64 | ## Questions and comments 65 | 66 | We'd love to get your feedback about the Microsoft Graph Connect Sample for ASP.NET Core. You can send your questions and suggestions to us in the [Issues](https://github.com/MicrosoftEdge/pushnotifications-demo-aspnetcore/issues) section of this repository. 67 | 68 | Questions about Microsoft Edge in general should be posted to [Stack Overflow](https://stackoverflow.com/questions/tagged/microsoft-edge). Make sure that your questions or comments are tagged with _[microsoft-edge]_. 69 | 70 | You can suggest changes for Microsoft Edge on [UserVoice](https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer). 71 | 72 | ## Copyright 73 | 74 | Copyright (c) 2018 Microsoft. All rights reserved. 75 | --------------------------------------------------------------------------------