├── .eslintrc.json
├── .gitignore
├── README.md
├── globals.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── arrow-down.svg
├── arrow-up.svg
├── bell.png
├── bell2.png
├── clipboard.svg
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── ios_step_1.jpg
├── ios_step_2.jpg
├── ios_step_3.jpg
├── ios_step_4.jpg
├── logo.svg
├── manifest.json
├── mstile-144x144.png
├── mstile-150x150.png
├── mstile-310x150.png
├── mstile-310x310.png
├── mstile-70x70.png
├── next.svg
├── rocket.svg
├── sharing-image.png
├── sw.js
└── vercel.svg
├── src
├── components
│ ├── button.tsx
│ ├── content-wrapper.tsx
│ ├── disclaimer.tsx
│ ├── error-diagnostics.tsx
│ ├── footer.tsx
│ ├── info.tsx
│ ├── ios-instructional-static.tsx
│ ├── ios-instructional-video.tsx
│ ├── links.tsx
│ ├── post-subscribe-actions.tsx
│ ├── seo-text.tsx
│ ├── subscriber.tsx
│ ├── topic-subscriber-wrapper.tsx
│ └── topic-subscriber.tsx
├── constants
│ └── topics.ts
├── hooks
│ └── useDeviceInfo.tsx
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── api
│ │ ├── cron
│ │ │ ├── hn_top_new.ts
│ │ │ └── hn_top_story.ts
│ │ ├── hn_random.ts
│ │ ├── hn_top_new.ts
│ │ ├── hn_top_story.ts
│ │ └── welcome.ts
│ └── index.tsx
├── services
│ ├── magicBell.ts
│ └── subscriptionManager.ts
├── styles
│ └── globals.css
└── utils
│ └── minVersionCheck.ts
├── tailwind.config.js
├── tsconfig.json
└── youtube.html
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "react/no-unescaped-entities": "off",
5 | "@next/next/no-page-custom-font": "off"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 | .env
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | vercel.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # About this project
2 |
3 | Apple released [beta](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados) support for Web Push notifications on iOS in February 2023, and made it official with the release of iOS 16.5 in [May 2023](https://www.macrumors.com/2023/05/09/apple-confirms-ios-16-5-release-date/).
4 |
5 | This project, hosted at https://webpushtest.com, showcases these new capabilities. In addition to iOS, it can also send Web Push notifications on desktop and Android (although this is somewhat old news).
6 |
7 | It is targeted at the end user, for whom installing a PWA and receiving a Web Push notification will likely be a new experience. As such, we have tried to include instructional information and device-specific error messages where relevant. Take a look at this Youtube short
8 |
9 | [](https://www.youtube.com/watch?v=aIlGLE_adzc)
10 |
11 | You can use the code for providing push notification support in your web/PWA apps, but you will need an API key from [MagicBell](https://www.magicbell.com). We offer a generous free tier so you can get started quickly. Apart from web-push, we offer a real-time in-app notification inbox you can add to your app in minutes.
12 |
13 | Relevant links:
14 |
15 | - [iOS now supports push notifications (and why you should care)](https://www.magicbell.com/blog/ios-now-supports-web-push-notifications-and-why-you-should-care)
16 | - [Twitter thread](https://twitter.com/Matt0xley/status/1668912123702030336)
17 | - If you'd like help, please start a [GitHub Discussion|(https://github.com/orgs/magicbell/discussions).
18 |
19 | ## Running locally
20 |
21 | First, install dependencies:
22 |
23 | ```bash
24 | npm install
25 | ```
26 |
27 | Then, assuming you have a MagicBell account and have created a new project, obtain the NEXT_PUBLIC_MAGICBELL_API_KEY and MAGICBELL_API_SECRET from the [MagicBell](https://www.magicbell.com/) dashboard and set them as environment variables in a `.env` file at the root of this project:
28 |
29 | ```bash
30 | NEXT_PUBLIC_MAGICBELL_API_KEY=...
31 | MAGICBELL_API_SECRET=...
32 | ```
33 |
34 | Then, start the development server:
35 |
36 | ```bash
37 | npm run dev
38 | ```
39 |
40 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
41 |
42 | To observe iOS push notifications from your local development environment, you will need to expose your local server to the internet. We recommend using [ngrok](https://ngrok.com/):
43 |
44 | ```bash
45 | ngrok http 3000
46 | ```
47 |
48 | After visiting the resulting public url on your device, be sure to also install the app as a PWA, using the "Add to Home Screen" option in the Safari share menu.
49 |
50 | ## Important files
51 |
52 | These will be the most relevant files to look at if you want to understand how this project works:
53 |
54 | - https://github.com/magicbell/webpush-test/blob/main/src/components/subscriber.tsx
55 | - https://github.com/magicbell/webpush-test/blob/main/src/services/subscriptionManager.ts
56 | - https://github.com/magicbell/webpush-test/blob/main/public/manifest.json
57 | - https://github.com/magicbell/webpush-test/blob/main/public/sw.js
58 |
--------------------------------------------------------------------------------
/globals.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace NodeJS {
3 | interface ProcessEnv {
4 | NEXT_PUBLIC_MAGICBELL_API_KEY: string
5 | MAGICBELL_API_SECRET: string
6 | }
7 | }
8 | }
9 |
10 | export {}
11 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | typescript: {
5 | // !! WARN !!
6 | // Dangerously allow production builds to successfully complete even if
7 | // your project has type errors.
8 | // !! WARN !!
9 | ignoreBuildErrors: true,
10 | },
11 | }
12 |
13 | module.exports = nextConfig
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "push-test",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@cloudflare/stream-react": "^1.8.0",
13 | "@magicbell/magicbell-react": "^10.7.3",
14 | "@magicbell/react-headless": "^4.2.7",
15 | "@magicbell/webpush": "^1.2.0",
16 | "@radix-ui/react-collapsible": "^1.0.3",
17 | "@radix-ui/react-toast": "^1.1.4",
18 | "@storybook/react": "^7.0.18",
19 | "@types/node": "20.2.5",
20 | "@types/react": "18.2.8",
21 | "@types/react-dom": "18.2.4",
22 | "@vercel/analytics": "^1.0.1",
23 | "autoprefixer": "10.4.14",
24 | "detectincognitojs": "^1.3.0",
25 | "eslint": "8.42.0",
26 | "eslint-config-next": "13.4.4",
27 | "firebase": "^9.22.2",
28 | "magicbell": "^1.7.2",
29 | "next": "13.4.4",
30 | "postcss": "8.4.24",
31 | "react": "18.2.0",
32 | "react-device-detect": "^2.2.3",
33 | "react-dom": "18.2.0",
34 | "tailwindcss": "3.3.2",
35 | "typescript": "5.1.3",
36 | "uuid": "^9.0.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/arrow-up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/bell.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/bell.png
--------------------------------------------------------------------------------
/public/bell2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/bell2.png
--------------------------------------------------------------------------------
/public/clipboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/favicon.ico
--------------------------------------------------------------------------------
/public/ios_step_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/ios_step_1.jpg
--------------------------------------------------------------------------------
/public/ios_step_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/ios_step_2.jpg
--------------------------------------------------------------------------------
/public/ios_step_3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/ios_step_3.jpg
--------------------------------------------------------------------------------
/public/ios_step_4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/ios_step_4.jpg
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Web Push Test",
3 | "short_name": "WebPushTest",
4 | "start_url": "/",
5 | "display": "standalone",
6 | "background_color": "#90cdf4",
7 | "theme_color": "#90cdf4",
8 | "orientation": "portrait-primary",
9 | "icons": [
10 | {
11 | "src": "bell2.png",
12 | "sizes": "500x500",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "bell.png",
17 | "sizes": "192x192",
18 | "type": "image/png"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/public/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/mstile-144x144.png
--------------------------------------------------------------------------------
/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/mstile-150x150.png
--------------------------------------------------------------------------------
/public/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/mstile-310x150.png
--------------------------------------------------------------------------------
/public/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/mstile-310x310.png
--------------------------------------------------------------------------------
/public/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/mstile-70x70.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/rocket.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/sharing-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/magicbell/webpush-ios-template/f0e73e33d89b4e8bcf71a2727bc829c26e1bafc5/public/sharing-image.png
--------------------------------------------------------------------------------
/public/sw.js:
--------------------------------------------------------------------------------
1 | importScripts("https://assets.magicbell.io/web-push-notifications/sw.js")
2 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/button.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import Image from "next/image"
3 |
4 | export default function Button(props: {
5 | text: string
6 | classname: string
7 | disabled: boolean
8 | onClick?: () => void
9 | loading?: true
10 | }) {
11 | const [hovered, setHovered] = useState(false)
12 | return (
13 | <>
14 |
25 |
40 | >
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/content-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import React from "react"
3 | import Info from "./info"
4 | import useDeviceInfo, { DeviceInfo } from "@/hooks/useDeviceInfo"
5 |
6 | export default function ContentWrapper(props: {
7 | children: React.ReactNode
8 | message: string
9 | }) {
10 | return (
11 |
12 |
{props.message}
13 | {props.children}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/disclaimer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import Image from "next/image"
3 |
4 | export const magicBellHandle = "magicbell_io"
5 |
6 | export default function Disclaimer() {
7 | return (
8 |
9 |
14 | built by
15 |
21 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/error-diagnostics.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react"
2 | import va from "@vercel/analytics"
3 |
4 | import IosInstructionalVideo from "./ios-instructional-video"
5 | import useDeviceInfo, { DeviceInfo } from "@/hooks/useDeviceInfo"
6 | import { clientSettings } from "@magicbell/react-headless"
7 | import minVersionCheck from "@/utils/minVersionCheck"
8 |
9 | /**
10 | * Here we show the user some diagnostics to help them troubleshoot
11 | */
12 |
13 | export default function ErrorDiagnostics(props: { error: string }) {
14 | const info = useDeviceInfo()
15 | useEffect(() => {
16 | va.track("error", {
17 | ...info,
18 | error: props.error,
19 | id: clientSettings.getState().userExternalId as string, // TODO: fix typing here
20 | })
21 | }, [info, props.error])
22 | function getContent() {
23 | if (!info) {
24 | return null
25 | }
26 | switch (info.deviceType) {
27 | case "browser":
28 | switch (info.notificationApiPermissionStatus) {
29 | case "denied": {
30 | if (info.isPrivate) {
31 | return (
32 |
33 | {`It looks like you are browsing in private mode in ${info.browserName}. Unfortunately, this mode does not support notifications. Please try again in a non-private window.`}
34 |
35 | )
36 | }
37 | return (
38 |
39 | {`It looks like you denied notification permissions for WebPushTest.com in ${info.browserName}. To receive push notifications, please permit us to notify you.`}
40 |
68 | {`It looks like you denied notification permissions for WebPushTest.com in ${info.browserName}. To receive push notifications, please permit us to notify you.`}
69 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | >
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/ios-instructional-video.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Stream } from "@cloudflare/stream-react"
3 |
4 | const width = 260
5 |
6 | type Props =
7 | | {
8 | withCaption: true
9 | captionText?: string
10 | }
11 | | {
12 | withCaption: false
13 | }
14 |
15 | export default function IosInstructionalVideo(props: Props) {
16 | return (
17 |
18 | {
19 |
20 | {props.withCaption
21 | ? props.captionText ||
22 | `If running this demo on iOS, ensure you are using 16.5 or later, and
23 | have this PWA "installed"; with Safari (installation
24 | instructions below`
25 | : null}
26 |
27 | }
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/links.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | export default function Links() {
4 | return (
5 |
11 |
16 |
29 | View source on Github
30 |
31 |
36 |
50 | A thread about web-push on iOS 16.5
51 |
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/post-subscribe-actions.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import Button from "./button"
3 | import TopicSubscriberWrapper from "./topic-subscriber-wrapper"
4 | import { topics } from "@/constants/topics"
5 | import magicBell from "@/services/magicBell"
6 |
7 | /**
8 | * This component shows once the user has successfully subscribed to Webpush.
9 | * Here they have the option to send more test notifications, or subscribe to granular topics.
10 | */
11 |
12 | interface IProps {
13 | interactive: boolean
14 | onAfterInteract: () => void
15 | onError: (message: string) => void
16 | }
17 |
18 | export default function PostSubscribeActions(props: IProps) {
19 | const handleResend = async () => {
20 | try {
21 | await magicBell.sendNotification("hn_random")
22 | } catch (error: any) {
23 | props.onError(error.message)
24 | }
25 | props.onAfterInteract()
26 | }
27 |
28 | return (
29 | <>
30 | {props.interactive ? (
31 |
37 | ) : (
38 |
44 | )}
45 |
OR
46 | (
48 |
49 | {" "}
50 | Now that you have webpush successfully enabled, subscribe to any of
51 | the{" "}
52 |
57 | HackerNews
58 | {" "}
59 | feeds above and receive a relevant notification about top posts
60 | every 6 hours. Unsubscribe at any time.
61 |
62 | )}
63 | topics={Object.values(topics)}
64 | interactive={props.interactive}
65 | onAfterInteract={props.onAfterInteract}
66 | />
67 | >
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/seo-text.tsx:
--------------------------------------------------------------------------------
1 | export default function SeoText() {
2 | return (
3 |
7 |
11 | About The Web Push Notifications Demo
12 |
13 |
14 |
15 | This is a demo of standards based web-push notifications on all
16 | platforms, including iOS. We don't use any personal information (not
17 | even Device ID), and utilize the Hacker News dataset to demo one-off
18 | or scheduled notifications. The code is open source and we encourage
19 | you to use it for your product! If you are looking for a hosted
20 | solution, you will love our product{" "}
21 | MagicBell!
22 |
49 | This web push notifications demo requires a non-private browser
50 | window, since the{" "}
51 |
56 | Notification API
57 | {" "}
58 | is set to "denied" by default.
59 |
109 | You should soon receive a notification on your device.
110 |
111 |
112 | If not, first try checking your browser notification settings at
113 | the operating system level (it is possible that notifications are
114 | muted for your current browser).
115 |
116 |
117 | If this does not explain it, we would love it if you could tag us{" "}
118 |
123 | @magicbell_io
124 |
125 | , with reference to your device settings displayed below.
126 |