Implementation Lifecycle: Personas
Commerce SFDX Environment Setup
Implement Custom Self-Registration for a B2B Store
Commerce LWR Storefront Performance Best Practices
Image Optimization Best Practices
Custom Rules for Product Readiness
Implement automated customer self-registration for Salesforce B2B Commerce storefronts using a custom Lightning Web Component and an Apex controller that orchestrates user provisioning, account creation, buyer access, and permission assignment.
The self-registration solution consists of two primary components:
Use this implementation when you want to:
Customize the store’s registration page using custom Lightning Web Components (LWC) and Apex code. For a sample Apex class, see the CommerceSelfRegistrationController.cls class.
Before you begin:
Add the new UI component to your store’s self-registration page.
a. In the navigation sidebar, select a store from the Store dropdown.
b. Select Website Design, and then click Experience Builder.
c. In Experience Builder, go to the Self-registration page.
d. To view the properties, click the page.
e. Drag and drop the component on the page.
f. Configure the component properties.
g. Publish your store.
Grant CommerceSelfRegistrationController.cls access to guest users.
Note: Grant guest users access only to the Apex classes required for self-registration. Don’t grant access to unrelated classes or classes that expose sensitive operations.
Don’t grant access to unrelated classes or classes that expose sensitive operations.
a. In the navigation sidebar, select a store from the Store dropdown.
b. Select Website Design, and then click Experience Builder.
c. In Experience Builder, go to Settings, and then click General.
d. Click the Guest User Profile.
e. In the Enabled Apex Class Access section, click Edit.
f. Add the Apex class to the list of enabled classes, and save your changes.
The CommerceSelfRegistrationController class is the core backend component that handles all server-side operations for customer self-registration.
The following sample shows a CommerceSelfRegistrationController class implementation.
1/**
2 * Self-registration controller for Experience Cloud with B2B Commerce integration
3 */
4global without sharing class CommerceSelfRegistrationController {
5
6 private static final String LOG_PREFIX = '[CommerceSelfReg]';
7
8 public class RegistrationException extends Exception {}
9
10 global class RegistrationResult {
11 @AuraEnabled global Boolean success;
12 @AuraEnabled global String message;
13 @AuraEnabled global String redirectUrl;
14 }
15
16 /**
17 * Register a new user with BuyerAccount and auto-login
18 * Returns: RegistrationResult with success status, message, and redirect URL
19 */
20 @AuraEnabled(cacheable=false)
21 global static RegistrationResult registerUserAndRedirectForLogin(
22 String firstName,
23 String lastName,
24 String email,
25 String phoneNumber,
26 String password,
27 String confirmPassword,
28 String startUrl
29 ) {
30 RegistrationResult result = new RegistrationResult();
31 result.success = false;
32
33 Savepoint sp = Database.setSavepoint();
34
35 try {
36 String validationError = validateRegistrationInput(lastName, email, password, confirmPassword);
37 if (validationError != null) {
38 result.message = validationError;
39 return result;
40 }
41
42 String permissionSetGroupId = getPermissionSetGroupIdFromNetwork();
43
44 if (String.isBlank(startUrl)) {
45 startUrl = '';
46 }
47
48 String accountId = createBusinessAccount(firstName, lastName);
49 String userId = createUser(firstName, lastName, email, phoneNumber, password, accountId);
50
51 createBuyerAccount(accountId);
52
53 // Add buyer to all default buyer groups from config
54 List<String> defaultBuyerGroupIds = getDefaultBuyerGroupsFromConfig();
55 if (!defaultBuyerGroupIds.isEmpty()) {
56 addBuyerToDefaultGroups(accountId, defaultBuyerGroupIds);
57 }
58
59 assignPermissionSetToUserAsync(userId, permissionSetGroupId);
60 performAutoLogin(email, password, startUrl, result);
61
62 } catch (Exception ex) {
63 Database.rollback(sp);
64 result.success = false;
65 result.message = 'Registration error: ' + ex.getMessage();
66 System.debug(LOG_PREFIX + ' Registration exception: ' + ex.getMessage());
67 System.debug(LOG_PREFIX + ' Stack trace: ' + ex.getStackTraceString());
68 }
69
70 return result;
71 }
72
73 /**
74 * Validate registration input fields
75 * Returns: Error message if validation fails, null if valid
76 */
77 private static String validateRegistrationInput(String lastName, String email, String password, String confirmPassword) {
78 if (String.isBlank(lastName)) {
79 return 'Last name is required';
80 }
81
82 if (String.isBlank(email)) {
83 return 'Email is required';
84 }
85
86 if (String.isBlank(password)) {
87 return 'Password is required';
88 }
89
90 if (password != confirmPassword) {
91 return 'Passwords do not match';
92 }
93
94 return null;
95 }
96
97 /**
98 * Create external user using Site.createExternalUser()
99 * Returns: User ID
100 */
101 private static String createUser(String firstName, String lastName, String email,
102 String phoneNumber, String password, String accountId) {
103 try {
104 User u = new User();
105 u.FirstName = firstName;
106 u.LastName = lastName;
107 u.Email = email;
108 u.Username = email;
109 u.CommunityNickname = generateNickname(firstName, lastName, email);
110 u.EmailEncodingKey = 'UTF-8';
111 u.LocaleSidKey = 'en_US';
112 u.LanguageLocaleKey = 'en_US';
113 u.TimeZoneSidKey = 'America/Los_Angeles';
114 u.Alias = generateAlias(firstName, lastName);
115
116 if (!String.isBlank(phoneNumber)) {
117 u.Phone = phoneNumber;
118 }
119
120 String userId = Site.createExternalUser(u, accountId, password);
121
122 if (userId == null) {
123 throw new RegistrationException('Site.createExternalUser returned null - user creation failed');
124 }
125
126 System.debug(LOG_PREFIX + ' Created external user: ' + userId);
127 return userId;
128
129 } catch (Exception ex) {
130 System.debug(LOG_PREFIX + ' Error creating user: ' + ex.getMessage());
131 System.debug(LOG_PREFIX + ' Stack trace: ' + ex.getStackTraceString());
132 throw ex;
133 }
134 }
135
136 /**
137 * This a sample implementation and customer's need to provider their own implementation
138 * Generate unique community nickname
139 * Returns: Nickname string (max 40 chars)
140 */
141 private static String generateNickname(String firstName, String lastName, String email) {
142 String nickname = (firstName != null ? firstName : '') + (lastName != null ? lastName : '');
143 if (String.isBlank(nickname)) {
144 nickname = email.substringBefore('@');
145 }
146 nickname = nickname.replaceAll('[^a-zA-Z0-9]', '');
147 nickname = nickname + String.valueOf(System.now().getTime()).substring(7);
148 return nickname.substring(0, Math.min(nickname.length(), 40));
149 }
150
151 /**
152 * This a sample implementation and customer's need to provider their own implementation
153 * Generate user alias.
154 * Returns: Alias string (max 8 chars)
155 */
156 private static String generateAlias(String firstName, String lastName) {
157 String alias = '';
158 if (firstName != null) alias += firstName.substring(0, Math.min(firstName.length(), 4));
159 if (lastName != null) alias += lastName.substring(0, Math.min(lastName.length(), 4));
160 if (String.isBlank(alias)) alias = 'user';
161 return alias.substring(0, Math.min(alias.length(), 8));
162 }
163
164 /**
165 * Perform auto-login after successful registration and update result object
166 */
167 private static void performAutoLogin(String email, String password, String startUrl, RegistrationResult result) {
168 try {
169 PageReference loginResult = Site.login(email, password, startUrl);
170 if (loginResult != null) {
171 result.redirectUrl = loginResult.getUrl();
172 result.success = true;
173 result.message = 'Registration and login successful!';
174 System.debug(LOG_PREFIX + ' Auto-login successful, redirect URL: ' + result.redirectUrl);
175 } else {
176 result.success = true;
177 result.redirectUrl = null;
178 result.message = 'Registration successful but auto-login failed. Please login manually.';
179 System.debug(LOG_PREFIX + ' Auto-login failed - Site.login returned null');
180 }
181 } catch (Exception loginEx) {
182 result.success = true;
183 result.redirectUrl = null;
184 result.message = 'Registration successful but auto-login failed. Please login manually.';
185 System.debug(LOG_PREFIX + ' Auto-login exception: ' + loginEx.getMessage());
186 }
187 }
188
189 /**
190 * Create a business account to be associated with registering user
191 * Returns: Business account ID
192 */
193 private static String createBusinessAccount(String firstName, String lastName) {
194 Account account = new Account();
195 account.Name = firstName + ' ' + lastName;
196
197 String recordTypeId = getDefaultAccountRecordTypeId();
198 if (String.isNotBlank(recordTypeId)) {
199 account.RecordTypeId = recordTypeId;
200 }
201
202 ID siteAdminId = getSiteAdminId();
203 if (siteAdminId != null) {
204 account.OwnerId = siteAdminId;
205 }
206
207 insert account;
208 System.debug(LOG_PREFIX + ' Created business account: ' + account.Id);
209 return account.Id;
210 }
211
212 /**
213 * Get default account record type ID from Commerce configuration
214 * Returns: Account record type ID (15 chars), or null
215 */
216 private static String getDefaultAccountRecordTypeId() {
217 String networkId = Network.getNetworkId();
218
219 List<CommerceConfigRelatedRecord> configs = [
220 SELECT ConfigReferenceId
221 FROM CommerceConfigRelatedRecord
222 WHERE ContextId = :networkId
223 AND ConfigUseCase = 'SelfRegistration'
224 AND ConfigKey = 'AccountRecordType'
225 LIMIT 1
226 ];
227
228 if (!configs.isEmpty() && String.isNotBlank(configs[0].ConfigReferenceId)) {
229 String recordTypeId = configs[0].ConfigReferenceId;
230 if (recordTypeId.length() == 18) {
231 recordTypeId = recordTypeId.substring(0, 15);
232 }
233 System.debug(LOG_PREFIX + ' Found account record type ID: ' + recordTypeId);
234 return recordTypeId;
235 }
236
237 return null;
238 }
239
240 /**
241 * Get site admin ID
242 * Returns: Site admin user ID as ID, or null
243 */
244 private static ID getSiteAdminId() {
245 return Site.getAdminId();
246 }
247
248 /**
249 * Create BuyerAccount for Commerce integration
250 */
251 private static void createBuyerAccount(String accountId) {
252 BuyerAccount buyerAccount = new BuyerAccount(
253 BuyerId = accountId,
254 Name = 'Buyer-' + accountId,
255 IsActive = true
256 );
257 insert buyerAccount;
258 System.debug(LOG_PREFIX + ' Created BuyerAccount: ' + buyerAccount.Id);
259 }
260
261 /**
262 * Add buyer to all default buyer groups
263 */
264 private static void addBuyerToDefaultGroups(String accountId, List<String> defaultBuyerGroupIds) {
265 List<BuyerGroupMember> members = new List<BuyerGroupMember>();
266 for (String buyerGroupId : defaultBuyerGroupIds) {
267 if (String.isNotBlank(buyerGroupId)) {
268 members.add(new BuyerGroupMember(
269 BuyerId = accountId,
270 BuyerGroupId = buyerGroupId
271 ));
272 }
273 }
274
275 if (!members.isEmpty()) {
276 insert members;
277 System.debug(LOG_PREFIX + ' Added account to ' + members.size() + ' default BuyerGroups');
278 }
279 }
280
281 /**
282 * Async wrapper to assign permission set (avoids Mixed DML)
283 */
284 @future
285 private static void assignPermissionSetToUserAsync(String userId, String permissionSetGroupId) {
286 assignPermissionSetToUser(userId, permissionSetGroupId);
287 }
288
289 /**
290 * Assign permission set group to user
291 */
292 private static void assignPermissionSetToUser(String userId, String permissionSetGroupId) {
293 PermissionSetAssignment psa = new PermissionSetAssignment(
294 AssigneeId = userId,
295 PermissionSetGroupId = permissionSetGroupId
296 );
297 insert psa;
298 System.debug(LOG_PREFIX + ' Assigned permission set group to user: ' + psa.Id);
299 }
300
301 /**
302 * Get permission set group ID from NetworkSelfRegistration configuration
303 * Returns: Permission set group ID
304 */
305 private static String getPermissionSetGroupIdFromNetwork() {
306 String networkId = Network.getNetworkId();
307 if (String.isBlank(networkId)) {
308 System.debug(LOG_PREFIX + ' No network ID found');
309 throw new RegistrationException('No network ID found');
310 }
311
312 List<NetworkSelfRegistration> selfRegConfigs = [
313 SELECT PermissionSetGroupId
314 FROM NetworkSelfRegistration
315 WHERE NetworkId = :networkId
316 LIMIT 1
317 ];
318
319 if (selfRegConfigs.isEmpty() || String.isBlank(selfRegConfigs[0].PermissionSetGroupId)) {
320 System.debug(LOG_PREFIX + ' No permission set group configured in NetworkSelfRegistration');
321 throw new RegistrationException('No permission set group configured in NetworkSelfRegistration');
322 }
323
324 String permSetGroupId = selfRegConfigs[0].PermissionSetGroupId;
325 System.debug(LOG_PREFIX + ' Found permission set group ID from NetworkSelfRegistration: ' + permSetGroupId);
326 return permSetGroupId;
327 }
328
329 /**
330 * Get default buyer group IDs from CommerceConfigRelatedRecord
331 * This follows Salesforce's official implementation pattern
332 * Returns: List of BuyerGroup IDs configured for self-registration
333 */
334 private static List<String> getDefaultBuyerGroupsFromConfig() {
335 String networkId = Network.getNetworkId();
336 List<String> buyerGroupIds = new List<String>();
337
338 try {
339 List<CommerceConfigRelatedRecord> configs = [
340 SELECT ConfigReferenceId
341 FROM CommerceConfigRelatedRecord
342 WHERE ContextId = :networkId
343 AND ConfigUseCase = 'SelfRegistration'
344 AND ConfigKey = 'DefaultBuyerGroup'
345 ];
346
347 for (CommerceConfigRelatedRecord config : configs) {
348 if (String.isNotBlank(config.ConfigReferenceId)) {
349 buyerGroupIds.add(config.ConfigReferenceId);
350 }
351 }
352
353 System.debug(LOG_PREFIX + ' Found ' + buyerGroupIds.size() + ' default buyer groups: ' + buyerGroupIds);
354 } catch (Exception e) {
355 System.debug(LOG_PREFIX + ' Error retrieving buyer groups: ' + e.getMessage());
356 throw new RegistrationException('Error retrieving buyer groups');
357 }
358
359 return buyerGroupIds;
360 }
361}