# Notification Deep-Links Reference for handling external deep-link URLs sent in customer notification emails (e.g. "Manage your subscription", "Update payment method"). --- ## Table of Contents 1. [Overview](#1-overview) 2. [Registered Routes](#2-registered-routes) 3. [How It Works — Full Flow](#3-how-it-works--full-flow) - [Authenticated user](#authenticated-user) - [Unauthenticated user](#unauthenticated-user) 4. [Key Files](#4-key-files) 5. [Adding a New Notification URL](#5-adding-a-new-notification-url) 6. [Design Decisions](#6-design-decisions) --- ## 1. Overview Notification emails link customers to top-level URLs such as `/#/manage-subscription`. These URLs must: - **Not require authentication themselves** — the customer may be logged out - **Skip the shell layout** — they live outside `AppMainComponent` - **Redirect instantly** — no blank-page flash, no component rendered - **Show a contextual notice** on the login screen when authentication is required All of this is handled by a single `NotificationRedirectGuard` combined with route `data`. Adding a new notification URL requires **one route entry and zero new files**. --- ## 2. Registered Routes All notification routes are declared at the top level in `app-routing.module.ts`, outside the `AppMainComponent` shell: ``` /#/manage-subscription → /profile/myservices (or /profile/services if no subs) /#/update-pm → /profile/payment-method-list /#/update-bill-address → /profile/billing-address ``` Each route in source: ```typescript // app-routing.module.ts { path: 'manage-subscription', component: PageNotFoundComponent, // never rendered — guard always redirects canActivate: [NotificationRedirectGuard], data: { redirectTo: ['profile', 'myservices'], redirectToNoSubs: ['profile', 'services'], loginNotice: $localize`@@manageSubLoginNotice:Please log in with your Master account to manage subscriptions.` } }, { path: 'update-pm', component: PageNotFoundComponent, canActivate: [NotificationRedirectGuard], data: { redirectTo: ['profile', 'payment-method-list'], loginNotice: $localize`@@updatePmLoginNotice:Please log in with your Master account to update your payment method.` } }, { path: 'update-bill-address', component: PageNotFoundComponent, canActivate: [NotificationRedirectGuard], data: { redirectTo: ['profile', 'billing-address'], loginNotice: $localize`@@updateBillAddrLoginNotice:Please log in with your Master account to update your billing address.` } }, ``` > `PageNotFoundComponent` is the placeholder — it is already declared in `AppModule` > and is never displayed because the guard always returns a `UrlTree` before any > component activates. --- ## 3. How It Works — Full Flow ### Authenticated user ``` User clicks link in email │ ▼ Browser opens /#/manage-subscription │ ▼ Angular router matches route │ ▼ NotificationRedirectGuard.canActivate() │ ├─ authSvc.loggedIn = true │ ├─[redirectToNoSubs defined AND master AND no subs] │ └──→ UrlTree: /profile/services │ └─[all other authenticated cases] └──→ UrlTree: /profile/myservices │ ▼ AppMainComponent loads normally /profile/myservices renders ``` The guard returns a `UrlTree` **synchronously** (auth state is rehydrated from `sessionStorage` before any guard runs). Angular processes the redirect before deactivating the current route or activating any component — no blank page, no shell teardown. --- ### Unauthenticated user ``` User clicks link in email (not logged in) │ ▼ Browser opens /#/manage-subscription │ ▼ NotificationRedirectGuard.canActivate() │ ├─ authSvc.loggedIn = false │ └──→ UrlTree: /login?returnUrl=manage-subscription &loginNotice= │ ▼ LoginComponent constructor reads queryParams nav.extractedUrl.queryParams['loginNotice'] │ ▼ Pushes { severity: 'info', detail: loginNotice } into this.msgs → rendered by │ ▼ ┌──────────────────────────────────────────┐ │ [AgMission logo] │ │ │ │ ℹ Please log in with your Master │ │ account to manage subscriptions. │ │ │ │ Username ________________________ │ │ Password ________________________ │ │ [ LOGIN ] │ └──────────────────────────────────────────┘ │ User logs in │ ▼ authActions.LoginSuccess dispatched │ ▼ AuthEffects.navigateDefault() router.parseUrl(router.url) .queryParams['returnUrl'] → 'manage-subscription' │ ▼ window.location.replace('/#/manage-subscription') │ ▼ NotificationRedirectGuard runs again (now authenticated) │ ▼ Redirects to /profile/myservices ``` --- ## 4. Key Files ### `src/app/domain/guards/notification-redirect.guard.ts` The single guard that handles all notification deep-links. **Route `data` contract:** | Field | Type | Required | Description | |---|---|---|---| | `redirectTo` | `string[]` | Yes | Router path segments for authenticated users | | `redirectToNoSubs` | `string[]` | No | Alternate path when master account has no subscriptions | | `loginNotice` | `string` | No | Message shown in the `` bar on the login screen | ```typescript canActivate(route: ActivatedRouteSnapshot): UrlTree { const { redirectTo, redirectToNoSubs } = route.data; if (!this.authSvc.loggedIn) { const { loginNotice } = route.data; return this.router.createUrlTree(['/login'], { queryParams: { returnUrl: route.url.map(s => s.path).join('/'), ...(loginNotice ? { loginNotice } : {}) } }); } const isMaster = !this.authSvc.user?.parent; if (redirectToNoSubs && isMaster && !this.authSvc.hasSubs()) { return this.router.createUrlTree(redirectToNoSubs); } return this.router.createUrlTree(redirectTo); } ``` --- ### `src/app/auth/effects/auth.effects.ts` — `navigateDefault()` After a successful login, reads `returnUrl` from the current router URL and replaces the browser location to trigger the notification route again (now authenticated): ```typescript private navigateDefault(lang) { const hash = (this.router.url.indexOf('#') == -1) ? '/#/' : '/'; const returnUrl = this.router.parseUrl(this.router.url).queryParams['returnUrl'] || 'home'; window.location.replace((lang === 'en' ? hash : `/${lang}${hash}`) + returnUrl); } ``` Uses `router.parseUrl()` instead of string splitting — correctly handles all URL encodings. If no `returnUrl` is present (normal login), falls back to `'home'` as before. --- ### `src/app/auth/login/login.component.ts` — constructor Generic `loginNotice` handling — no hardcoded route names: ```typescript const nav = this.router.getCurrentNavigation(); if (nav) { const msgs: any[] = []; if (nav.extras?.state?.changedPwd) { msgs.push({ severity: 'info', summary: '', detail: globals.pwdChangedOk }); } const loginNotice = nav.finalUrl?.queryParams?.['loginNotice'] ?? nav.extractedUrl?.queryParams?.['loginNotice']; if (loginNotice) { msgs.push({ severity: 'info', summary: '', detail: loginNotice }); } if (msgs.length) this.msgs = msgs; } ``` Reads from `getCurrentNavigation()` — the only safe place to read query params on a login redirect since the router replaces the URL before `ngOnInit` runs. --- ## 5. Adding a New Notification URL **Zero new files required.** Add one entry to `app-routing.module.ts`: ```typescript { path: 'renew-subscription', // URL path: /#/renew-subscription component: PageNotFoundComponent, // never rendered canActivate: [NotificationRedirectGuard], data: { redirectTo: ['profile', 'checkout'], // authenticated destination // redirectToNoSubs: ['profile', 'services'], // optional alternate loginNotice: $localize`:@@renewSubLoginNotice:Please log in with your Master account to renew your subscription.` } }, ``` That's it. The guard, the login notice, and the post-login redirect all work automatically. **Checklist:** - [ ] Add route entry with `redirectTo` (required) and optionally `redirectToNoSubs` / `loginNotice` - [ ] Add the `loginNotice` i18n key to all locale `.xlf` translation files if the app is translated - [ ] Provide the deep-link URL to the notifications/email team: `https://app.agmission.com/#/renew-subscription` --- ## 6. Design Decisions ### Why a guard returning `UrlTree` instead of a redirect component? A component that calls `router.navigate()` in `ngOnInit` causes: - The current shell (`AppMainComponent`) to deactivate and re-activate — visible flicker - A blank template to render briefly while `ngOnInit` executes A guard returning a `UrlTree` is processed by Angular **before any component is activated or deactivated**. The redirect is invisible and instantaneous. ### Why route `data` instead of a guard per URL? One guard file per URL creates N files for N routes with identical logic. Putting the routing targets in `data` makes the guard a pure engine and the route definition the configuration — consistent with Angular's `canActivate`+`data` idiom used throughout the app (e.g. `RoleGuard` + `data.roles`). ### Why `PageNotFoundComponent` as the placeholder? Angular requires a `component` on every non-lazy route. The component is never rendered (the guard always redirects), so any already-declared component works. `PageNotFoundComponent` is the most semantically appropriate fallback if the guard ever fails to redirect for an unexpected reason. ### Why `window.location.replace()` instead of `router.navigate()` in `navigateDefault`? This was the pre-existing pattern to prevent the login page from appearing in the browser's Back history after authentication. `location.replace` replaces the current history entry rather than pushing a new one. ### Sub-accounts vs. Master accounts Subscription management (`/profile/myservices`) is only meaningful for master accounts, but sub-accounts (users with `user.parent` set) are redirected there too — the manage-subscription view enforces its own access rules once loaded. `redirectToNoSubs` is only evaluated for master accounts with zero subscriptions, sending them to the `/profile/services` plan picker instead.