### **PR Type** Enhancement ___ ### **Description** - Add cross-framework Todos CRUD examples - Introduce file upload/download tutorials - Provide unified AuthProvider context implementations - Include email templates and backend actions ___ <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>19 files</summary><table> <tr> <td><strong>Todos.tsx</strong><dd><code>Add web demo Todos CRUD page</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-53f3b3d582fef21d5ec90cb590b73afcf09407071dba60883ed1ed7360955fc5">+648/-0</a> </td> </tr> <tr> <td><strong>todos.tsx</strong><dd><code>Add React Native tutorial Todos</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-3f6be5ef4f443091687addd404fe71f219498b9db7dea992d18d78b4f1b6ffa3">+561/-0</a> </td> </tr> <tr> <td><strong>commonStyles.ts</strong><dd><code>Add common React Native tutorial styles</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-bf0de16179ecc80a8e88e223c890dc2c73c30b4a9b7cadd62e910ca015ce342b">+667/-0</a> </td> </tr> <tr> <td><strong>Todos.tsx</strong><dd><code>Add React tutorial Todos page</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-be700e4847b0a745821f156c381e583097f2083123065a45a20611c2ba1876a7">+504/-0</a> </td> </tr> <tr> <td><strong>files.tsx</strong><dd><code>Add React Native tutorial file upload</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-d116805053a943f271a3297d06d14dba39c0f5775080e67e1e9e2778c176e9da">+454/-0</a> </td> </tr> <tr> <td><strong>Files.tsx</strong><dd><code>Add React tutorial Files page</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-cdbca4ed68690df84463df7765dff52c85a60502f175c19519c8b42474e9282c">+404/-0</a> </td> </tr> <tr> <td><strong>signup.tsx</strong><dd><code>Enhance signup flow with email verification</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-262b09b9dd7234cad96fe092d7131a23451c9e50b98c126c9e36599b3a127ac6">+185/-141</a></td> </tr> <tr> <td><strong>FilesClient.tsx</strong><dd><code>Add Next.js Files client component</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-ea74386f232b9ae7e7957ab4eb1f0d1d6076b338173e8b1e917369fb7f1b39bb">+359/-0</a> </td> </tr> <tr> <td><strong>TodosClient.tsx</strong><dd><code>Add Next.js Todos client component</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-96efb4db6bd61a7faee3c383e77b092eccee1a1876770c36691e7356268cfad4">+368/-0</a> </td> </tr> <tr> <td><strong>AuthProvider.tsx</strong><dd><code>Implement React AuthProvider context</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-4cc7e41420d71d448eeec4f77043e0c5bff2c606986439454dade5ffcd433e33">+174/-0</a> </td> </tr> <tr> <td><strong>AuthProvider.tsx</strong><dd><code>Implement RN AuthProvider context</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-93f9f27a35d0039a64cd6889a296d04d37542aa5a777925e61e8e60ee5a6d744">+148/-0</a> </td> </tr> <tr> <td><strong>actions.ts</strong><dd><code>Add Next.js server actions for Todos</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-76d57097940d3043c8e0ab29761767861b78fe86ab8a90a2d8209f1818131d31">+223/-0</a> </td> </tr> <tr> <td><strong>signup.tsx</strong><dd><code>Add RN signup tutorial screen</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-153c2f0bf6b3744bb84b95e356dd78c8771206a1b22218bc4c6f90641e4143ad">+183/-0</a> </td> </tr> <tr> <td><strong>explore.tsx</strong><dd><code>Add RN tutorial Explore screen</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-980313b6c7f75f2ecd45fc476895bea122364d000f5988e21564bf5db73d7f57">+125/-0</a> </td> </tr> <tr> <td><strong>route.ts</strong><dd><code>Add Next.js file download route</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-5c39cd02d478ad625a7cdb6df3f7b6d20a76f40488636fdd87282c66174b2bd8">+57/-0</a> </td> </tr> <tr> <td><strong>SignUp.tsx</strong><dd><code>Enhance demo signup with verification state</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-75657efa0e1c29f59692ced3cd90e9c734836977900dc64015dd5d217bb263da">+38/-3</a> </td> </tr> <tr> <td><strong>SignUpForm.tsx</strong><dd><code>Add Next.js signup client form</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-9034cd412c47033a01e604a8250984aa1d1ecefc9884b79ebd7f7f3af17e3167">+89/-0</a> </td> </tr> <tr> <td><strong>SignInForm.tsx</strong><dd><code>Add Next.js signin client form</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-c276aadee4507d515832909164d447c3c1a4870277d0adbd1f7e836f7c66259e">+75/-0</a> </td> </tr> <tr> <td><strong>server.tsx</strong><dd><code>Add server Nhost client for Next.js</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-9d650defc223584e4aa06ddbf1d9a97c47a5a7ec4c9589a72ac7ea5369853400">+89/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Configuration changes</strong></td><td><details><summary>1 files</summary><table> <tr> <td><strong>_layout.tsx</strong><dd><code>Configure RN tutorial tab navigation</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-c3b0bac088aef9f1a2d5cd2f4b51dd19fb301034668a062860a1e6a3512c15c9">+39/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>101 files</summary><table> <tr> <td><strong>examples_tutorials_checks.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-745712dc409289d30fe4caa74de567df9cc9fac97750ae5566f93a51e48bf539">+94/-0</a> </td> </tr> <tr> <td><strong>docs.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-873ce17c654718debe2fe308a2f2279bde8663686423c51f97fab2dd0722b8d9">+52/-7</a> </td> </tr> <tr> <td><strong>overview.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-8c9b35da559a5de5fe14ee078573e8d487453e26ed760c03ffd7f0ad476ca24d">+5/-5</a> </td> </tr> <tr> <td><strong>nextjs.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-a5210d45e7d33a57d43078dbe2a2ccbf0667b157291fd92c3986092d7d33ab9c">+0/-508</a> </td> </tr> <tr> <td><strong>1-introduction.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-35413705a524a3eb2c7d096daef02d660b479fbc288674aa260e3f159988652b">+116/-0</a> </td> </tr> <tr> <td><strong>2-protected-routes.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-d901b3a6d8a96e3070f27afb934c34365aa79aeee1505238de2cb77a9ffd8546">+1364/-0</a></td> </tr> <tr> <td><strong>3-user-authentication.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-7b96576ad1b1b6b8e50dd3889e24df391662df1b7b51a111f0239f655135939f">+804/-0</a> </td> </tr> <tr> <td><strong>4-graphql-operations.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-16906637a292d22c018167d45f2da67a17fe73155ca161de91c3b329fee9d399">+958/-0</a> </td> </tr> <tr> <td><strong>5-file-uploads.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-d6153781dbf251499f2ffeeb707102349776d788859c2da0f63c8b0a8e35c821">+822/-0</a> </td> </tr> <tr> <td><strong>react.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-6f5adda9f7b29d98c68cab6ec754c0bac501666a49dd635ee830789e2c812b68">+0/-497</a> </td> </tr> <tr> <td><strong>1-introduction.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-4dac916b2b27af668674965d9680f4ba8d2a417f7b155d4b052b1043fc71beb6">+116/-0</a> </td> </tr> <tr> <td><strong>2-protected-routes.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-8ab0206ae4a4c73e540cf549fdbdd9de4ed7c8d59cc88ea26a4a7ddc0a9a4460">+1435/-0</a></td> </tr> <tr> <td><strong>3-user-authentication.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-e02466ba1abd5d457724c7508da083a94b16d700cab36384567592c7d772cbf6">+647/-0</a> </td> </tr> <tr> <td><strong>4-graphql-operations.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-ae759670f7bf513c66bc08952556514e7193f23eb7cc536aebf38ad79e12c02f">+856/-0</a> </td> </tr> <tr> <td><strong>5-file-uploads.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-02dc5fa1da67b0478bb797ed99c89685be3ec4509023441fb29b730ba74fff4e">+715/-0</a> </td> </tr> <tr> <td><strong>reactnative.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-a85b3aa1caaeaa0a2cb6219ca531f89aaf2b23b41cd424c189b3cb948af1fd57">+0/-443</a> </td> </tr> <tr> <td><strong>1-introduction.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-573f696fb008f0e047e97209a1595b1f5c69dd5a1fe4907b65d6883273c53096">+121/-0</a> </td> </tr> <tr> <td><strong>2-protected-routes.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-b4ab0ba037b62e9eb5ee10cf5f56fd1c148f1e053f95291e42113d9f924ae711">+1499/-0</a></td> </tr> <tr> <td><strong>3-user-authentication.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-dda89bc27be58e7fa58aef9aeb937dce455f69d9566e734a10ef4e0df993ab42">+805/-0</a> </td> </tr> <tr> <td><strong>4-graphql-operations.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-59a46fa74310dc797cdfd6e1a34d09fd8002e9accdade9d865b898419addc2bc">+881/-0</a> </td> </tr> <tr> <td><strong>5-file-uploads.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-cb48bca7fdd422250b2482167b9a136d76ab60d2d5efccb94fb1a022b814d449">+776/-0</a> </td> </tr> <tr> <td><strong>6-sign-in-with-apple.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-a4da2a574e88122505df17fcf1f9705841a8240b33f60b6fcde86e068b8b6cfe">+684/-0</a> </td> </tr> <tr> <td><strong>1-introduction.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-1eb8289a9ffc0484d2f8f21ede882c9e892d169f36b69f2ed9af940e2d1b8faa">+116/-0</a> </td> </tr> <tr> <td><strong>2-protected-routes.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-bff6efc64eeda0dae0d2a133ab6c9be9dfe725ee24b364b89060ec93344ad436">+1308/-0</a></td> </tr> <tr> <td><strong>3-user-authentication.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-f6bac1bb9476f921272f61911c703529663fb6ff37217c38d1702a1c8b70171a">+578/-0</a> </td> </tr> <tr> <td><strong>4-graphql-operations.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-3a43d1ebc0b4fd7f74699c4f33ea110bba5ffa2695e6228dccafbfa38798c633">+763/-0</a> </td> </tr> <tr> <td><strong>5-file-uploads.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-2244e838acdbe909772f82f66cc63800aa9dbf5fae292a40d2d4f663d6618dd4">+642/-0</a> </td> </tr> <tr> <td><strong>sveltekit.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-9ed4ad735d1142a65a2da2dbbd8d46c508b7aef3d032cdb102d0f329602c4ce1">+0/-497</a> </td> </tr> <tr> <td><strong>vue.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-f6c4215fa6909fd3accebe0691a7364d17befb8ef90da5a4aeaee83d598c0540">+0/-504</a> </td> </tr> <tr> <td><strong>1-introduction.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-9485f8a6ad6398b19aa7a3739af3eef5158a7d285dc5a55e8bc6c1d58970fb84">+116/-0</a> </td> </tr> <tr> <td><strong>2-protected-routes.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-5ba36ef4388ad18b7efc6d405e2b66a69b063d483d4521d6acb5b5da65aee97a">+1368/-0</a></td> </tr> <tr> <td><strong>3-user-authentication.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-63f56a07c48f78462b47f74420e58cb8411d46aef880cfdf24b11ddea4bb382e">+637/-0</a> </td> </tr> <tr> <td><strong>4-graphql-operations.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-ad7a84d0c924b946d992f5277cde596fb9d42eff021b888b5a8046822afabca8">+828/-0</a> </td> </tr> <tr> <td><strong>5-file-uploads.mdx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-00c26651dea4650fd77222a24b31387569067783a87d9c6715935e2bf133428e">+720/-0</a> </td> </tr> <tr> <td><strong>auth_user_security_keys.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-385036f3c18b6efd97138842e95f992a0569f612f627e71091c10952d8d31609">+2/-11</a> </td> </tr> <tr> <td><strong>auth_users.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-aba47a0c058f0a0a8122dee4d7b4394c4a8997bac9d4ccea04c41f0d00819050">+0/-24</a> </td> </tr> <tr> <td><strong>storage_files.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-ea0619f75a1f7dd13ca80d81cc2e28529594a73fff0356dc0b1d49ee0d1cc9ad">+17/-36</a> </td> </tr> <tr> <td><strong>tables.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-a57c35142b6c4029fad7d5c8407305a61d18078cd0e264d41286625cf5cc4d30">+1/-5</a> </td> </tr> <tr> <td><strong>down.sql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-d7be59389936d96f1c3b10f2147b177d488906d2155139536fae635201859c3e">+1/-0</a> </td> </tr> <tr> <td><strong>up.sql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-69c6c175ba9819f7b41d6a27e78fd0390e222665c9cc95fbb7fde702f14a1f4c">+26/-0</a> </td> </tr> <tr> <td><strong>nhost.toml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-45587ca86b6441ceacd1318e72a60ee0c30c70c00edb0a06cf061e404998f3bc">+4/-4</a> </td> </tr> <tr> <td><strong>actions.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-a0279ec5849eb0c93ec7c2f444fbecd2b681f47d954a84da5221f72d9805c2ca">+12/-0</a> </td> </tr> <tr> <td><strong>page.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-8fffbc65277c93c49eea289ae1174cdba0632e5a295b74668437e9b1b0669097">+61/-30</a> </td> </tr> <tr> <td><strong>middleware.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-817666e0f29fa78d2d7f77ed93cff4efbc8d85a996ab58d0dd13ad61e47c9125">+5/-3</a> </td> </tr> <tr> <td><strong>App.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-849f3aa52970f348de49a27094aac4e4b8cb8cf29580cada70d37f1a04249725">+2/-0</a> </td> </tr> <tr> <td><strong>Navigation.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-9845c755953914963f404a14104d93cd326f78e48614ece8c4d9df3ab1600ffd">+3/-0</a> </td> </tr> <tr> <td><strong>index.css</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-8e415aa656412ff29b45f56035e84691f3abcc924bfb5b4e3cd0c9ba5237543b">+200/-0</a> </td> </tr> <tr> <td><strong>+page.svelte</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-00da2a0f738ffa5c66a07437c7e85cacb7780551bad7e81899d905f065bf0892">+29/-8</a> </td> </tr> <tr> <td><strong>main.css</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-1d2ba653024753e96f2db949f818dccba45498456146171c4a2f883b58177d2f">+373/-0</a> </td> </tr> <tr> <td><strong>Navigation.vue</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-27e2f662a54c47023a1342005bcf92602a846ad7a59e9d7d528aa8fbf40a0250">+7/-0</a> </td> </tr> <tr> <td><strong>index.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-3bf427bc13a501168d321cf4bd428d09d8c3a042ef3be955c7a2e99bbf5f39dd">+7/-0</a> </td> </tr> <tr> <td><strong>SignUp.vue</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-1838012876d16508d53d0345615b91c7c6fbccca3c9333d2311ac5fcd7b4c210">+28/-3</a> </td> </tr> <tr> <td><strong>Todos.vue</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-3334950776983589e7061d69be930b71663329a6ba56726bcbd3f1c2352dab47">+466/-0</a> </td> </tr> <tr> <td><strong>Makefile</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-85a3083c78e211e9eb36d741342bcbc85a1a0c375060f45c5426b560196de27f">+2/-2</a> </td> </tr> <tr> <td><strong>.secrets.example</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-be7a988f18be877344f8584befc094ec2e66fe7784a2007300053707ad8ec7f1">+16/-0</a> </td> </tr> <tr> <td><strong>Makefile</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-36623bab6fe16382fd3e61b06b9586f2b14bea7c1b492e50db14ea98935016a4">+7/-0</a> </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-ed02e93d01774c3796335c83badc90cbc8a2035040965031946b69c8d91db3dd">+29/-0</a> </td> </tr> <tr> <td><strong>env-up.sh</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-b6846a8b7ff5eea35ba5ff2aac8773f526c118ef59712a750f69578f25afda54">+8/-0</a> </td> </tr> <tr> <td><strong>package.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-f0d63d57cf77fdb7f2526d10c2d32d51ed2b6d43e3495d3f8879499e0c04f04b">+13/-0</a> </td> </tr> <tr> <td><strong>tsconfig.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-cad0e83f030efa9fcad494c7797838c5099909f2c2ae0de0210812b966ffa6db">+11/-0</a> </td> </tr> <tr> <td><strong>config.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-997042f08b088725275a28a7e2d275b86e2e74f3c972800e8116785d5d0fea59">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-3c921023f82fc74f5b657cb92bc8906ea19d7eb8f746713e4fae2a5a894a8a59">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-5f67fb54ebc23bfccd1e23fbb7dc955bad8ad9011fae19369c87bf3f4d3e58fa">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-3c21997bc2db49d5714afc007f214316d17eb69d8ea70dd1cfe558bdb47df4bf">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-e3ab8737d4a9ea68a115f28ce578b7502107f68212d92f035bb86178ccdfb6fe">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-ed34cbd1c3e612ac3124ccce382b84ea7cda7eb22051b07102b68f5c28c58e97">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-b8cead9967aaddc7c52be376fcd54b27c6ec223445e0063729c7255ab6c4f5fc">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-295cddc1774338f5b6ba396ef5ca9a0832b9b2346703ec22abf3b3874f903cc2">+43/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-de3e6c264684c0dddb2f6e70dcb236fe8cb1a27789c01a11b2a707c110ce7fc2">+1/-0</a> </td> </tr> <tr> <td><strong>body.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-ae93f5f97d52d1d42be91b50e2a44522aef884065559f993f781f1ebeaa89b66">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-13511e1d49a2796e2ee0bb2082f02ed59c49bef7f7533735e60ac62ad16cc2c6">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-293b460447946e2de5e4ba74cf64895a4f6fff7d879bb2e555c46ad7edd857a8">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-843300abe10319ac2b6ccffdd8daf3445b8eaa839486acb95566d398cad4e053">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-8ff22c43814b945c0a22ea9089755a144e54039bdd12a2f06a128b4b155b3e38">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-f9e07ca4b5b9ae54e1668c3dfb8ac32612df366eb7d44819163f17c792507295">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-4f829f73c5bce9bcbb67ed1357a111cdf9974c36d2db22493067d201b35f4cdf">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-64f6273ffcec137bbad9a104cd46e6a8776915a804bb3f89c24701071cbb2c50">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-e19bb81615465996e46da50eacac7fddf71dd8bb18200c410b1837575bd76f95">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-a202edc97b202ec41b83decb7b358a6beb89f3edef46194b576ecaada51f073d">+43/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-c2338d16f093d53b8a86b9a859c7bef0d9ef2c83a762d46cd49ab9e2f1f2a773">+1/-0</a> </td> </tr> <tr> <td><strong>body.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-7fff5a89330dd91d3c9781e37d8f1e1f48bfc58c686b463b7fb1f43a2d01813d">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-c9031a1f7ac5ad3d0c3c94cff2f563560c1e3c05d1acc00353e603f3dcac439e">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-847d33f3faf4a7f928445eb389ba8aa24901b9025d4bae0c38becd5e9365a881">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-3f5c827ff0c5a3270dc4c7737b0afd09327800104cb379ed2a556029eb58aef3">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-e499fcf53b46507a64be23ae309eba07e2c3ee289be4de316bd8ee83c16ea42e">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-5a6337adab234155f13e6b7d63eac5c654d5c75f4ea207c9db16b67685ce3e43">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-b8f9c01ea13429ca9990a95e81ac6bedb273ec41198f34903584e9a68c22c6ef">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-5bfe4c29aee11546871f9127b5c5a954f313c64d2344cd4e6cd402fe22cf0ded">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-262bf98a3f4801aff364e19a4c00bf37197764454b274faf1dbd947506b843a7">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-8a4ad52e5c3cd679ede51f7d411ca5aaf89f2f519f49c540e09408bf13823323">+43/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-46f8a60e052ff7b9b3d8583f83f36b826aea9c7a24af9e9998cc0b699900b3b1">+1/-0</a> </td> </tr> <tr> <td><strong>body.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-03da104febe035cf096fccbce0dbbf32197d289de814810792a319154241200c">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-d7fff652e5e21d234b1ab733de23d179d5901ff82165ffbd9495b3a1ab1c7611">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-a84317c8ffe3b56140c923228666555d0e9e93c34eb5a8376b08ab97e6e4ae96">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-5025ef2a43d19c4f668177eea4e8002664a22522e11fef04aabbcdb2fbff3178">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-e941c399a4d0b15de5bf2b20e722c2e6ffc990ecdef15efc2367ae302a52ff63">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-126da45f26e2d9256a28fadd38f363277926de563e6b303027cbea24779c729c">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-4686c2e7f183e071c8ccd456189d9bd056ead4874078dba16b1a2b1e53c7e9e4">+1/-0</a> </td> </tr> <tr> <td><strong>body.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-331385785301bc1e55cfb6a3c707e3704b32d82a38f7ccd9aa4aa834a8569df4">+52/-0</a> </td> </tr> <tr> <td><strong>subject.txt</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-57d1b1020e0d2e327acca4154b0e4776f3a7a5db1d6c99edae3ee45561780c6a">+1/-0</a> </td> </tr> <tr> <td><strong>Additional files not shown</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3469/files#diff-2f328e4cd8dbe3ad193e49d92bcf045f47a6b72b1e9487d366f6b8288589b4ca"></a></td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___
685 lines
21 KiB
Plaintext
685 lines
21 KiB
Plaintext
---
|
|
title: Sign in with Apple in React Native
|
|
description: Learn how to implement Sign in with Apple authentication in your React Native app using Nhost Auth with proper configuration and native integration
|
|
sidebarTitle: "Sign in with Apple"
|
|
icon: apple
|
|
---
|
|
|
|
This tutorial extends the authentication system built in the previous parts by adding Sign in with Apple functionality. You'll learn how to configure Apple Sign In in the Nhost dashboard, implement the native iOS integration, and handle the authentication flow in your React Native application.
|
|
|
|
<Info>
|
|
This is **Part 6** in the Full-Stack React Native Development with Nhost series. This part focuses on integrating Apple's native authentication system with your existing Nhost authentication flow.
|
|
</Info>
|
|
|
|
## Full-Stack React Native Development with Nhost
|
|
|
|
<CardGroup cols={3}>
|
|
<Card title="1. Create Project" icon="plus" href="/getting-started/tutorials/reactnative/1-introduction">
|
|
Set up your Nhost project
|
|
</Card>
|
|
|
|
<Card title="2. Protected Routes" icon="lock" href="/getting-started/tutorials/reactnative/2-protected-routes">
|
|
Route protection basics
|
|
</Card>
|
|
|
|
<Card title="3. User Authentication" icon="user" href="/getting-started/tutorials/reactnative/3-user-authentication">
|
|
Complete auth flow
|
|
</Card>
|
|
|
|
<Card title="4. GraphQL Operations" icon="list-check" href="/getting-started/tutorials/reactnative/4-graphql-operations">
|
|
CRUD operations with GraphQL
|
|
</Card>
|
|
|
|
<Card title="5. File Uploads" icon="upload" href="/getting-started/tutorials/reactnative/5-file-uploads">
|
|
File upload and management
|
|
</Card>
|
|
|
|
<Card title="6. Sign in with Apple" icon="apple" href="/getting-started/tutorials/reactnative/6-sign-in-with-apple">
|
|
**Current** - Apple authentication integration
|
|
</Card>
|
|
</CardGroup>
|
|
|
|
## Prerequisites
|
|
|
|
- Complete the [User Authentication part](/getting-started/tutorials/reactnative/3-user-authentication) first
|
|
- Access to an Apple Developer account (required for Sign in with Apple)
|
|
- An iOS device or simulator for testing (Sign in with Apple requires iOS)
|
|
- The project from the previous parts set up and running
|
|
|
|
## What You'll Build
|
|
|
|
By the end of this part, you'll have:
|
|
- **Apple Developer Console configuration** for Sign in with Apple
|
|
- **Nhost Auth provider setup** for Apple authentication
|
|
- **Native Sign in with Apple button** in your React Native app
|
|
- **Seamless integration** with your existing authentication flow
|
|
- **Error handling** for Apple Sign In specific scenarios
|
|
|
|
## Step-by-Step Guide
|
|
|
|
<Steps>
|
|
<Step>
|
|
|
|
### Configure Apple Developer Console
|
|
|
|
Before we start we need to configure Apple Sign In. To do so, follow the [sign in with Apple](/products/auth/social/sign-in-apple) guide. In addition, you will have to configure the audience in the nhost dashboard. To do so, set the audience with your application's bundle identifier (e.g. `com.yourcompany.yourapp`).
|
|
|
|
<Warning>
|
|
If you are testing this flow in Expo Go, you will need to configure the audience to be `host.exp.Exponent`.
|
|
</Warning>
|
|
|
|
</Step>
|
|
|
|
<Step>
|
|
|
|
### Install Required Dependencies
|
|
|
|
Install the necessary packages for Apple Sign In in React Native:
|
|
|
|
```bash
|
|
npx expo install expo-apple-authentication@7 expo-crypto@14
|
|
```
|
|
|
|
The required packages provide:
|
|
- **expo-apple-authentication**: Native Apple Sign In functionality and Apple-styled sign-in buttons
|
|
- **expo-crypto**: Cryptographic utilities needed for secure nonce generation and hashing
|
|
|
|
</Step>
|
|
|
|
<Step>
|
|
|
|
### Update App Configuration
|
|
|
|
Add the Sign in with Apple configuration to your `app.json` or `app.config.js`:
|
|
|
|
```json app.json lines highlight={11-23,52}
|
|
{
|
|
"expo": {
|
|
"name": "nhost-reactnative-tutorial",
|
|
"slug": "nhost-reactnative-tutorial",
|
|
"version": "1.0.0",
|
|
"orientation": "portrait",
|
|
"icon": "./assets/images/icon.png",
|
|
"scheme": "nhostreactnativetutorial",
|
|
"userInterfaceStyle": "automatic",
|
|
"newArchEnabled": true,
|
|
"ios": {
|
|
"supportsTablet": true,
|
|
"bundleIdentifier": "io.example.nhost-reactnative-tutorial",
|
|
"jsEngine": "jsc",
|
|
"infoPlist": {
|
|
"NSFaceIDUsageDescription": "This app uses Face ID for signing in",
|
|
"CFBundleURLTypes": [
|
|
{
|
|
"CFBundleURLSchemes": ["nhost-reactnative-tutorial"]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"android": {
|
|
"adaptiveIcon": {
|
|
"backgroundColor": "#E6F4FE",
|
|
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
|
"backgroundImage": "./assets/images/android-icon-background.png",
|
|
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
|
},
|
|
"edgeToEdgeEnabled": true,
|
|
"predictiveBackGestureEnabled": false
|
|
},
|
|
"web": {
|
|
"output": "static",
|
|
"favicon": "./assets/images/favicon.png"
|
|
},
|
|
"plugins": [
|
|
"expo-router",
|
|
[
|
|
"expo-splash-screen",
|
|
{
|
|
"image": "./assets/images/splash-icon.png",
|
|
"imageWidth": 200,
|
|
"resizeMode": "contain",
|
|
"backgroundColor": "#ffffff",
|
|
"dark": {
|
|
"backgroundColor": "#000000"
|
|
}
|
|
}
|
|
],
|
|
["expo-apple-authentication"]
|
|
],
|
|
"experiments": {
|
|
"typedRoutes": true,
|
|
"reactCompiler": true
|
|
},
|
|
"extra": {
|
|
"NHOST_REGION": <region>,
|
|
"NHOST_SUBDOMAIN": <subdomain>
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
<Important>
|
|
Make sure the `bundleIdentifier` matches the App ID you configured in the Apple Developer Console, and set `usesAppleSignIn` to `true`.
|
|
</Important>
|
|
|
|
</Step>
|
|
|
|
<Step>
|
|
|
|
### Create Apple Sign In Component
|
|
|
|
Create a reusable component for Apple Sign In functionality following the ReactNativeDemo approach:
|
|
|
|
```tsx app/components/AppleSignInButton.tsx
|
|
import * as AppleAuthentication from "expo-apple-authentication";
|
|
import * as Crypto from "expo-crypto";
|
|
import { useRouter } from "expo-router";
|
|
import { useEffect, useState } from "react";
|
|
import { Alert, Platform, StyleSheet } from "react-native";
|
|
import { useAuth } from "../lib/nhost/AuthProvider";
|
|
|
|
interface AppleSignInButtonProps {
|
|
isLoading: boolean;
|
|
setIsLoading: (isLoading: boolean) => void;
|
|
}
|
|
|
|
export default function AppleSignInButton({
|
|
setIsLoading,
|
|
}: AppleSignInButtonProps) {
|
|
const { nhost } = useAuth();
|
|
const router = useRouter();
|
|
const [appleAuthAvailable, setAppleAuthAvailable] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const checkAvailability = async () => {
|
|
if (Platform.OS === "ios") {
|
|
const isAvailable = await AppleAuthentication.isAvailableAsync();
|
|
setAppleAuthAvailable(isAvailable);
|
|
}
|
|
};
|
|
|
|
void checkAvailability();
|
|
}, []);
|
|
|
|
const handleAppleSignIn = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
|
|
// Generate a random nonce for security
|
|
const nonce = Math.random().toString(36).substring(2, 15);
|
|
|
|
// Hash the nonce for Apple Authentication
|
|
const hashedNonce = await Crypto.digestStringAsync(
|
|
Crypto.CryptoDigestAlgorithm.SHA256,
|
|
nonce,
|
|
);
|
|
|
|
// Request Apple authentication with our hashed nonce
|
|
const credential = await AppleAuthentication.signInAsync({
|
|
requestedScopes: [
|
|
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
|
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
|
],
|
|
nonce: hashedNonce,
|
|
});
|
|
|
|
if (credential.identityToken) {
|
|
// Use the identity token to sign in with Nhost
|
|
// Pass the original unhashed nonce to the SDK
|
|
// so the server can verify it
|
|
const response = await nhost.auth.signInIdToken({
|
|
provider: "apple",
|
|
idToken: credential.identityToken,
|
|
nonce,
|
|
});
|
|
|
|
if (response.body?.session) {
|
|
router.replace("/profile");
|
|
} else {
|
|
Alert.alert(
|
|
"Authentication Error",
|
|
"Failed to authenticate with Nhost",
|
|
);
|
|
}
|
|
} else {
|
|
Alert.alert(
|
|
"Authentication Error",
|
|
"No identity token received from Apple",
|
|
);
|
|
}
|
|
} catch (error: unknown) {
|
|
// Handle user cancellation gracefully
|
|
if (error instanceof Error && error.message.includes("canceled")) {
|
|
// User cancelled the sign-in flow, don't show an error
|
|
return;
|
|
}
|
|
|
|
// Handle other errors
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: "Failed to authenticate with Apple";
|
|
Alert.alert("Authentication Error", message);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// Only show the button on iOS devices where Apple authentication is available
|
|
if (Platform.OS !== "ios" || !appleAuthAvailable) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<AppleAuthentication.AppleAuthenticationButton
|
|
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
|
|
buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
|
|
cornerRadius={5}
|
|
style={styles.appleButton}
|
|
onPress={handleAppleSignIn}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
appleButton: {
|
|
width: "100%",
|
|
height: 45,
|
|
marginBottom: 10,
|
|
},
|
|
});
|
|
```
|
|
|
|
</Step>
|
|
|
|
<Step>
|
|
|
|
### Update Sign In Screen
|
|
|
|
Now integrate the Apple Sign In button into your existing sign-in screen following the tutorial structure:
|
|
|
|
```tsx app/signin.tsx lines highlight={13,70-81}
|
|
import { Link, router } from "expo-router";
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
ScrollView,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
import AppleSignInButton from "./components/AppleSignInButton";
|
|
import { useAuth } from "./lib/nhost/AuthProvider";
|
|
import { commonStyles } from "./styles/commonStyles";
|
|
import { colors } from "./styles/theme";
|
|
|
|
export default function SignIn() {
|
|
const { nhost, isAuthenticated } = useAuth();
|
|
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Use useEffect for navigation after authentication is confirmed
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
router.replace("/profile");
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
const handleSubmit = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Use the signIn function from auth context
|
|
const response = await nhost.auth.signInEmailPassword({
|
|
email,
|
|
password,
|
|
});
|
|
|
|
// If we have a session, sign in was successful
|
|
if (response.body?.session) {
|
|
router.replace("/profile");
|
|
} else {
|
|
setError("Failed to sign in. Please check your credentials.");
|
|
}
|
|
} catch (err) {
|
|
const message = (err as Error).message || "Unknown error";
|
|
setError(`An error occurred during sign in: ${message}`);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
style={commonStyles.container}
|
|
>
|
|
<ScrollView
|
|
contentContainerStyle={commonStyles.centerContent}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
<Text style={commonStyles.title}>Sign In</Text>
|
|
|
|
<View style={commonStyles.card}>
|
|
{/* Apple Sign In Button */}
|
|
<AppleSignInButton
|
|
isLoading={isLoading}
|
|
setIsLoading={setIsLoading}
|
|
/>
|
|
|
|
{/* Divider */}
|
|
<View style={commonStyles.dividerContainer}>
|
|
<View style={commonStyles.divider} />
|
|
<Text style={commonStyles.dividerText}>or</Text>
|
|
<View style={commonStyles.divider} />
|
|
</View>
|
|
|
|
<View style={commonStyles.formField}>
|
|
<Text style={commonStyles.labelText}>Email</Text>
|
|
<TextInput
|
|
style={commonStyles.input}
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
placeholder="Enter your email"
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
autoComplete="email"
|
|
/>
|
|
</View>
|
|
|
|
<View style={commonStyles.formField}>
|
|
<Text style={commonStyles.labelText}>Password</Text>
|
|
<TextInput
|
|
style={commonStyles.input}
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
placeholder="Enter your password"
|
|
secureTextEntry
|
|
autoCapitalize="none"
|
|
/>
|
|
</View>
|
|
|
|
{error && (
|
|
<View style={commonStyles.errorContainer}>
|
|
<Text style={commonStyles.errorText}>{error}</Text>
|
|
</View>
|
|
)}
|
|
|
|
<TouchableOpacity
|
|
style={[commonStyles.button, commonStyles.fullWidth]}
|
|
onPress={handleSubmit}
|
|
disabled={isLoading}
|
|
>
|
|
{isLoading ? (
|
|
<ActivityIndicator size="small" color={colors.surface} />
|
|
) : (
|
|
<Text style={commonStyles.buttonText}>Sign In</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={commonStyles.linkContainer}>
|
|
<Text style={commonStyles.linkText}>
|
|
Don't have an account?{" "}
|
|
<Link href="/signup" style={commonStyles.link}>
|
|
Sign Up
|
|
</Link>
|
|
</Text>
|
|
</View>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
```
|
|
|
|
</Step>
|
|
|
|
<Step>
|
|
|
|
### Update Sign Up Screen
|
|
|
|
Also add Apple Sign In to your sign-up screen for consistency:
|
|
|
|
```tsx app/signup.tsx lines highlight={14,103-114}
|
|
import * as Linking from "expo-linking";
|
|
import { Link, router } from "expo-router";
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
ScrollView,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
import AppleSignInButton from "./components/AppleSignInButton";
|
|
import { useAuth } from "./lib/nhost/AuthProvider";
|
|
import { commonStyles } from "./styles/commonStyles";
|
|
import { colors } from "./styles/theme";
|
|
|
|
export default function SignUp() {
|
|
const { nhost, isAuthenticated } = useAuth();
|
|
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [displayName, setDisplayName] = useState("");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
// Redirect authenticated users to profile
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
router.replace("/profile");
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
const handleSubmit = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
setSuccess(false);
|
|
|
|
try {
|
|
const response = await nhost.auth.signUpEmailPassword({
|
|
email,
|
|
password,
|
|
options: {
|
|
displayName,
|
|
// Set the redirect URL for email verification
|
|
redirectTo: Linking.createURL("verify"),
|
|
},
|
|
});
|
|
|
|
if (response.body?.session) {
|
|
// Successfully signed up and automatically signed in
|
|
router.replace("/profile");
|
|
} else {
|
|
// Verification email sent
|
|
setSuccess(true);
|
|
}
|
|
} catch (err) {
|
|
const message = (err as Error).message || "Unknown error";
|
|
setError(`An error occurred during sign up: ${message}`);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
if (success) {
|
|
return (
|
|
<View style={commonStyles.centerContent}>
|
|
<Text style={commonStyles.title}>Check Your Email</Text>
|
|
<View style={commonStyles.successContainer}>
|
|
<Text style={commonStyles.successText}>
|
|
We've sent a verification link to{" "}
|
|
<Text style={commonStyles.emailText}>{email}</Text>
|
|
</Text>
|
|
<Text style={[commonStyles.bodyText, commonStyles.textCenter]}>
|
|
Please check your email and click the verification link to activate
|
|
your account.
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
style={[commonStyles.button, commonStyles.fullWidth]}
|
|
onPress={() => router.replace("/signin")}
|
|
>
|
|
<Text style={commonStyles.buttonText}>Back to Sign In</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
style={commonStyles.container}
|
|
>
|
|
<ScrollView
|
|
contentContainerStyle={commonStyles.centerContent}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
<Text style={commonStyles.title}>Sign Up</Text>
|
|
|
|
<View style={commonStyles.card}>
|
|
{/* Apple Sign In Button */}
|
|
<AppleSignInButton
|
|
isLoading={isLoading}
|
|
setIsLoading={setIsLoading}
|
|
/>
|
|
|
|
{/* Divider */}
|
|
<View style={commonStyles.dividerContainer}>
|
|
<View style={commonStyles.divider} />
|
|
<Text style={commonStyles.dividerText}>or</Text>
|
|
<View style={commonStyles.divider} />
|
|
</View>
|
|
|
|
<View style={commonStyles.formField}>
|
|
<Text style={commonStyles.labelText}>Display Name</Text>
|
|
<TextInput
|
|
style={commonStyles.input}
|
|
value={displayName}
|
|
onChangeText={setDisplayName}
|
|
placeholder="Enter your name"
|
|
autoCapitalize="words"
|
|
/>
|
|
</View>
|
|
|
|
<View style={commonStyles.formField}>
|
|
<Text style={commonStyles.labelText}>Email</Text>
|
|
<TextInput
|
|
style={commonStyles.input}
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
placeholder="Enter your email"
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
autoComplete="email"
|
|
/>
|
|
</View>
|
|
|
|
<View style={commonStyles.formField}>
|
|
<Text style={commonStyles.labelText}>Password</Text>
|
|
<TextInput
|
|
style={commonStyles.input}
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
placeholder="Enter your password"
|
|
secureTextEntry
|
|
autoCapitalize="none"
|
|
/>
|
|
<Text style={commonStyles.helperText}>Minimum 8 characters</Text>
|
|
</View>
|
|
|
|
{error && (
|
|
<View style={commonStyles.errorContainer}>
|
|
<Text style={commonStyles.errorText}>{error}</Text>
|
|
</View>
|
|
)}
|
|
|
|
<TouchableOpacity
|
|
style={[commonStyles.button, commonStyles.fullWidth]}
|
|
onPress={handleSubmit}
|
|
disabled={isLoading}
|
|
>
|
|
{isLoading ? (
|
|
<ActivityIndicator size="small" color={colors.surface} />
|
|
) : (
|
|
<Text style={commonStyles.buttonText}>Sign Up</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={commonStyles.linkContainer}>
|
|
<Text style={commonStyles.linkText}>
|
|
Already have an account?{" "}
|
|
<Link href="/signin" style={commonStyles.link}>
|
|
Sign In
|
|
</Link>
|
|
</Text>
|
|
</View>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
```
|
|
|
|
</Step>
|
|
|
|
<Step>
|
|
|
|
### Test Sign in with Apple
|
|
|
|
Build and test your application on an iOS device or simulator:
|
|
|
|
```bash
|
|
npm start
|
|
```
|
|
|
|
Things to test:
|
|
|
|
1. **Apple Sign In Flow**: Tap the "Sign in with Apple" button and complete the authentication
|
|
2. **User Creation**: Sign in with Apple for the first time to create a new user account
|
|
3. **Returning Users**: Sign in again with the same Apple ID to verify user recognition
|
|
4. **Profile Information**: Check that user name and email are properly populated
|
|
5. **Integration**: Verify that Apple Sign In users can access todos, files, and other protected features
|
|
6. **Error Handling**: Test network errors and authentication failures
|
|
|
|
<Note>
|
|
Sign in with Apple only works on physical iOS devices or iOS simulators running iOS 13+. It will not work on Android or web platforms in this implementation.
|
|
</Note>
|
|
|
|
</Step>
|
|
|
|
</Steps>
|
|
|
|
## Key Features Implemented
|
|
|
|
<AccordionGroup>
|
|
<Accordion title="Apple Developer Configuration" icon="gear">
|
|
Complete setup of Apple Developer Console including App ID configuration, Service ID creation, and private key generation for secure authentication.
|
|
</Accordion>
|
|
|
|
<Accordion title="Nhost Provider Integration" icon="link">
|
|
Seamless integration with Nhost Auth system using Apple's identity tokens and authorization codes for secure user authentication.
|
|
</Accordion>
|
|
|
|
<Accordion title="Native iOS Integration" icon="mobile">
|
|
Native Apple Sign In button using expo-apple-authentication with proper iOS styling and user experience guidelines.
|
|
</Accordion>
|
|
|
|
<Accordion title="Error Handling" icon="triangle-exclamation">
|
|
Comprehensive error handling for Apple Sign In failures, network issues, and user cancellations with appropriate user feedback.
|
|
</Accordion>
|
|
|
|
</AccordionGroup>
|