@salesforce/ui-bundle-template-feature-angular-authentication 12.7.0 → 12.8.0-canary.1019.37.1.g1f7b157f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [12.8.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v12.7.0...v12.8.0) (2026-09-23)
7
+
8
+ **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
9
+
10
+
11
+
12
+
13
+
6
14
  ## [12.7.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v12.6.3...v12.7.0) (2026-09-22)
7
15
 
8
16
 
@@ -0,0 +1,82 @@
1
+ Terms of Use
2
+
3
+ Copyright 2026 Salesforce, Inc. All rights reserved.
4
+
5
+ These Terms of Use govern the download, installation, and/or use of this
6
+ software provided by Salesforce, Inc. ("Salesforce") (the "Software"), were
7
+ last updated on April 15, 2025, and constitute a legally binding
8
+ agreement between you and Salesforce. If you do not agree to these Terms of
9
+ Use, do not install or use the Software.
10
+
11
+ Salesforce grants you a worldwide, non-exclusive, no-charge, royalty-free
12
+ copyright license to reproduce, prepare derivative works of, publicly
13
+ display, publicly perform, sublicense, and distribute the Software and
14
+ derivative works subject to these Terms. These Terms shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ Subject to the limited rights expressly granted hereunder, Salesforce
18
+ reserves all rights, title, and interest in and to all intellectual
19
+ property subsisting in the Software. No rights are granted to you hereunder
20
+ other than as expressly set forth herein. Users residing in countries on
21
+ the United States Office of Foreign Assets Control sanction list, or which
22
+ are otherwise subject to a US export embargo, may not use the Software.
23
+
24
+ Implementation of the Software may require development work, for which you
25
+ are responsible. The Software may contain bugs, errors and
26
+ incompatibilities and is made available on an AS IS basis without support,
27
+ updates, or service level commitments.
28
+
29
+ Salesforce reserves the right at any time to modify, suspend, or
30
+ discontinue, the Software (or any part thereof) with or without notice. You
31
+ agree that Salesforce shall not be liable to you or to any third party for
32
+ any modification, suspension, or discontinuance.
33
+
34
+ You agree to defend Salesforce against any claim, demand, suit or
35
+ proceeding made or brought against Salesforce by a third party arising out
36
+ of or accruing from (a) your use of the Software, and (b) any application
37
+ you develop with the Software that infringes any copyright, trademark,
38
+ trade secret, trade dress, patent, or other intellectual property right of
39
+ any person or defames any person or violates their rights of publicity or
40
+ privacy (each a "Claim Against Salesforce"), and will indemnify Salesforce
41
+ from any damages, attorney fees, and costs finally awarded against
42
+ Salesforce as a result of, or for any amounts paid by Salesforce under a
43
+ settlement approved by you in writing of, a Claim Against Salesforce,
44
+ provided Salesforce (x) promptly gives you written notice of the Claim
45
+ Against Salesforce, (y) gives you sole control of the defense and
46
+ settlement of the Claim Against Salesforce (except that you may not settle
47
+ any Claim Against Salesforce unless it unconditionally releases Salesforce
48
+ of all liability), and (z) gives you all reasonable assistance, at your
49
+ expense.
50
+
51
+ WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, THE SOFTWARE IS NOT
52
+ SUPPORTED AND IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
53
+ IMPLIED. IN NO EVENT SHALL SALESFORCE HAVE ANY LIABILITY FOR ANY DAMAGES,
54
+ INCLUDING, BUT NOT LIMITED TO, DIRECT, INDIRECT, SPECIAL, INCIDENTAL,
55
+ PUNITIVE, OR CONSEQUENTIAL DAMAGES, OR DAMAGES BASED ON LOST PROFITS, DATA,
56
+ OR USE, IN CONNECTION WITH THE SOFTWARE, HOWEVER CAUSED AND WHETHER IN
57
+ CONTRACT, TORT, OR UNDER ANY OTHER THEORY OF LIABILITY, WHETHER OR NOT YOU
58
+ HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
59
+
60
+ These Terms of Use shall be governed exclusively by the internal laws of
61
+ the State of California, without regard to its conflicts of laws
62
+ rules. Each party hereby consents to the exclusive jurisdiction of the
63
+ state and federal courts located in San Francisco County, California to
64
+ adjudicate any dispute arising out of or relating to these Terms of Use and
65
+ the download, installation, and/or use of the Software. Except as expressly
66
+ stated herein, these Terms of Use constitute the entire agreement between
67
+ the parties, and supersede all prior and contemporaneous agreements,
68
+ proposals, or representations, written or oral, concerning their subject
69
+ matter. No modification, amendment, or waiver of any provision of these
70
+ Terms of Use shall be effective unless it is by an update to these Terms of
71
+ Use that Salesforce makes available, or is in writing and signed by the
72
+ party against whom the modification, amendment, or waiver is to be
73
+ asserted.
74
+
75
+ Data Privacy: Salesforce may collect, process, and store device,
76
+ system, and other information related to your use of the Software. This
77
+ information includes, but is not limited to, IP address, user metrics, and
78
+ other data ("Usage Data"). Salesforce may use Usage Data for analytics,
79
+ product development, and marketing purposes. You acknowledge that files
80
+ generated in conjunction with the Software may contain sensitive or
81
+ confidential data, and you are solely responsible for anonymizing and
82
+ protecting such data.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * REST endpoint for Social Login configuration on React Experience Cloud sites.
3
+ *
4
+ * Endpoint: GET /services/apexrest/auth/social-login-config?startURL=/
5
+ *
6
+ * This replicates the behavior of the OOTB community_login/socialLogin LWC:
7
+ * - applauncher.SocialLoginController.getAuthProviders()
8
+ * - applauncher.SocialLoginController.getSamlProviders()
9
+ * - applauncher.SocialLoginController.getCommunityDomainSsoUrl()
10
+ * - applauncher.SocialLoginController.getSamlSsoUrl()
11
+ * - applauncher.SocialLoginController.handleIdp()
12
+ *
13
+ * The OOTB LWC does not render on React (Site Container) sites due to a platform
14
+ * restriction (isSiteContainer check in node-loginsettings.ftl).
15
+ * This endpoint enables a custom React component to achieve the same functionality.
16
+ *
17
+ * @see https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_Auth_AuthConfiguration.htm
18
+ */
19
+ @RestResource(urlMapping='/auth/social-login-config')
20
+ global with sharing class UIBundleSocialLoginConfig {
21
+
22
+ @HttpGet
23
+ global static SocialLoginConfigResponse getConfig() {
24
+ try {
25
+ // Resolve the post-authentication redirect path, sanitized against
26
+ // open-redirect attacks (CWE-601). See resolveStartUrl.
27
+ String startUrl = resolveStartUrl();
28
+
29
+ String communityUrl = Site.getBaseUrl();
30
+ Auth.AuthConfiguration authConfig = new Auth.AuthConfiguration(communityUrl, startUrl);
31
+
32
+ List<AuthProviderInfo> authProviders = new List<AuthProviderInfo>();
33
+ List<SamlProviderInfo> samlProviders = new List<SamlProviderInfo>();
34
+ String autoRedirectUrl = null;
35
+
36
+ // Fetch OAuth Auth Providers (Google, Facebook, etc.)
37
+ List<AuthProvider> rawAuthProviders = authConfig.getAuthProviders();
38
+ for (AuthProvider ap : rawAuthProviders) {
39
+ String ssoUrl = Auth.AuthConfiguration.getAuthProviderSsoDomainUrl(
40
+ communityUrl, startUrl, ap.DeveloperName
41
+ );
42
+ authProviders.add(new AuthProviderInfo(
43
+ ap.DeveloperName,
44
+ ap.FriendlyName,
45
+ ap.IconUrl,
46
+ ssoUrl
47
+ ));
48
+ }
49
+
50
+ // Fetch SAML SSO Providers
51
+ List<SamlSsoConfig> rawSamlProviders = authConfig.getSamlProviders();
52
+ for (SamlSsoConfig saml : rawSamlProviders) {
53
+ String ssoUrl = Auth.AuthConfiguration.getSamlSsoUrl(
54
+ communityUrl, startUrl, saml.Id
55
+ );
56
+ samlProviders.add(new SamlProviderInfo(
57
+ saml.Id,
58
+ saml.MasterLabel,
59
+ ssoUrl
60
+ ));
61
+ }
62
+
63
+ // handleIdp behavior: auto-redirect if exactly one IDP and no username/password login
64
+ Integer totalProviders = rawAuthProviders.size() + rawSamlProviders.size();
65
+ if (totalProviders == 1 && !authConfig.getUsernamePasswordEnabled()) {
66
+ if (!authProviders.isEmpty()) {
67
+ autoRedirectUrl = authProviders[0].ssoUrl;
68
+ } else if (!samlProviders.isEmpty()) {
69
+ autoRedirectUrl = samlProviders[0].ssoUrl;
70
+ }
71
+ }
72
+
73
+ return new SocialLoginConfigResponse(authProviders, samlProviders, autoRedirectUrl);
74
+ } catch (Exception ex) {
75
+ System.debug(LoggingLevel.ERROR, 'UIBundleSocialLoginConfig error: ' + ex.getMessage());
76
+ return new SocialLoginConfigResponse(
77
+ new List<AuthProviderInfo>(),
78
+ new List<SamlProviderInfo>(),
79
+ null
80
+ );
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Resolves the post-authentication redirect path from the request and
86
+ * sanitizes it to prevent open-redirect attacks (CWE-601).
87
+ *
88
+ * Delegates to the shared UIBundleAuthUtils.getSanitizedStartUrl — the same
89
+ * validation used by UIBundleLogin and UIBundleRegistration — which
90
+ * URL-decodes the value (catching encoded bypasses such as %2f%2f) and then
91
+ * rejects absolute, protocol-relative ("//"), backslash, "@", ":" and
92
+ * control-character inputs, falling back to the community path prefix. Only
93
+ * safe, community-relative paths survive, prefixed with Site.getPathPrefix()
94
+ * so the redirect lands on this community rather than the org default.
95
+ */
96
+ @TestVisible
97
+ private static String resolveStartUrl() {
98
+ return UIBundleAuthUtils.getSanitizedStartUrl(
99
+ RestContext.request.params.get('startURL'),
100
+ Site.getPathPrefix()
101
+ );
102
+ }
103
+
104
+ global class SocialLoginConfigResponse {
105
+ global List<AuthProviderInfo> authProviders;
106
+ global List<SamlProviderInfo> samlProviders;
107
+ global String autoRedirectUrl;
108
+
109
+ global SocialLoginConfigResponse(
110
+ List<AuthProviderInfo> authProviders,
111
+ List<SamlProviderInfo> samlProviders,
112
+ String autoRedirectUrl
113
+ ) {
114
+ this.authProviders = authProviders;
115
+ this.samlProviders = samlProviders;
116
+ this.autoRedirectUrl = autoRedirectUrl;
117
+ }
118
+ }
119
+
120
+ global class AuthProviderInfo {
121
+ global String developerName;
122
+ global String friendlyName;
123
+ global String iconUrl;
124
+ global String ssoUrl;
125
+
126
+ global AuthProviderInfo(String developerName, String friendlyName, String iconUrl, String ssoUrl) {
127
+ this.developerName = developerName;
128
+ this.friendlyName = friendlyName;
129
+ this.iconUrl = iconUrl;
130
+ this.ssoUrl = ssoUrl;
131
+ }
132
+ }
133
+
134
+ global class SamlProviderInfo {
135
+ global String id;
136
+ global String masterLabel;
137
+ global String ssoUrl;
138
+
139
+ global SamlProviderInfo(String id, String masterLabel, String ssoUrl) {
140
+ this.id = id;
141
+ this.masterLabel = masterLabel;
142
+ this.ssoUrl = ssoUrl;
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <apiVersion>67.0</apiVersion>
4
+ <status>Active</status>
5
+ </ApexClass>
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Test class for UIBundleSocialLoginConfig REST endpoint.
3
+ *
4
+ * Auth.AuthConfiguration requires a valid community URL so these tests
5
+ * exercise the error-handling path and verify the response wrapper structure.
6
+ * Integration-level testing of actual Auth Provider/SAML discovery happens
7
+ * in the target org via the React component E2E flow.
8
+ */
9
+ @isTest
10
+ private class UIBundleSocialLoginConfig_Test {
11
+
12
+ /**
13
+ * Test that the endpoint returns an empty config when no community context
14
+ * is available (covers the catch block graceful fallback).
15
+ */
16
+ @isTest
17
+ static void testGetConfigNoSiteContext() {
18
+ RestRequest req = new RestRequest();
19
+ req.requestURI = '/services/apexrest/auth/social-login-config';
20
+ req.httpMethod = 'GET';
21
+ req.params.put('startURL', '/');
22
+ RestContext.request = req;
23
+ RestContext.response = new RestResponse();
24
+
25
+ Test.startTest();
26
+ UIBundleSocialLoginConfig.SocialLoginConfigResponse result =
27
+ UIBundleSocialLoginConfig.getConfig();
28
+ Test.stopTest();
29
+
30
+ // Should return empty lists (graceful fallback), not null
31
+ System.assertNotEquals(null, result, 'Response should not be null');
32
+ System.assertNotEquals(null, result.authProviders, 'authProviders list should not be null');
33
+ System.assertNotEquals(null, result.samlProviders, 'samlProviders list should not be null');
34
+ System.assertEquals(0, result.authProviders.size(), 'authProviders should be empty without community context');
35
+ System.assertEquals(0, result.samlProviders.size(), 'samlProviders should be empty without community context');
36
+ System.assertEquals(null, result.autoRedirectUrl, 'autoRedirectUrl should be null without providers');
37
+ }
38
+
39
+ /**
40
+ * Test that the endpoint handles a missing startURL parameter gracefully
41
+ * (defaults to "/").
42
+ */
43
+ @isTest
44
+ static void testGetConfigNoStartUrl() {
45
+ RestRequest req = new RestRequest();
46
+ req.requestURI = '/services/apexrest/auth/social-login-config';
47
+ req.httpMethod = 'GET';
48
+ // Intentionally not setting startURL param
49
+ RestContext.request = req;
50
+ RestContext.response = new RestResponse();
51
+
52
+ Test.startTest();
53
+ UIBundleSocialLoginConfig.SocialLoginConfigResponse result =
54
+ UIBundleSocialLoginConfig.getConfig();
55
+ Test.stopTest();
56
+
57
+ System.assertNotEquals(null, result, 'Response should not be null even without startURL');
58
+ System.assertNotEquals(null, result.authProviders, 'authProviders should not be null');
59
+ System.assertNotEquals(null, result.samlProviders, 'samlProviders should not be null');
60
+ }
61
+
62
+ /**
63
+ * Test the inner class constructors for code coverage.
64
+ */
65
+ @isTest
66
+ static void testInnerClassConstructors() {
67
+ Test.startTest();
68
+
69
+ UIBundleSocialLoginConfig.AuthProviderInfo authInfo =
70
+ new UIBundleSocialLoginConfig.AuthProviderInfo(
71
+ 'TestProvider', 'Test Provider', 'https://example.com/icon.png', 'https://example.com/sso'
72
+ );
73
+ System.assertEquals('TestProvider', authInfo.developerName);
74
+ System.assertEquals('Test Provider', authInfo.friendlyName);
75
+ System.assertEquals('https://example.com/icon.png', authInfo.iconUrl);
76
+ System.assertEquals('https://example.com/sso', authInfo.ssoUrl);
77
+
78
+ UIBundleSocialLoginConfig.SamlProviderInfo samlInfo =
79
+ new UIBundleSocialLoginConfig.SamlProviderInfo(
80
+ '001000000000001', 'Test SAML', 'https://example.com/saml/sso'
81
+ );
82
+ System.assertEquals('001000000000001', samlInfo.id);
83
+ System.assertEquals('Test SAML', samlInfo.masterLabel);
84
+ System.assertEquals('https://example.com/saml/sso', samlInfo.ssoUrl);
85
+
86
+ List<UIBundleSocialLoginConfig.AuthProviderInfo> authList =
87
+ new List<UIBundleSocialLoginConfig.AuthProviderInfo>{ authInfo };
88
+ List<UIBundleSocialLoginConfig.SamlProviderInfo> samlList =
89
+ new List<UIBundleSocialLoginConfig.SamlProviderInfo>{ samlInfo };
90
+
91
+ UIBundleSocialLoginConfig.SocialLoginConfigResponse resp =
92
+ new UIBundleSocialLoginConfig.SocialLoginConfigResponse(
93
+ authList, samlList, 'https://example.com/redirect'
94
+ );
95
+ System.assertEquals(1, resp.authProviders.size());
96
+ System.assertEquals(1, resp.samlProviders.size());
97
+ System.assertEquals('https://example.com/redirect', resp.autoRedirectUrl);
98
+
99
+ Test.stopTest();
100
+ }
101
+
102
+ // ---- Open-redirect sanitization (CWE-601) -------------------------------
103
+ // resolveStartUrl() is the boundary where the endpoint sanitizes the
104
+ // caller-supplied startURL before it is embedded in the SSO redirect URL.
105
+ // These tests assert malicious values never survive that boundary and that
106
+ // legitimate relative paths do.
107
+
108
+ @isTest
109
+ static void resolveStartUrlRejectsAbsoluteUrl() {
110
+ assertNotReflected('https://evil-attacker.com');
111
+ }
112
+
113
+ @isTest
114
+ static void resolveStartUrlRejectsProtocolRelativeUrl() {
115
+ assertNotReflected('//evil-attacker.com');
116
+ }
117
+
118
+ @isTest
119
+ static void resolveStartUrlRejectsBackslashVariant() {
120
+ assertNotReflected('/\\evil-attacker.com');
121
+ }
122
+
123
+ @isTest
124
+ static void resolveStartUrlRejectsEncodedBypass() {
125
+ assertNotReflected('%2f%2fevil-attacker.com');
126
+ }
127
+
128
+ @isTest
129
+ static void resolveStartUrlAllowsRelativePath() {
130
+ setRequest('/dashboard');
131
+ String resolved = UIBundleSocialLoginConfig.resolveStartUrl();
132
+ System.assertNotEquals(null, resolved, 'A valid relative path should resolve to a non-null value');
133
+ System.assert(
134
+ resolved.endsWith('/dashboard'),
135
+ 'A valid relative path must be preserved (optionally prefixed): ' + resolved
136
+ );
137
+ System.assert(!resolved.startsWith('http'), 'Result must not be absolute: ' + resolved);
138
+ }
139
+
140
+ @isTest
141
+ static void getConfigWithMaliciousStartUrlReturnsGracefulEmpty() {
142
+ // End-to-end: a malicious startURL must not crash the endpoint; without
143
+ // a real site context it returns the graceful empty response (unchanged).
144
+ setRequest('https://evil-attacker.com');
145
+
146
+ Test.startTest();
147
+ UIBundleSocialLoginConfig.SocialLoginConfigResponse result =
148
+ UIBundleSocialLoginConfig.getConfig();
149
+ Test.stopTest();
150
+
151
+ System.assertNotEquals(null, result, 'Response should not be null');
152
+ System.assertEquals(0, result.authProviders.size(), 'No providers without site context');
153
+ System.assertEquals(0, result.samlProviders.size(), 'No SAML providers without site context');
154
+ System.assertEquals(null, result.autoRedirectUrl, 'No auto-redirect without providers');
155
+ }
156
+
157
+ // ---- helpers ------------------------------------------------------------
158
+
159
+ /** Asserts a malicious startURL value is not reflected by resolveStartUrl(). */
160
+ private static void assertNotReflected(String maliciousStartUrl) {
161
+ setRequest(maliciousStartUrl);
162
+ String resolved = UIBundleSocialLoginConfig.resolveStartUrl();
163
+ String safe = resolved == null ? '' : resolved;
164
+ System.assert(!safe.contains('evil-attacker'), 'Malicious host must not be reflected: ' + safe);
165
+ System.assert(!safe.startsWith('http'), 'Result must not be an absolute URL: ' + safe);
166
+ System.assert(!safe.startsWith('//'), 'Result must not be protocol-relative: ' + safe);
167
+ System.assert(!safe.contains('\\'), 'Result must not contain a backslash: ' + safe);
168
+ }
169
+
170
+ /** Builds a GET RestContext with the given startURL param (null => param omitted). */
171
+ private static void setRequest(String startUrlValue) {
172
+ RestRequest req = new RestRequest();
173
+ req.requestURI = '/services/apexrest/auth/social-login-config';
174
+ req.httpMethod = 'GET';
175
+ if (startUrlValue != null) {
176
+ req.params.put('startURL', startUrlValue);
177
+ }
178
+ RestContext.request = req;
179
+ RestContext.response = new RestResponse();
180
+ }
181
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <apiVersion>67.0</apiVersion>
4
+ <status>Active</status>
5
+ </ApexClass>
@@ -28,8 +28,8 @@
28
28
  "@angular/router": "^21.2.0",
29
29
  "@ng-icons/core": "^32.2.0",
30
30
  "@ng-icons/lucide": "^32.2.0",
31
- "@salesforce/angular-plugin-ui-bundle": "^12.7.0",
32
- "@salesforce/platform-sdk": "^12.7.0",
31
+ "@salesforce/angular-plugin-ui-bundle": "12.8.0-canary.1019.37.1.g1f7b157f",
32
+ "@salesforce/platform-sdk": "12.8.0-canary.1019.37.1.g1f7b157f",
33
33
  "@spartan-ng/brain": "^1.3.1",
34
34
  "class-variance-authority": "^0.7.1",
35
35
  "clsx": "^2.1.1",
@@ -61,7 +61,7 @@
61
61
  "typescript": "~5.9.2",
62
62
  "typescript-eslint": "^8.0.0",
63
63
  "vitest": "^4.1.11",
64
- "@salesforce/graphiti": "^12.7.0"
64
+ "@salesforce/graphiti": "12.8.0-canary.1019.37.1.g1f7b157f"
65
65
  },
66
66
  "overrides": {
67
67
  "undici": "^7.28.0"
@@ -10,7 +10,7 @@
10
10
  <div hlmDialogHeader>
11
11
  <ng-content select="[header]" />
12
12
  </div>
13
- <div class="text-sm">
13
+ <div class="min-w-0 text-sm">
14
14
  <ng-content />
15
15
  </div>
16
16
  <div hlmDialogFooter>
@@ -12,13 +12,16 @@ import {
12
12
  lucideChevronDown,
13
13
  lucideChevronRight,
14
14
  lucideCircleAlert,
15
+ lucideFile,
15
16
  lucideFileQuestion,
16
17
  lucideGlobe,
18
+ lucideImage,
17
19
  lucideListFilter,
18
20
  lucideLogIn,
19
21
  lucideLogOut,
20
22
  lucideSearch,
21
23
  lucideSearchX,
24
+ lucideUpload,
22
25
  lucideUser,
23
26
  lucideUserPen,
24
27
  lucideUserPlus,
@@ -43,6 +46,9 @@ export const APP_ICONS = {
43
46
  notFound: lucideFileQuestion,
44
47
  globe: lucideGlobe,
45
48
  check: lucideCheck,
49
+ image: lucideImage,
50
+ upload: lucideUpload,
51
+ file: lucideFile,
46
52
  } as const;
47
53
 
48
54
  export type AppIconName = keyof typeof APP_ICONS;
@@ -0,0 +1,39 @@
1
+ @if (hasProviders()) {
2
+ <div class="mt-6">
3
+ @if (showHeader()) {
4
+ <div class="relative">
5
+ <div class="absolute inset-0 flex items-center">
6
+ <span class="w-full border-t"></span>
7
+ </div>
8
+ <div class="relative flex justify-center text-xs uppercase">
9
+ <span class="bg-background px-2 text-muted-foreground">{{ headerText() }}</span>
10
+ </div>
11
+ </div>
12
+ }
13
+
14
+ <div class="mt-4 grid gap-2">
15
+ @for (provider of authProviders(); track provider.developerName) {
16
+ <button
17
+ type="button"
18
+ (click)="redirectTo(provider.ssoUrl)"
19
+ [attr.aria-label]="'Sign in with ' + provider.friendlyName"
20
+ class="inline-flex w-full items-center justify-center gap-2 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
21
+ >
22
+ @if (provider.iconUrl) {
23
+ <img [src]="provider.iconUrl" alt="" class="h-4 w-4" aria-hidden="true" />
24
+ }
25
+ <span>{{ provider.friendlyName }}</span>
26
+ </button>
27
+ } @for (provider of samlProviders(); track provider.id) {
28
+ <button
29
+ type="button"
30
+ (click)="redirectTo(provider.ssoUrl)"
31
+ [attr.aria-label]="'Sign in with ' + provider.masterLabel"
32
+ class="inline-flex w-full items-center justify-center gap-2 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
33
+ >
34
+ <span>{{ provider.masterLabel }}</span>
35
+ </button>
36
+ }
37
+ </div>
38
+ </div>
39
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ import {
7
+ ChangeDetectionStrategy,
8
+ Component,
9
+ type OnInit,
10
+ computed,
11
+ inject,
12
+ input,
13
+ signal,
14
+ } from "@angular/core";
15
+ import { ActivatedRoute } from "@angular/router";
16
+ import { DataClient } from "../../../api/data-client.service";
17
+ import { getStartUrl } from "../utils/auth-helpers";
18
+
19
+ interface AuthProviderInfo {
20
+ developerName: string;
21
+ friendlyName: string;
22
+ iconUrl: string | null;
23
+ ssoUrl: string;
24
+ }
25
+
26
+ interface SamlProviderInfo {
27
+ id: string;
28
+ masterLabel: string;
29
+ ssoUrl: string;
30
+ }
31
+
32
+ interface SocialLoginConfigResponse {
33
+ authProviders: AuthProviderInfo[];
34
+ samlProviders: SamlProviderInfo[];
35
+ autoRedirectUrl: string | null;
36
+ }
37
+
38
+ /**
39
+ * Social Login (IDP) component — Angular port of the React `SocialLogin`.
40
+ *
41
+ * Replicates the OOTB community_login/socialLogin LWC behavior which does not
42
+ * render on Site Container sites due to a platform restriction (isSiteContainer
43
+ * check in node-loginsettings.ftl).
44
+ *
45
+ * Behavior:
46
+ * - On init, fetches configured Auth Providers + SAML Providers
47
+ * - If only one IDP is configured and username/password is disabled, auto-redirects
48
+ * - Renders a button per provider with icon (Auth Providers) or without (SAML)
49
+ * - On click, redirects to the SSO URL to initiate the OAuth/SAML flow
50
+ *
51
+ * Backend: UIBundleSocialLoginConfig.cls (Apex REST endpoint), copied verbatim
52
+ * from the React feature. Uses the Auth.AuthConfiguration API.
53
+ */
54
+ @Component({
55
+ selector: "app-social-login",
56
+ changeDetection: ChangeDetectionStrategy.OnPush,
57
+ templateUrl: "./social-login.html",
58
+ })
59
+ export class SocialLoginComponent implements OnInit {
60
+ private readonly route = inject(ActivatedRoute);
61
+ private readonly dataClient = inject(DataClient);
62
+
63
+ /** Whether to show the header text above the buttons. Default: true */
64
+ readonly showHeader = input(true);
65
+
66
+ /** Header text. Default: "Or log in using:" */
67
+ readonly headerText = input("Or log in using:");
68
+
69
+ protected readonly authProviders = signal<AuthProviderInfo[]>([]);
70
+ protected readonly samlProviders = signal<SamlProviderInfo[]>([]);
71
+ protected readonly hasProviders = computed(
72
+ () => this.authProviders().length > 0 || this.samlProviders().length > 0,
73
+ );
74
+
75
+ async ngOnInit(): Promise<void> {
76
+ const startUrl = getStartUrl(this.route.snapshot.queryParamMap);
77
+ const config = await this.fetchConfig(startUrl);
78
+ if (!config) {
79
+ return;
80
+ }
81
+ if (config.autoRedirectUrl) {
82
+ window.location.assign(config.autoRedirectUrl);
83
+ return;
84
+ }
85
+ this.authProviders.set(config.authProviders ?? []);
86
+ this.samlProviders.set(config.samlProviders ?? []);
87
+ }
88
+
89
+ protected redirectTo(ssoUrl: string): void {
90
+ window.location.assign(ssoUrl);
91
+ }
92
+
93
+ private async fetchConfig(startUrl: string): Promise<SocialLoginConfigResponse | null> {
94
+ try {
95
+ const response = await this.dataClient.fetch(
96
+ `/services/apexrest/auth/social-login-config?startURL=${encodeURIComponent(startUrl)}`,
97
+ { method: "GET", headers: { Accept: "application/json" } },
98
+ );
99
+ if (!response.ok) {
100
+ return null;
101
+ }
102
+ return (await response.json()) as SocialLoginConfigResponse;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ }
@@ -43,5 +43,7 @@
43
43
  </app-field>
44
44
  </div>
45
45
  </app-auth-form>
46
+
47
+ <app-social-login />
46
48
  </div>
47
49
  </main>
@@ -8,6 +8,7 @@ import { FormBuilder } from "@angular/forms";
8
8
  import { ActivatedRoute, Router, RouterLink } from "@angular/router";
9
9
  import { DataClient } from "../../api/data-client.service";
10
10
  import { AuthFormComponent } from "../../features/authentication/auth-form/auth-form";
11
+ import { SocialLoginComponent } from "../../features/authentication/social-login/social-login";
11
12
  import { ReactiveFormPageBase } from "../../features/authentication/reactive-form-page.base";
12
13
  import { FieldComponent } from "../../components/ui/field/field";
13
14
  import { InputComponent } from "../../components/ui/input/input";
@@ -32,7 +33,7 @@ import {
32
33
  @Component({
33
34
  selector: "app-login-page",
34
35
  changeDetection: ChangeDetectionStrategy.OnPush,
35
- imports: [RouterLink, AuthFormComponent, FieldComponent, InputComponent],
36
+ imports: [RouterLink, AuthFormComponent, FieldComponent, InputComponent, SocialLoginComponent],
36
37
  templateUrl: "./login-page.html",
37
38
  })
38
39
  export class LoginPageComponent extends ReactiveFormPageBase {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "12.7.0",
3
+ "version": "12.8.0-canary.1019.37.1.g1f7b157f",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
9
- "version": "12.7.0",
9
+ "version": "12.8.0-canary.1019.37.1.g1f7b157f",
10
10
  "license": "SEE LICENSE IN LICENSE.txt",
11
11
  "dependencies": {
12
12
  "fast-xml-parser": "^5.9.3",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "12.7.0",
3
+ "version": "12.8.0-canary.1019.37.1.g1f7b157f",
4
4
  "description": "Base SFDX project template",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "publishConfig": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-feature-angular-authentication",
3
- "version": "12.7.0",
3
+ "version": "12.8.0-canary.1019.37.1.g1f7b157f",
4
4
  "private": false,
5
5
  "engines": {
6
6
  "node": ">=22"