Skip to main content

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 NgbModule is required for ngBootstrap (angular powered bootstrap components)
  • The AppModuleStorybook is required to get access to all the shared components in io-storybook
  • The GlobalInterceptorModule is required to manage all API requests and respond to them accordingly
  • The HttpClientModule is required to make API calls
  • The TrusteerModule is 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
}
]
},
];

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;
}
});

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 SetSelectedProfile in 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));
}
}