Commonalities
The following explains some of the modules and features that you will need in your application
Angular-CLI generate calls
ng g module
ng g component
ng g service
ng g guard
ng g class
ng g interface
Note everything is generated through the CLI
Modules that you need to import in your app.module.ts file
imports: [
...
NgbModule.forRoot(),
GlobalInterceptorModule,
HttpClientModule,
TrusteerModule,
AppModuleStorybook
],
- The
NgbModuleis required for ngBootstrap (angular powered bootstrap components) - The
AppModuleStorybookis required to get access to all the shared components in io-storybook - The
GlobalInterceptorModuleis required to manage all API requests and respond to them accordingly - The
HttpClientModuleis required to make API calls - The
TrusteerModuleis required to execute specific security scripts in your project
Setting up child routes for your app
The following explains how to setup child routes for applications who need to route between different pages
- point the default route to your base component or app.component
- define the default child route and point it to the component you want to render first
const routes: Routes = [
{
path: '',
redirectTo: '',
pathMatch: 'full'
},
{
path: '',
component: AppComponent,
children: [
{
path: '',
redirectTo: 'portfolio-summary',
pathMatch: 'full'
},
{
path: 'portfolio-summary',
component: PortfolioSummaryComponent
}
]
},
];
Header footer config
The following explains how to pass through properties to platform header and footer for each route. If there is no config, it will use the default config.
In your app routing module:
- Import the resolver and interfcae
import { HeaderFooterConfigInterface, HeaderFooterConfigResolver } from '@investec-online/global-navigation';
- Now add the HeaderFooterConfigInterface
const myAppHeaderFooter: HeaderFooterConfigInterface = {
menuPreference: 'personalBanking', <-- client type
hasNotifications: true, <-- the notification bell
hasLogOut: true, <-- logout button
fullFooter: true, <-- the full footer with drop-down and feedback button
hasMenu: true, <-- the mega menu
fullHeaderWidth: false <--span the complete width of the page
};
- Now pass that object to the route that you want it to apply to.
path: '',
component: MyComponent,
resolve: {data: HeaderFooterConfigResolver},
data: {headerFooterConfig: myAppHeaderFooter},
The config will be used for that route component.
Implementing trusteer
In your app.component.ts you need to add the following to implement trusteer:
constructor(private _ts: TsService){}
ngOnInit() {
if (this.env === 'prod' || this.env === 'staging') {
//trusteer implementation
this._ts.initializeTsForGeneralPages();
}
}
How to make API calls
The following example illustrates how to make a GET call
getUserSettings(){
let params = new HttpParams();
// Begin assigning parameters
params = params.append('', new Date().getTime().toString());
return this._http.get<UserSettings>('/proxy/user/settings',{params: params});
}
- Wrap the API call in a function that returns the HTTP GET call (Required for unit testing purposes)
- Add parameters to the API call if needed (optional)
- You can define the model that we expect back from the API: this._http.get
<UserSettings> - You can also define it inline:
- this._http.get
<{ Bank, Borrow, Save, Invest }><-- (object) - this._http.get
<[{ segments: [any], url }]><--- (array)
You can now subscribe to the API call in your component
this.getUserSettings().subscribe(res => {
this.userSettings = res;
});
The following illustrates how to make a POST call
this._http.post<{ Success: boolean }>('/proxy/feedback', this.feedbackInfo).subscribe(res => {
this.showFeedbackSuccess = res.Success;
});
How to read and write to the state store
In order to reduce the number of calls to the API, we store the client context so that applications can use the cached data instead of hitting the API constantly.
Add the following extension to your browser in order to view all the persisted cached data in the state store:
Redux DevTools
This allows you to view what is currently in the state store.
In order to use the store, you need to import it
import {Store} from "@ngxs/store";
and add it in your constructor
constructor(private _store: Store){ }
the following illustrates how to read user settings from the store
this._store.select(state => state.currencies.currencies).subscribe(currList => {
if(!isUndefined(currList)){
//use currList data here
}
})
You can also control when the store should give you values
this._store.selectOnce(state => state.userSettings.settings).subscribe(value => {
if(!isUndefined(value)){
this.userSettings = value;
}
});
- In the above example, the store will only emit the current store value once.
- for more information about store selectors: https://ngxs.gitbook.io/ngxs/concepts/select
The following illustrates how to write to the store
this._store.dispatch(new SetSelectedProfile(profile));
- The above example is applicable when a user changes their profile and we need to update the store.
- Additionally you would need to import
SetSelectedProfilein your component.
Adding new items to the store needs to be discussed with the platform team and the broader front-end community.
How to update a specific item in the store
The following explains how to update an item in the store using the user settings as an example:
create a class
export class UpdateProfileLoadingPreference {
static readonly type = '[UserSetting] Update'
constructor(public payload: {KeyId,KeyName,Value}) {}
}
Create an action
@Action(UpdateProfileLoadingPreference)
updateProfileLoadingPreference(ctx: StateContext<UserSettingsStateModel>, { payload }: UpdateProfileLoadingPreference) {
ctx.setState(
patch({
settings: updateItem<UserSetting>(item => item.KeyName === payload.KeyName, patch({Value: payload.Value}))
})
);
}
Use it
this.store.dispatch(new UpdateProfileLoadingPreference(payload));
This will update a specific object in an array in the state store.
2FA
In order to use 2fa you need to instansiate the SecondFactorService in your constructor
constructor(private _secondFactorService: SecondFactorService, private _myService: MyService) {}
The following explains how to use 2fa on a specific endpoint.
submitFunction() {
this._myService.postQuickPass(payload).subscribe(res => {
//Catch the second factor filter from the API
if(res['Method']){
let secondFactorStatusSubscription = true;
//Wait for the user to complete second factor
this._secondFactorService.secondFactorStatus.pipe(takeWhile(() =>
secondFactorStatusSubscription)).subscribe(secondFactorStatus => {
//Once 2fa are done check whether it was successful or not
if (secondFactorStatus) {
secondFactorStatusSubscription = false;
this._secondFactorService.secondFactorStatus.next(false);
this.submitFunction(); // Rerun the entire function
}
})
} else {
//logic goes here for when you get the correct API response
}
}, error => {
//Api error response
})
}
Note: This is dependent on the global interceptor module that must be imported into your module.
Transactional Second Factor
In order to use transactional 2fa you need to import the TransactionalSecondFactorModule into your module.
...
imports: [
CommonModule,
AppRoutingModule,
AppModuleStorybook,
TransactionalSecondFactorModule
],
...
Also add the component into the view.
<transactional-second-factor-wrapper></transactional-second-factor-wrapper>
This module will use an HTTP interceptor (txn2fa-interceptor.service.ts) to check the API response for the flag Txn2Fa: true.
if (!httpRequest.headers.has('txn-2fa-service') && httpEvent instanceof HttpResponse) {
const httpResponse = <HttpResponse<Txn2faFallbackResponse>>httpEvent;
if (httpResponse.body.Txn2Fa) {
return this.txn2faService.addNewTxn2fa(httpRequest, httpResponse.body)
}
}
If the flag is true, this will kick off a new 2fa event. This includes passing the request and 2fa response into a queue, which will then handle polling and fallback methods based on what is returned during the 2fa process.
Based on the API response you can get either a Txn2faFallbackResponse or Txn2faPollResponse model (txn2fa.model.ts) returned which will control which second factor method is displayed (otppassword, inappbio, etc...) while also a locking user if too many attempts are made to authenticate, among other things.
Second factor methods include:
SecondFactorMethod {
sms = 'SMS',
ussd = 'USSD',
inapp = 'APP',
otppassword = 'OTPPASSWORD',
inappbio = 'INAPPBIO',
}
Second factor responses are as follows:
SecondFactorResponse {
invalid = 'Invalid',
error = 'Error',
expired = 'Expired',
declined = 'Declined',
noSecondFactor = 'NoSecondFactor',
noResponse = 'NoResponse',
authorised = 'Authorised',
failed = 'Failed',
rejected = 'Rejected'
}
Once the 2fa process is authenticated, the interceptor will allow the original request to continue. If the user is locked out, they will be logged out.
Profile & Account selectors
The following explains how to quickly get the profile and account selectors setup in the platform using the state store. it includes:
- Selected profile
- Selected account
- Available accounts
- base currency
<ui-profile-selector [disableButtons]="false"
[profileList]="profileList"
[isSticky]="false"
[accountList]="accountList"
[selectedProfile]="selectedProfile"
[selectedAccount]="selectedAccount"
[disabled]="isLoading"
(returnProfileEvt)="changeProfile($event)"
(returnAccountEvt)="changeAccount($event)"
[showProfileDropdown]="true"
[showAccountDropdown]="true"></ui-profile-selector>
Typescript:
selectedProfile: SelectedProfile;
selectedAccount: SelectedAccount;
profileList = [];
accountList = [];
availableAccounts: PcAvailableAccounts;
isLoading = true;
constructor(private _store: Store, private _sharedHttp: SharedHttpService, private _userContext: UserContextService) {
//get available profiles (this is for SA only)
this._sharedHttp.getProfileList().subscribe(profileList => {
this.profileList = profileList;
this._store.select(SelectedProfileState.getSelectedProfile).subscribe(profile => {
if (isUndefined(profile)) {
this.profileList.forEach(value => {
if (value.Default) {
//Set the default profile in the store
this._store.dispatch(new SetSelectedProfile(value));
}
})
} else {
this.selectedProfile = profile;
}
});
this._store.select(SelectedAccountState.getSelectedAccount).subscribe(account => {
if (!isUndefined(account)) {
this.selectedAccount = account;
}
});
//get the accounts form the portfolio
this.getAccounts();
});
}
getAccounts(){
this.isLoading = true;
//Make the portfolio call and return it (for example this is the za pb portfolio call)
this._userContext.refreshPrivateClientPortfolioAndReturnIt('za', '/pbv2/portfolio', this.selectedProfile.ProfileId, this._store.selectSnapshot(UserSettingsState.getDefaultCurrency)).then((res) => {
//get all available accounts
this.availableAccounts = this._store.selectSnapshot(PcAvailableAccountsState.getPcAvailableAccounts)
//if there are no availabe accounts, go get it, but exclude the ones you already have in the above portfolio
if (isUndefined(this.availableAccounts)) {
this._userContext.setPrivateClientAccountsListFromPortfolios(['PrivateBankZA'], res['PrivateBankAccounts'],'za').then(() => {
this.availableAccounts = this._store.selectSnapshot(PcAvailableAccountsState.getPcAvailableAccounts)
this.accountList = [...this.availableAccounts];
}
)
} else {
this.accountList = [...this.availableAccounts];
};
this.isLoading = false;
});
//if there is no selected account, take the first one
if(isUndefined(this.selectedAccount)){
this.selectedAccount = this.accountList[0];
}
}
changeProfile(profile){
this._store.dispatch(new SetSelectedProfile(profile));
this.getAccounts();
}
changeAccount(account){
//if you select an account that is not part of your page's context. for instance selecting a UK account on SA page
if(account.Country !== 'ZAF'){
//Go the the equivalent page for that account. in this case beneficiary page
console.log(this._userContext.mapGeoLinkedPrivateClientAccountURL(account,'beneficiary'));
} else {
this._store.dispatch(new SetSelectedAccount(account));
}
}