sensei-uaepass
Version:
🥋 Master of UAE Pass integration! Angular OAuth 2.0 (PKCE) client with sensei-level signals-based state management, multi-language support, and elegant UI components.
811 lines (797 loc) • 41.9 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, signal, computed, Injectable, input, output, ChangeDetectionStrategy, Component, effect } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { timeout } from 'rxjs/operators';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
const UAE_PASS_CONFIG = new InjectionToken('UAE_PASS_CONFIG');
function provideUaePass(config) {
return [{ provide: UAE_PASS_CONFIG, useValue: config }];
}
// Centralized enums for sensei-uaepass
var UaePassAuthStatus;
(function (UaePassAuthStatus) {
UaePassAuthStatus["Idle"] = "idle";
UaePassAuthStatus["Authorizing"] = "authorizing";
UaePassAuthStatus["ExchangingToken"] = "exchangingToken";
UaePassAuthStatus["Authenticated"] = "authenticated";
UaePassAuthStatus["Error"] = "error";
UaePassAuthStatus["LoggedOut"] = "loggedOut";
})(UaePassAuthStatus || (UaePassAuthStatus = {}));
var UaePassStorageMode;
(function (UaePassStorageMode) {
UaePassStorageMode["None"] = "none";
UaePassStorageMode["Session"] = "session";
UaePassStorageMode["Local"] = "local";
})(UaePassStorageMode || (UaePassStorageMode = {}));
var UaePassAcr;
(function (UaePassAcr) {
UaePassAcr["MobileOnDevice"] = "urn:digitalid:authentication:flow:mobileondevice";
UaePassAcr["Web"] = "urn:safelayer:tws:policies:authentication:level:low";
})(UaePassAcr || (UaePassAcr = {}));
var UaePassLanguageCode;
(function (UaePassLanguageCode) {
UaePassLanguageCode["En"] = "en";
UaePassLanguageCode["Ar"] = "ar";
})(UaePassLanguageCode || (UaePassLanguageCode = {}));
// Optional convenience if consumers prefer enums over booleans for environment
var UaePassEnvironment;
(function (UaePassEnvironment) {
UaePassEnvironment["Production"] = "production";
UaePassEnvironment["Staging"] = "staging";
})(UaePassEnvironment || (UaePassEnvironment = {}));
var OAuthResponseType;
(function (OAuthResponseType) {
OAuthResponseType["Code"] = "code";
})(OAuthResponseType || (OAuthResponseType = {}));
var CodeChallengeMethod;
(function (CodeChallengeMethod) {
CodeChallengeMethod["S256"] = "S256";
})(CodeChallengeMethod || (CodeChallengeMethod = {}));
var OAuthGrantType;
(function (OAuthGrantType) {
OAuthGrantType["AuthorizationCode"] = "authorization_code";
})(OAuthGrantType || (OAuthGrantType = {}));
// UAE PASS constants and endpoint helpers
// Mirrors Flutter implementation in lib/uaepass/constant.dart
const UAE_PASS_BASE_URL = {
prod: 'https://id.uaepass.ae',
stg: 'https://stg-id.uaepass.ae',
};
// Backward-compatible constant mapping to enums
const UAE_PASS_ACR = {
mobileOnDevice: UaePassAcr.MobileOnDevice,
web: UaePassAcr.Web,
};
function baseUrl(isProduction) {
return isProduction ? UAE_PASS_BASE_URL.prod : UAE_PASS_BASE_URL.stg;
}
function authorizeUrl(isProduction) {
return `${baseUrl(isProduction)}/idshub/authorize`;
}
function tokenUrl(isProduction) {
return `${baseUrl(isProduction)}/idshub/token`;
}
function userInfoUrl(isProduction) {
return `${baseUrl(isProduction)}/idshub/userinfo`;
}
function logoutUrl(isProduction, redirectUri) {
return `${baseUrl(isProduction)}/idshub/logout?redirect_uri=${encodeURIComponent(redirectUri)}`;
}
// Ephemeral storage across redirect for PKCE + state
// Uses sessionStorage to survive full-page redirects and be cleared easily.
const PREFIX$1 = 'uae-pass:';
function key(name) {
return `${PREFIX$1}${name}`;
}
const UaePassMemory = {
setState(state) {
if (typeof sessionStorage === 'undefined')
return;
sessionStorage.setItem(key('state'), state);
},
getState() {
if (typeof sessionStorage === 'undefined')
return null;
return sessionStorage.getItem(key('state'));
},
setCodeVerifier(verifier) {
if (typeof sessionStorage === 'undefined')
return;
sessionStorage.setItem(key('code_verifier'), verifier);
},
getCodeVerifier() {
if (typeof sessionStorage === 'undefined')
return null;
return sessionStorage.getItem(key('code_verifier'));
},
clear() {
if (typeof sessionStorage === 'undefined')
return;
sessionStorage.removeItem(key('state'));
sessionStorage.removeItem(key('code_verifier'));
},
};
// PKCE and state utilities for UAE PASS OAuth 2.0
// Mirrors behavior of Flutter implementation (random state, S256 code challenge)
const PKCE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
function randomString(length, charset = PKCE_CHARSET) {
const result = [];
const cryptoObj = globalThis.crypto || globalThis.msCrypto;
if (!cryptoObj) {
// Non-cryptographic fallback; consumers should polyfill crypto in SSR if needed
for (let i = 0; i < length; i++) {
result.push(charset[Math.floor(Math.random() * charset.length)]);
}
return result.join('');
}
const rnd = new Uint8Array(length);
cryptoObj.getRandomValues(rnd);
for (let i = 0; i < length; i++) {
result.push(charset[rnd[i] % charset.length]);
}
return result.join('');
}
function generateState(length = 32) {
return randomString(length);
}
function base64Encode(bytes) {
// Avoid relying on btoa (not available in Node/SSR). Encode manually with correct padding.
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let output = '';
let i = 0;
while (i < bytes.length) {
const o1 = bytes[i++];
const o2 = i < bytes.length ? bytes[i++] : NaN;
const o3 = i < bytes.length ? bytes[i++] : NaN;
const c1 = o1 >> 2;
const c2 = ((o1 & 0x03) << 4) | (isNaN(o2) ? 0 : (o2 >> 4));
const c3 = isNaN(o2) ? 64 : ((o2 & 0x0f) << 2) | (isNaN(o3) ? 0 : (o3 >> 6));
const c4 = isNaN(o3) ? 64 : (o3 & 0x3f);
output += chars[c1] + chars[c2] + chars[c3] + chars[c4];
}
return output;
}
function base64UrlEncode(bytes) {
const base64 = base64Encode(bytes);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
async function sha256(input) {
const encoder = new TextEncoder();
const data = encoder.encode(input);
const cryptoObj = globalThis.crypto ?? globalThis.msCrypto;
if (!cryptoObj?.subtle) {
throw new Error('WebCrypto SubtleCrypto is not available');
}
const digest = await cryptoObj.subtle.digest('SHA-256', data);
return new Uint8Array(digest);
}
async function generateCodeChallengeS256(codeVerifier) {
const hash = await sha256(codeVerifier);
return base64UrlEncode(hash);
}
async function generatePkcePair() {
const codeVerifier = randomString(64);
const codeChallenge = await generateCodeChallengeS256(codeVerifier);
return { codeVerifier, codeChallenge };
}
const PREFIX = 'uae-pass:';
const TOKENS_KEY = `${PREFIX}tokens`;
const PROFILE_KEY = `${PREFIX}profile`;
function normalizeMode(mode) {
// Enum values are already the exact string values
if (mode === 'session' || mode === 'local' || mode === 'none')
return mode;
// Typescript enum runtime value is the string literal as well
return mode;
}
function getStore(mode) {
const m = normalizeMode(mode);
if (typeof window === 'undefined')
return null;
try {
if (m === 'session')
return window.sessionStorage ?? null;
if (m === 'local')
return window.localStorage ?? null;
}
catch {
// Storage may be blocked
}
return null;
}
function saveTokens(mode, tokens) {
const store = getStore(mode);
if (!store)
return;
if (!tokens) {
try {
store.removeItem(TOKENS_KEY);
}
catch { }
return;
}
try {
store.setItem(TOKENS_KEY, JSON.stringify(tokens));
}
catch { }
}
function loadTokens(mode) {
const store = getStore(mode);
if (!store)
return null;
try {
const raw = store.getItem(TOKENS_KEY);
return raw ? JSON.parse(raw) : null;
}
catch {
return null;
}
}
function saveProfile(mode, profile) {
const store = getStore(mode);
if (!store)
return;
if (!profile) {
try {
store.removeItem(PROFILE_KEY);
}
catch { }
return;
}
try {
store.setItem(PROFILE_KEY, JSON.stringify(profile));
}
catch { }
}
function loadProfile(mode) {
const store = getStore(mode);
if (!store)
return null;
try {
const raw = store.getItem(PROFILE_KEY);
return raw ? JSON.parse(raw) : null;
}
catch {
return null;
}
}
function clearAll(mode) {
const store = getStore(mode);
if (!store)
return;
try {
store.removeItem(TOKENS_KEY);
store.removeItem(PROFILE_KEY);
}
catch { }
}
const UAE_PASS_TEXTS_EN = {
signInWithUaePass: 'Sign with UAE PASS',
completingSignIn: 'Completing sign-in…',
signedInSuccessfully: 'Signed in successfully!',
redirectingToHomePage: 'Redirecting to home page...',
error: 'Error',
returnToHome: 'Return to home',
loading: 'Loading',
pleaseWait: 'Please wait',
windowNotAvailable: 'Window is not available to redirect',
noUrlContext: 'No URL context available to handle callback',
securityCheckFailed: 'Security check failed: state mismatch',
tokenExchangeFailed: 'Failed to obtain access token',
noUserProfile: 'No user profile data received',
userProfileFetchFailed: 'Failed to fetch user profile',
uaePass: 'UAE PASS'
};
const UAE_PASS_TEXTS_AR = {
signInWithUaePass: 'تسجيل الدخول بهوية الإمارات الرقمية',
completingSignIn: 'جاري إكمال تسجيل الدخول…',
signedInSuccessfully: 'تم تسجيل الدخول بنجاح!',
redirectingToHomePage: 'جاري التوجيه إلى الصفحة الرئيسية...',
error: 'خطأ',
returnToHome: 'العودة إلى الصفحة الرئيسية',
loading: 'جاري التحميل',
pleaseWait: 'يرجى الانتظار',
windowNotAvailable: 'النافذة غير متاحة للتوجيه',
noUrlContext: 'لا يوجد سياق URL متاح للتعامل مع الاستدعاء',
securityCheckFailed: 'فشل في الفحص الأمني: عدم تطابق الحالة',
tokenExchangeFailed: 'فشل في الحصول على رمز الوصول',
noUserProfile: 'لم يتم استلام بيانات الملف الشخصي للمستخدم',
userProfileFetchFailed: 'فشل في جلب الملف الشخصي للمستخدم',
uaePass: 'هوية الإمارات الرقمية'
};
function getUaePassTexts(language = 'en') {
return language === 'ar' ? UAE_PASS_TEXTS_AR : UAE_PASS_TEXTS_EN;
}
class UaePassAuthService {
http = inject(HttpClient);
cfg = inject(UAE_PASS_CONFIG);
// Signals-based state
_status = signal(UaePassAuthStatus.Idle);
_tokens = signal(null);
_profile = signal(null);
_error = signal(null);
status = this._status.asReadonly();
tokens = this._tokens.asReadonly();
profile = this._profile.asReadonly();
error = this._error.asReadonly();
isAuthenticated = computed(() => this._status() === UaePassAuthStatus.Authenticated && !!this._tokens());
// Localized texts
texts = computed(() => {
const lang = this.cfg.language || 'en';
return getUaePassTexts(lang);
});
// Defaults
requestTimeoutMs() {
return this.cfg.requestTimeoutMs ?? 20_000;
}
storageMode() {
return this.cfg.storage ?? 'none';
}
constructor() {
// Restore from storage if configured
const tokens = loadTokens(this.storageMode());
if (tokens) {
this._tokens.set(tokens);
const profile = loadProfile(this.storageMode());
if (profile)
this._profile.set(profile);
this._status.set(UaePassAuthStatus.Authenticated);
}
}
// 1) Build UAE PASS authorize URL with PKCE + state; store ephemeral values in sessionStorage
async buildAuthorizeUrl() {
const state = generateState();
const { codeVerifier, codeChallenge } = await generatePkcePair();
UaePassMemory.setState(state);
UaePassMemory.setCodeVerifier(codeVerifier);
const params = new URLSearchParams({
response_type: OAuthResponseType.Code,
client_id: this.cfg.clientId,
scope: this.cfg.scope ?? 'urn:uae:digitalid:profile:general',
state,
redirect_uri: this.cfg.redirectUri,
ui_locales: this.cfg.language ?? UaePassLanguageCode.En,
acr_values: UaePassAcr.Web, // Web ACR for browser apps
code_challenge: codeChallenge,
code_challenge_method: CodeChallengeMethod.S256,
});
return `${authorizeUrl(this.cfg.isProduction)}?${params.toString()}`;
}
// 2) Redirect the browser to UAE PASS
async redirectToAuthorization() {
this._status.set(UaePassAuthStatus.Authorizing);
const url = await this.buildAuthorizeUrl();
if (typeof window !== 'undefined' && window?.location) {
window.location.assign(url);
}
else {
this._status.set(UaePassAuthStatus.Error);
this._error.set(this.texts().windowNotAvailable);
}
}
// 3) Handle callback on your redirect route. Provide url or it will use window.location.href
async handleRedirectCallback(currentUrl) {
const href = currentUrl ?? (typeof window !== 'undefined' ? window.location.href : '');
if (!href) {
this._status.set(UaePassAuthStatus.Error);
this._error.set(this.texts().noUrlContext);
return;
}
const url = new URL(href);
const returnedState = url.searchParams.get('state');
const code = url.searchParams.get('code');
const error = url.searchParams.get('error');
const errorDescription = url.searchParams.get('error_description');
if (!code) {
// If provider returned an error, surface it
if (error) {
this._status.set(UaePassAuthStatus.Error);
this._error.set(`${error}${errorDescription ? `: ${decodeURIComponent(errorDescription)}` : ''}`);
UaePassMemory.clear();
return;
}
// No-op if this is not the redirect URL
return;
}
const expectedState = UaePassMemory.getState();
if (!expectedState || returnedState !== expectedState) {
this._status.set(UaePassAuthStatus.Error);
this._error.set(this.texts().securityCheckFailed);
// Clear ephemeral state to avoid reuse
UaePassMemory.clear();
return;
}
this._status.set(UaePassAuthStatus.ExchangingToken);
try {
const tokens = await this.exchangeToken(code);
this._tokens.set(tokens);
saveTokens(this.storageMode(), tokens);
this._status.set(UaePassAuthStatus.Authenticated);
// Optionally fetch user info
try {
const profile = await this.fetchUserInfo(tokens.access_token);
if (profile) {
this._profile.set(profile);
saveProfile(this.storageMode(), profile);
}
else {
console.warn(`UAE Pass: ${this.texts().noUserProfile}`);
}
}
catch (e) {
console.error(`UAE Pass: ${this.texts().userProfileFetchFailed}:`, e);
// Non-fatal - user is still authenticated even without profile
}
}
catch (e) {
this._status.set(UaePassAuthStatus.Error);
this._error.set(e instanceof Error ? e.message : String(e));
}
finally {
UaePassMemory.clear();
}
}
// 4) Exchange code for tokens
async exchangeToken(code) {
const verifier = UaePassMemory.getCodeVerifier() ?? '';
// If a proxy is configured, prefer it (avoids exposing client_secret in the browser)
if (this.cfg.tokenProxyUrl) {
const body = {
code,
redirect_uri: this.cfg.redirectUri,
code_verifier: verifier,
};
const tokens = await firstValueFrom(this.http
.post(this.cfg.tokenProxyUrl, body)
.pipe(timeout(this.requestTimeoutMs())));
if (!tokens?.access_token)
throw new Error(this.texts().tokenExchangeFailed);
return tokens;
}
// Direct call to UAE PASS token endpoint (only safe if your tenant allows public SPA flow without secret)
const tokenEndpoint = tokenUrl(this.cfg.isProduction);
const params = new URLSearchParams();
params.set('redirect_uri', this.cfg.redirectUri);
params.set('client_id', this.cfg.clientId);
params.set('grant_type', OAuthGrantType.AuthorizationCode);
params.set('code', code);
params.set('code_verifier', verifier);
if (this.cfg.clientSecret) {
params.set('client_secret', this.cfg.clientSecret);
}
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
});
const tokens = await firstValueFrom(this.http
.post(tokenEndpoint, params.toString(), { headers })
.pipe(timeout(this.requestTimeoutMs())));
if (!tokens?.access_token)
throw new Error(this.texts().tokenExchangeFailed);
return tokens;
}
// 5) Fetch user profile (optional)
async fetchUserInfo(accessToken) {
if (this.cfg.userInfoProxyUrl) {
const profile = await firstValueFrom(this.http
.post(this.cfg.userInfoProxyUrl, {
token: accessToken,
})
.pipe(timeout(this.requestTimeoutMs())));
return profile ?? null;
}
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${accessToken}`,
});
const url = userInfoUrl(this.cfg.isProduction);
const profile = await firstValueFrom(this.http
.get(url, { headers })
.pipe(timeout(this.requestTimeoutMs())));
return profile ?? null;
}
// 6) Logout: clear local state and logout from UAE Pass servers (official flow)
logout() {
this._status.set(UaePassAuthStatus.LoggedOut);
this._tokens.set(null);
this._profile.set(null);
this._error.set(null);
clearAll(this.storageMode());
UaePassMemory.clear();
if (typeof window !== 'undefined' && window?.location) {
// Use logoutRedirectUri if provided, otherwise use redirectUri
const logoutUri = this.cfg.logoutRedirectUri || this.cfg.redirectUri;
const url = logoutUrl(this.cfg.isProduction, logoutUri);
window.location.assign(url);
}
}
resetError() {
this._error.set(null);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UaePassAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UaePassAuthService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UaePassAuthService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
class UaePassLoginButtonComponent {
auth = inject(UaePassAuthService);
config = inject(UAE_PASS_CONFIG);
// Default asset paths for UAE Pass button
defaultAssetPaths = {
english: 'assets/UAEPASS_Sign_with_Btn_Outline_Active@2x.svg',
arabic: 'assets/UAEPASS_Sign_with_Btn_Outline_Active_AR@2x.svg',
};
// Inputs
language = input();
customImageSrc = input(null);
customStyles = input('');
isDisabled = input(false);
// Outputs
pressed = output();
// Computed properties
isBusy = computed(() => {
const s = this.auth.status();
return (s === UaePassAuthStatus.Authorizing ||
s === UaePassAuthStatus.ExchangingToken);
});
imageSrc = computed(() => {
console.log('Input language:', this.language());
console.log('Config language:', this.config.language);
if (this.customImageSrc()) {
return this.customImageSrc();
}
// Use input language first, then config language, then default to 'en'
const configLang = this.config.language;
const inputLang = this.language();
// Handle the case where config.language might be 'ar' string or enum value
let normalizedConfigLang = 'en';
if (configLang === 'ar' || configLang === 'Arabic' || String(configLang).toLowerCase() === 'ar') {
normalizedConfigLang = 'ar';
}
const lang = inputLang || normalizedConfigLang;
console.log('Final language used:', lang);
console.log('Config language raw:', configLang);
console.log('Normalized config language:', normalizedConfigLang);
const configLogos = this.config.buttonLogos;
if (configLogos) {
console.log('Config logos:', configLogos);
if (lang === 'ar' && configLogos.arabic) {
console.log('Using config Arabic logo:', configLogos.arabic);
return configLogos.arabic;
}
if (lang === 'en' && configLogos.english) {
console.log('Using config English logo:', configLogos.english);
return configLogos.english;
}
}
// Fallback to default asset paths
const finalImage = lang === 'ar'
? this.defaultAssetPaths.arabic
: this.defaultAssetPaths.english;
console.log('Using default asset path:', finalImage);
return finalImage;
});
disabled = computed(() => {
return this.isDisabled() || this.isBusy();
});
handleLogin() {
this.pressed.emit();
this.auth.redirectToAuthorization();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UaePassLoginButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.14", type: UaePassLoginButtonComponent, isStandalone: true, selector: "uae-pass-login-button", inputs: { language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, customImageSrc: { classPropertyName: "customImageSrc", publicName: "customImageSrc", isSignal: true, isRequired: false, transformFunction: null }, customStyles: { classPropertyName: "customStyles", publicName: "customStyles", isSignal: true, isRequired: false, transformFunction: null }, isDisabled: { classPropertyName: "isDisabled", publicName: "isDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pressed: "pressed" }, host: { classAttribute: "uaepass-login-button" }, ngImport: i0, template: `
<button
type="button"
class="uae-pass-login-btn"
[disabled]="disabled()"
(click)="handleLogin()"
[attr.aria-label]="'Sign in with UAE Pass'"
>
<img
[src]="imageSrc()"
alt="Sign with UAE PASS"
class="uae-pass-btn-image"
[class.loading]="disabled()"
[style]="customStyles()"
/>
<div class="loading-spinner" *ngIf="disabled()"></div>
</button>
`, isInline: true, styles: [":host{display:block;width:100%}.uae-pass-login-btn{display:block;width:100%;border:none;background:transparent;padding:0;cursor:pointer;position:relative;transition:all .3s ease}.uae-pass-btn-image{width:100%;height:auto;display:block;transition:all .3s ease}.uae-pass-login-btn:hover:not([disabled]) .uae-pass-btn-image{transform:translateY(-2px);filter:drop-shadow(0 6px 20px rgba(0,0,0,.15))}.uae-pass-login-btn:active:not([disabled]) .uae-pass-btn-image{transform:translateY(0);filter:drop-shadow(0 2px 8px rgba(0,0,0,.1))}.uae-pass-login-btn[disabled] .uae-pass-btn-image{opacity:.6;cursor:not-allowed}.uae-pass-btn-image.loading{opacity:.6}.loading-spinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:24px;height:24px;border:3px solid rgba(0,0,0,.1);border-top:3px solid #1e40af;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:translate(-50%,-50%) rotate(0)}to{transform:translate(-50%,-50%) rotate(360deg)}}@media (max-width: 768px){.uae-pass-btn-image{max-width:280px;margin:0 auto}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UaePassLoginButtonComponent, decorators: [{
type: Component,
args: [{ selector: 'uae-pass-login-button', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, host: {
class: 'uaepass-login-button',
}, template: `
<button
type="button"
class="uae-pass-login-btn"
[disabled]="disabled()"
(click)="handleLogin()"
[attr.aria-label]="'Sign in with UAE Pass'"
>
<img
[src]="imageSrc()"
alt="Sign with UAE PASS"
class="uae-pass-btn-image"
[class.loading]="disabled()"
[style]="customStyles()"
/>
<div class="loading-spinner" *ngIf="disabled()"></div>
</button>
`, styles: [":host{display:block;width:100%}.uae-pass-login-btn{display:block;width:100%;border:none;background:transparent;padding:0;cursor:pointer;position:relative;transition:all .3s ease}.uae-pass-btn-image{width:100%;height:auto;display:block;transition:all .3s ease}.uae-pass-login-btn:hover:not([disabled]) .uae-pass-btn-image{transform:translateY(-2px);filter:drop-shadow(0 6px 20px rgba(0,0,0,.15))}.uae-pass-login-btn:active:not([disabled]) .uae-pass-btn-image{transform:translateY(0);filter:drop-shadow(0 2px 8px rgba(0,0,0,.1))}.uae-pass-login-btn[disabled] .uae-pass-btn-image{opacity:.6;cursor:not-allowed}.uae-pass-btn-image.loading{opacity:.6}.loading-spinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:24px;height:24px;border:3px solid rgba(0,0,0,.1);border-top:3px solid #1e40af;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:translate(-50%,-50%) rotate(0)}to{transform:translate(-50%,-50%) rotate(360deg)}}@media (max-width: 768px){.uae-pass-btn-image{max-width:280px;margin:0 auto}}\n"] }]
}] });
class UaePassCallbackComponent {
auth = inject(UaePassAuthService);
config = inject(UAE_PASS_CONFIG);
Status = UaePassAuthStatus;
// Optional current URL input (useful for SSR or router-less contexts)
url = input(null);
// Outputs for host app to react
success = output();
failed = output();
status = this.auth.status;
error = this.auth.error;
// Localized texts
texts = computed(() => {
const lang = this.config.language || 'en';
return getUaePassTexts(lang);
});
// RTL support
isRTL = computed(() => {
const lang = this.config.language || 'en';
return lang === 'ar';
});
// React to status changes
_fx = effect(() => {
const s = this.auth.status();
if (s === UaePassAuthStatus.Authenticated) {
this.success.emit();
// Auto-redirect to home after successful authentication
setTimeout(() => {
if (typeof window !== 'undefined') {
window.location.href = '/';
}
}, 2000);
}
if (s === UaePassAuthStatus.Error) {
const msg = this.auth.error();
if (msg)
this.failed.emit(msg);
}
});
constructor() {
// Only handle callback if we're actually on the callback route with a code parameter
const currentUrl = this.url() ?? (typeof window !== 'undefined' ? window.location.href : '');
if (currentUrl && (currentUrl.includes('code=') || currentUrl.includes('error='))) {
// No await to avoid blocking change detection; service sets signals when done
void this.auth.handleRedirectCallback(currentUrl);
}
else {
// If no OAuth parameters, redirect to home immediately
if (typeof window !== 'undefined') {
window.location.href = '/';
}
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UaePassCallbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: UaePassCallbackComponent, isStandalone: true, selector: "uae-pass-callback", inputs: { url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { success: "success", failed: "failed" }, host: { classAttribute: "uaepass-callback" }, ngImport: i0, template: `
<div class="callback-container" [attr.dir]="isRTL() ? 'rtl' : 'ltr'">
(status() === Status.ExchangingToken || status() === Status.Authorizing) {
<div class="loading-state">
<div class="spinner-container">
<div class="spinner"></div>
</div>
<h2 class="status-title">{{ texts().uaePass }}</h2>
<p class="status-message" aria-live="polite">{{ texts().completingSignIn }}</p>
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
</div>
} if (status() === Status.Authenticated) {
<div class="success-state">
<div class="success-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#10b981"/>
<path d="m9 12 2 2 4-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="status-title success">{{ texts().signedInSuccessfully }}</h2>
<p class="status-message" aria-live="polite">{{ texts().redirectingToHomePage }}</p>
<div class="countdown-dots">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
} if (status() === Status.Error) {
<div class="error-state">
<div class="error-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#ef4444"/>
<path d="m15 9-6 6m0-6 6 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="status-title error">{{ texts().error }}</h2>
<p class="error-message" role="alert">{{ error() }}</p>
<a href="/" class="return-button">{{ texts().returnToHome }}</a>
</div>
} {
<div class="loading-state">
<div class="spinner-container">
<div class="spinner"></div>
</div>
<h2 class="status-title">{{ texts().uaePass }}</h2>
<p class="status-message">{{ texts().redirectingToHomePage }}</p>
</div>
}
</div>
`, isInline: true, styles: [":host{display:block;min-height:100vh;background:linear-gradient(135deg,#667eea,#764ba2);font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.callback-container{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:2rem;text-align:center}.loading-state,.success-state,.error-state{background:#fffffff2;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:24px;padding:3rem 2rem;max-width:400px;width:100%;box-shadow:0 20px 40px #0000001a;border:1px solid rgba(255,255,255,.2)}.spinner-container{margin-bottom:2rem}.spinner{width:64px;height:64px;border:4px solid #e5e7eb;border-top:4px solid #1e40af;border-radius:50%;animation:spin 1s linear infinite;margin:0 auto}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.status-title{font-size:1.5rem;font-weight:700;color:#1f2937;margin:0 0 1rem;line-height:1.3}.status-title.success{color:#059669}.status-title.error{color:#dc2626}.status-message{font-size:1rem;color:#6b7280;margin:0 0 2rem;line-height:1.5}.error-message{font-size:.95rem;color:#dc2626;margin:0 0 2rem;padding:1rem;background:#fef2f2;border:1px solid #fecaca;border-radius:12px;line-height:1.5}.progress-bar{width:100%;height:4px;background:#e5e7eb;border-radius:2px;overflow:hidden}.progress-fill{height:100%;background:linear-gradient(90deg,#1e40af,#3b82f6);border-radius:2px;animation:progress 2s ease-in-out infinite}@keyframes progress{0%{width:0%}50%{width:70%}to{width:100%}}.success-icon,.error-icon{margin-bottom:1.5rem;animation:scaleIn .5s ease-out}@keyframes scaleIn{0%{transform:scale(0)}to{transform:scale(1)}}.countdown-dots{display:flex;justify-content:center;gap:.5rem;margin-top:1rem}.dot{width:8px;height:8px;background:#9ca3af;border-radius:50%;animation:pulse 1.5s ease-in-out infinite}.dot:nth-child(2){animation-delay:.3s}.dot:nth-child(3){animation-delay:.6s}@keyframes pulse{0%,to{opacity:.3}50%{opacity:1}}.return-button{display:inline-block;background:#1e40af;color:#fff;text-decoration:none;padding:.75rem 2rem;border-radius:12px;font-weight:600;transition:all .3s ease;border:none;cursor:pointer}.return-button:hover{background:#1d4ed8;transform:translateY(-2px);box-shadow:0 8px 20px #1e40af4d}.return-button:active{transform:translateY(0)}[dir=rtl] .callback-container{font-family:Noto Sans Arabic,Inter,-apple-system,BlinkMacSystemFont,sans-serif}[dir=rtl] .status-title,[dir=rtl] .status-message,[dir=rtl] .error-message{text-align:right}@media (max-width: 768px){.callback-container{padding:1rem}.loading-state,.success-state,.error-state{padding:2rem 1.5rem;border-radius:16px}.status-title{font-size:1.25rem}.spinner{width:48px;height:48px}}@media (prefers-color-scheme: dark){:host{background:linear-gradient(135deg,#1e293b,#334155)}.loading-state,.success-state,.error-state{background:#0f172af2;border:1px solid rgba(255,255,255,.1)}.status-title{color:#f8fafc}.status-message{color:#cbd5e1}.spinner{border-color:#475569;border-top-color:#3b82f6}.progress-bar{background:#475569}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UaePassCallbackComponent, decorators: [{
type: Component,
args: [{ selector: 'uae-pass-callback', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'uaepass-callback' }, template: `
<div class="callback-container" [attr.dir]="isRTL() ? 'rtl' : 'ltr'">
(status() === Status.ExchangingToken || status() === Status.Authorizing) {
<div class="loading-state">
<div class="spinner-container">
<div class="spinner"></div>
</div>
<h2 class="status-title">{{ texts().uaePass }}</h2>
<p class="status-message" aria-live="polite">{{ texts().completingSignIn }}</p>
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
</div>
} if (status() === Status.Authenticated) {
<div class="success-state">
<div class="success-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#10b981"/>
<path d="m9 12 2 2 4-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="status-title success">{{ texts().signedInSuccessfully }}</h2>
<p class="status-message" aria-live="polite">{{ texts().redirectingToHomePage }}</p>
<div class="countdown-dots">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
} if (status() === Status.Error) {
<div class="error-state">
<div class="error-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#ef4444"/>
<path d="m15 9-6 6m0-6 6 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="status-title error">{{ texts().error }}</h2>
<p class="error-message" role="alert">{{ error() }}</p>
<a href="/" class="return-button">{{ texts().returnToHome }}</a>
</div>
} {
<div class="loading-state">
<div class="spinner-container">
<div class="spinner"></div>
</div>
<h2 class="status-title">{{ texts().uaePass }}</h2>
<p class="status-message">{{ texts().redirectingToHomePage }}</p>
</div>
}
</div>
`, styles: [":host{display:block;min-height:100vh;background:linear-gradient(135deg,#667eea,#764ba2);font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.callback-container{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:2rem;text-align:center}.loading-state,.success-state,.error-state{background:#fffffff2;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:24px;padding:3rem 2rem;max-width:400px;width:100%;box-shadow:0 20px 40px #0000001a;border:1px solid rgba(255,255,255,.2)}.spinner-container{margin-bottom:2rem}.spinner{width:64px;height:64px;border:4px solid #e5e7eb;border-top:4px solid #1e40af;border-radius:50%;animation:spin 1s linear infinite;margin:0 auto}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.status-title{font-size:1.5rem;font-weight:700;color:#1f2937;margin:0 0 1rem;line-height:1.3}.status-title.success{color:#059669}.status-title.error{color:#dc2626}.status-message{font-size:1rem;color:#6b7280;margin:0 0 2rem;line-height:1.5}.error-message{font-size:.95rem;color:#dc2626;margin:0 0 2rem;padding:1rem;background:#fef2f2;border:1px solid #fecaca;border-radius:12px;line-height:1.5}.progress-bar{width:100%;height:4px;background:#e5e7eb;border-radius:2px;overflow:hidden}.progress-fill{height:100%;background:linear-gradient(90deg,#1e40af,#3b82f6);border-radius:2px;animation:progress 2s ease-in-out infinite}@keyframes progress{0%{width:0%}50%{width:70%}to{width:100%}}.success-icon,.error-icon{margin-bottom:1.5rem;animation:scaleIn .5s ease-out}@keyframes scaleIn{0%{transform:scale(0)}to{transform:scale(1)}}.countdown-dots{display:flex;justify-content:center;gap:.5rem;margin-top:1rem}.dot{width:8px;height:8px;background:#9ca3af;border-radius:50%;animation:pulse 1.5s ease-in-out infinite}.dot:nth-child(2){animation-delay:.3s}.dot:nth-child(3){animation-delay:.6s}@keyframes pulse{0%,to{opacity:.3}50%{opacity:1}}.return-button{display:inline-block;background:#1e40af;color:#fff;text-decoration:none;padding:.75rem 2rem;border-radius:12px;font-weight:600;transition:all .3s ease;border:none;cursor:pointer}.return-button:hover{background:#1d4ed8;transform:translateY(-2px);box-shadow:0 8px 20px #1e40af4d}.return-button:active{transform:translateY(0)}[dir=rtl] .callback-container{font-family:Noto Sans Arabic,Inter,-apple-system,BlinkMacSystemFont,sans-serif}[dir=rtl] .status-title,[dir=rtl] .status-message,[dir=rtl] .error-message{text-align:right}@media (max-width: 768px){.callback-container{padding:1rem}.loading-state,.success-state,.error-state{padding:2rem 1.5rem;border-radius:16px}.status-title{font-size:1.25rem}.spinner{width:48px;height:48px}}@media (prefers-color-scheme: dark){:host{background:linear-gradient(135deg,#1e293b,#334155)}.loading-state,.success-state,.error-state{background:#0f172af2;border:1px solid rgba(255,255,255,.1)}.status-title{color:#f8fafc}.status-message{color:#cbd5e1}.spinner{border-color:#475569;border-top-color:#3b82f6}.progress-bar{background:#475569}}\n"] }]
}], ctorParameters: () => [] });
/*
* Public API surface of sensei-uaepass
*/
// Configuration and types
/**
* Generated bundle index. Do not edit.
*/
export { CodeChallengeMethod, OAuthGrantType, OAuthResponseType, UAE_PASS_ACR, UAE_PASS_BASE_URL, UAE_PASS_CONFIG, UaePassAcr, UaePassAuthService, UaePassAuthStatus, UaePassCallbackComponent, UaePassEnvironment, UaePassLanguageCode, UaePassLoginButtonComponent, UaePassStorageMode, authorizeUrl, baseUrl, logoutUrl, provideUaePass, tokenUrl, userInfoUrl };
//# sourceMappingURL=sensei-uaepass.mjs.map