Building a Successfactors extension with SAP CP SDK for iOS
You’ve maybe seen Matt’s post where he describes how Successfactors content can be mobilized and consumed using SAP Mobile Cards. This is a great first step in mobilizing content as it doesn’t require any particular mobile app development skills, and no backend modifications either.
What if you wanted a bit more? Using the SAP Cloud Platform SDK for iOS you can consume the Successfactors OData APIs and build very flexible native apps as extensions to the Successfactors core system. I’ve expanded one of the Mobile Cards scenarios and built a mobile app that exposes payroll information to employees.
Backend and proxy classes
First, you need the backend. A sandbox API is made available at https://sandbox.api.sap.com/successfactors/odata/v2 (requires a valid API key from SAP API Business Hub). In general, you can either configure Mobile Services yourself to point to the destination and then start an app from scratch, or you could use the SAP Cloud Platform SDK for iOS Assistant to do all that for you. One particular problem with the Successfactors metadata is, that at the time of writing it doesn’t specify the ‘Scale‘ facet for its Decimal properties correctly (the scale is omitted, which means it defaults to ‘0’; however, in reality it is used for properties like amounts with fraction values). So, I decided to fix this manually and downloaded the metadata from https://sandbox.api.sap.com/successfactors/odata/v2/$metadata. I then fixed up the metadata and added the Scale=”2″ facet to the relevant property that I wanted to access from the EmployeePayrollRunResultsItems collection:
<Property Name="amount" Type="Edm.Decimal" Scale="2" Nullable="true" ...
Depending on your scenario, you may run into other similar problems; until the metadata is fixed centrally, just go ahead and modify the metadata locally so it matches the reality from the server. In case you missed a property and the server returns data that is not valid relative to the metadata, our OData parser will throw meaningful error messages that help you fix things up in iterations until all is good.
With that slightly modified metadata document I could go ahead and generate the proxy classes using our command line tool (installed from Assistant main menu -> Install Tools):
sapcpsdk-proxygenerator -m ./sf-metadata.xml -d successfactors/Proxy\ Classes/successfactors/
The metadata of Successfactors is massive, so this will generate quite a bunch of proxy classes, which used to be a problem in earlier Xcode versions. However, the new build system shipping in Xcode 9.4 handles this quite well, and once you’ve been patient enough for the whole batch to compile okay once, subsequent delta builds will be fast as usual.
Connectivity
I’m using OAuth2SAMLBearerAssertion to establish single-sign-on across my SAP Cloud Platform account and the Successfactors instance that I’m testing with. This part is not possible with the APIs published on Business Hub – there you’ll probably just want to experiment with a technical user.
This requires now establishing trust across the two cloud systems, which means copying some keys and certificates back and forth. We start in the SAP Cloud Platform Cockpit, under Security->Trust we need to copy the signing certificate. (In case you run with default configuration, i.e. SAP IAS – click ‘edit’ and change to ‘custom’ so you see all this data; then once you’ve copied the certificate please click ‘cancel’).
Now, we switch to the Successfactors admin console and create a new OAuth client for our native app.
This will give us the API Key that we’ll now need to provide in the Mobile Services Cockpit on Cloud Platform to establish mutual trust. When setting up the connectivity for the mobile app, we’ll set up the destination to point to our Successfactors API endpoint. The API key goes into the ‘Client Key’ and ‘SAML Assertion Issuer’ field, the SF company id goes into the ‘Company Identifier’.
With this, we’re done. When the app connects to Mobile Services, the server will produce a SAML assertion, sign it with the key that is trusted by Successfactors (as we’ve uploaded the certificate), Successfactors will return an OAuth2 token, and with that we can then make API calls in the context of the currently logged in user.
In my case, I was using SAP IAS, so the user that I used to connect to Mobile Services (a P# user) has to exist in SF now as well. I simply created a test employee in my case with that Person Id. As all these API calls now happen in the context of the end user, you also need to be careful with the permissions these users have. You definitely don’t want folks to see other employees payroll data. So, when setting up the permission roles, make sure you’re not exposing the OData API in admin, but in user mode and assign permissions only for ‘everyone in self’ target. Here’s more information on permissions in Successfactors.
Retrieving the data
Now, I’m reproducing the same OData query that we’re using for the SAP Mobile Card showing payroll information.
/odata/v2/EmployeePayrollRunResults?$format=json&$filter=userId%20eq%20'XXX'&$expand=employeePayrollRunResultsItems,userIdNav/empInfo/paymentInformationNav/toPaymentInformationDetailV3/bankNav&$top=1&$orderby=payDate%20desc
I use our fluent query API and proxy classes here.
let query = DataQuery()
.filter(EmployeePayrollRunResults.userID == currentUser)
.expand(EmployeePayrollRunResults.employeePayrollRunResultsItems, EmployeePayrollRunResults.userIdNav.path(User.empInfo).path(EmpEmployment.paymentInformationNav).path(PaymentInformationV3.toPaymentInformationDetailV3).path(PaymentInformationDetailV3.bankNav))
.select(
EmployeePayrollRunResults.userIdNav.path(User.defaultFullName),
EmployeePayrollRunResults.payDate,
EmployeePayrollRunResults.userIdNav.path(User.empInfo).path(EmpEmployment.paymentInformationNav).path(PaymentInformationV3.toPaymentInformationDetailV3).path(PaymentInformationDetailV3.bankNav).path(Bank.bankName),
EmployeePayrollRunResults.userIdNav.path(User.empInfo).path(EmpEmployment.paymentInformationNav).path(PaymentInformationV3.toPaymentInformationDetailV3).path(PaymentInformationDetailV3.accountNumber),
EmployeePayrollRunResults.currency,
EmployeePayrollRunResults.employeePayrollRunResultsItems.path(EmployeePayrollRunResultsItems.amount),
EmployeePayrollRunResults.employeePayrollRunResultsItems.path(EmployeePayrollRunResultsItems.wageType)
)
.orderBy(EmployeePayrollRunResults.payDate, .descending)
This looks a bit verbose, but this is mainly do to the nested nature of the underlying OData service. This query expands the payroll items as well as the employee information and bank details. As these data structures contain quite a few properties, I’m selecting only what I need then, and order it all by the pay date (newest first).
Data visualization
My idea of visualizing this data is to use an object header to show some basic information on the user and target bank account for the pay out, as well as using the header to also show salary development over the last couple months in a chart. The table itself would have multiple sections and show the same payroll details that we saw on the Card for a single payroll run.
This is the object header definition:
//Programmatically add an Object header to tableview
let objectHeader = FUIObjectHeader()
self.tableView.tableHeaderView = objectHeader
objectHeader.detailImageView.image = imageLiteral(resourceName: "Cash")
objectHeader.headlineLabel.text = "My Pay Check"
objectHeader.subheadlineLabel.text = (result.userIdNav?.defaultFullName)!
objectHeader.bodyLabel.text = (result.userIdNav?.empInfo?.paymentInformationNav[0].toPaymentInformationDetailV3[0].bankNav?.bankName)! + " (#" + (result.userIdNav?.empInfo?.paymentInformationNav[0].toPaymentInformationDetailV3[0].accountNumber!)! + ")"
let view = FUIObjectHeaderChartView()
view.title.text = "Historical Overview"
view.subtitle.text = "my net payout"
view.chartView.dataSource = self
view.isEnabled = false
objectHeader.detailContentView = view
and the implementation for the chart data source
// MARK: - FUIChartViewDataSource
func chartView(_ chartView: FUIChartView, valueForSeries seriesIndex: Int, category categoryIndex: Int, dimension dimensionIndex: Int) -> Double? {
for item in payrollResults![payrollResults!.count - categoryIndex - 1].employeePayrollRunResultsItems {
if item.wageType == "NETPAY" {
return item.amount?.doubleValue()
}
}
return 0
}
func chartView(_ chartView: FUIChartView, numberOfValuesInSeries seriesIndex: Int) -> Int {
return payrollResults!.count
}
func numberOfSeries(in: FUIChartView) -> Int {
return 1
}
The table itself uses a simple text field form cell to show each payroll run in a separate section
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return payrollResults!.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return payrollResults![section].employeePayrollRunResultsItems.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "Date: \(payrollResults![section].payDate!.month)/\(payrollResults![section].payDate!.year)"
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: FUITextFieldFormCell.reuseIdentifier, for: indexPath) as! FUITextFieldFormCell
cell.isEditable = false
cell.keyName = (payrollResults![indexPath.section].employeePayrollRunResultsItems[indexPath.row].wageType ?? "n/a").capitalized
cell.value = (payrollResults![indexPath.section].employeePayrollRunResultsItems[indexPath.row].amount?.toString())! + payrollResults![indexPath.section].currency!
return cell
}
I’ve taken a few shortcuts here when it comes to data validation and force-unwrapping. This serves better readability of the code, in reality you may want to do more careful checking and fallback in case some properties aren’t available.
The outcome
This was all relatively straight-forward use of SDK components to handle the OData backend and data visualization. Additionally – I didn’t describe this here – I’ve also used the Flows framework to handle onboarding for me; simply the stuff that is generated by the Assistant, nothing special and in particular no extra work. I took the Card that showed the most recent payroll run, enhanced this by showing the full history, added nice visualization in a line chart, and now taking this further, adding more features, or enriching this with other data is a matter of spending time in Xcode and hacking away.
Here are a couple of screenshots how this looks like:
<img /><img />
Next steps
This little experiment showed a few things. On the pro side, building an interface with slick controls, nice charts, and a great overall appearance was really easy. Using the existing controls and wiring it all up per our API documentation, I could get the visual part going very fast. Leveraging the Assistant, Mobile Services, and built-in Flows, also the infrastructure aspect of enabling access to the backend in a secure way was a walk in the park. Interacting with the OData service using generated proxy classes and our fluent query API was straight-forward as well, and with code completion in Xcode even this very complex data model was manageable. This leads to the not so easy part, though. In order to build Successfactors extensions, you really got to have a good handle on the underlying data model and functionality – otherwise you’ll just get lost. I’m not blaming anybody here, this is just the way it is with complex business solutions. Just to give it a positive spin towards the end, once you got the hang of the backend model, going into native app building will feel like a walk in the park.
So, if you’re a Successfactors expert and this inspired you to building a great native app, I’d be happy to learn about it. Feedback very welcome.
Andreas
Looks good!
However, I'm interested in how you handled authentication? And application management, i.e how to provision/remove devices from having access to the data.
Personally I would have probably leveraged SAP cloud platform OAuth and then used the SAML OAuth Bearer tokens from a SucessFactors extension account and used an intermediary neo app.
But nothing that complex mentioned... So guessing it was just basic auth.?
I'd suggest there is a whole lot more than the complexity of the API (which admittedly is complex) to worry about in this scenario!
But it's certainly nice to open people's eyes to the possibilities!
Cheers,
Chris
Chris,
I've used SAP CP Mobile Services as the middle tier and defined a destination in there that points to my internal testing Successfactors landscape. For my testing I used OAuth authentication to connect from the mobile device to SAP CP Mobile Services (see Mobile Services doc for configuration and SDK for iOS doc or this tutorial for consumption part) and then the hop from Mobile Services to Successfactors was a simple technical user in my case. As I indicated in the blog post, if you use the SDK for iOS Assistant, the Assistant would do it all for you: configure the app on Mobile Services, create the Xcode project, and have all the required code and configuration for the OAuth2 authentication between Mobile app and Cloud to work.
For production however, I would suggest exactly doing what you describe as well, using OAuth2SAMLBearer assertions for that second hop from Mobile Services to Successfactors - see the documentation on destination configuration, point (5) SSO mechanism.
I'll see that I find some time to set this all up and add it to the post itself in a bit more detail, but I hope this makes sense on the high level. And yes, security is always one of the topics that can throw a project off track, so it is very relevant information here.
You also asked about application management and app provisioning; we're not providing you specific MDM features here, but assuming the authentication is set up as discussed above, I presume revoking users access rights on the backend is just what you need. If you also want to avoid that users use the app offline while they've already lost access permissions, we've also implemented a lock&wipe feature that can be used to force users coming online regularly.
Thanks
Andreas
Thanks Andreas,
Some excellent details! ?
Where I have struggled in the past has been having to have a seperate application management portal for initial authentication of the device and subsequent deprovisioning of OAuth token in the case that device is lost.
Do you build your own for this?, I find the generic SAP Cloud OAuth management portal not to be nearly user friendly enough, nor customisable enough, so I have to build my own. Plus I then need to embed this in SAPSF so people know where to go to remove access to a lost device.
Your thoughts on that process would also be very interesting!
Thanks,
Chris
Chris, Mike,
I've update the blog with some more detail on how to set up the trust between SAP Cloud Platform and Successfactors and the configuration in Mobile Services. That worked fine for me and I had SSO for a P# user across Cloud Platform into Successfactors.
I haven't considered building my own token management portal so far, but I tend to agree that the separate portal isn't very intuitive to use for end users. With Mobile Services in the game, you can block users at that level as an admin, so end users wouldn't need to take care of this. I know - not exactly addressing your concern - but should be a valid approach.
Thanks
Andreas
Great blog Andreas, thanks for sharing. I have a requirement to build an offline timesheets app with a SuccessFactors back-end. It's great to have the tip about the manual adjustment to the metadata.
Like Chris Paine I'd love to know some more about the authentication method. My app is going to show some leave records and so it won't be appropriate to use a system user/basic auth because I want to use the SF authorisations. I can't afford to have an OData API exposed (via Cloud Platform) that allows people to view everyone's leave records.
Finally, I wondered about calling this an extension. I'd always considered an extension to be something that adds business logic to a back-end system from outside that system. If I wrote a bespoke Fiori app calling ECC or S/4 I wouldn't call that an extension, but if I was writing some Java code on Cloud Platform I probably would (especially if there was a DB involved). I'd be interested to know whether I'm on my own there......
Mike,
Please refer to my response on Chris' question for some detail on authentication - I hope I get to adding more detail to the blog post itself sometime soon.
I may have used the word extension a bit liberally here - I am extending the user interface of Successfactors without adding new business logic to the backend. I'd think any time you add business logic to the backend you would also want to expose this through UI somehow as well. This may end up becoming a bit of a philosophical debate and not sure there's a right or wrong.
For what it's worth, a semi-official answer might be https://www.sapappsdevelopmentpartnercenter.com/en/get-started/cloud-applications/successfactors-extensions/ and the linked PDF here describes:
The extension package includes:
Thanks
Andreas
Re the extension naming thing, I'd probably go 50/50. Whilst the app itself I wouldn't call an extension, you are probably going to have to build an extension app for the management of OAuth tokens and access to the SAPSF data ?
You could probably bundle the whole thing up and offer it as a bundled extension at that point ?
So I'm not going to fight the name. Although I would suggest that unless you are using custom MDF objects that anything you build is probably already covered in the standard mobile app.
Cheers,
Chris
Hey Andreas,
This is a great article but a bit outdated.
Is there an update how to do that in 2023 using Cloud Foundry and the Mobile Services?
Also, what would be the right approach to the application a multi-tenant application in order for it to be published in the store and used by multiple companies?