Salesforce - Call external API on button click and update custom field from response - salesforce

So I'll start by saying I'm a C# .Net/Javascript developer with a lot of experience, but I have zero experience with Salesforce. Never ever seen it before today. So, I've been asked by another team to add a custom button to a Contact object, which when clicked calls an external API and updates a custom field in the Contact with the response. It was pitched as "just write some Javascript that calls an API when a button is clicked, it's literally embedded into the page, 15 minute job...".
Following what appears to be quite an outdated document, I've ended up in the Object Manager, selected the Contact object and I'm into the Buttons, Links and Actions page. I'm assuming before this was done using the Execute Javascript behaviour, which in the Lightning version I'm advised against using. So after much Googling I've read about APEX classes, Visualforce Components, Lightning Components, the Salesforce REST API, etc, etc. Not a 15 min job.
Essentially the requirements are to embed a button (or action, or..?) into this Contact page, so that when the sales guy clicks it, it gathers some of the Contact's details and uses them to form an API call to an external service. The data will be used to form a response, which must then be read (as JSON, then parsed) and written into a custom field on the Contact.
What would be the best approach for developing a solution? In the Apex Debug environment I've put together the code to call the API and parse the JSON response, I'm assuming I need to wrap this in an Apex class, with a method that calls this code and returns the response. What I'm not sure of is how to call into this from the button, and update the field in the Contact.
Do I need to do all that from the Apex Class method? passing in a reference to the Contact, or is there another component that needs to sit in between and do this.
Am I right in assuming I'll need to use the Salesforce API to update the Contact?
Any pointers appreciated.

Oh man. It can be a 15 min job but it's definitely "easy when you know how" or have some examples ;)
What's your user interface, Classic or Lightning? Do they have plans to migrate to Lightning soon? I'm assuming it's Lightning if you figured out the "Execute JavaScript" hacks are passé.
Do you care where the button/action will be? Is the top right corner with all other buttons fine or do you want it to be droppable to pretty much any area in the page?
Does the API callout need username, password, maybe certificate? It'll determine whether you need just to whitelist the endpoint on firewall (Setup -> Remote Site Settings) or you'll need something more advanced (Setup -> Named Credentials).
Do you have SFDX command line (CLI), VSCode / are determined to install some tooling? The Lightning Web Components are cutting edge, most sleek etc but you can't create them straight in the browser (at least not yet), you need tooling. Visualforce is OK but nothing special for this use case, Aura components are bit clunky to write - but you can do both in Developer Console without extra tooling.
Parsing the JSON response - depends how complex it is, you can hand-craft parser with JSON.deserializeUntyped() but life's too short for this. Here's nice apex code generator similar to what you'd get from parsing WSDL: https://json2apex.herokuapp.com/
We'll try to do Aura component way. It's ugly, LWC is future but hey, it'll get you started.
Go to Setup -> Remote Site Settings and add new entry with https://en.wikipedia.org/
Create new Apex class:
public with sharing class Stack63364119 {
static final String endpoint = 'https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&srsearch=';
#AuraEnabled
public static String doCallout(Id contactId){
if(contactId == null){
throw new MyException('Missing record id');
}
List<Contact> contacts = [SELECT MailingCountry FROM Contact WHERE Id = :contactId];
if(contacts.isEmpty() || String.isBlank(contacts[0].MailingCountry)){
throw new MyException('Could not find the contact');
}
Contact c = contacts[0];
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint + c.MailingCountry);
req.setMethod('GET');
HTTPResponse res = new Http().send(req);
System.debug(res.getStatus());
System.debug(res.getBody());
// no special parsing, just chuck it into Description field
// no error handling
if(res.getStatusCode() == 200){
c.Description = res.getBody().abbreviate(32000);
update c;
}
return res.getBody();
}
public class MyException extends Exception{}
}
Make new "Lighning Component" in developer console (it'll be Aura, not LWC). You can tick the last checkbox about "lightning quick action". Name can be same as class but doesn't have to be.
For component (~ html part) paste this
<!-- Loosely based on https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/controllers_server_actions_call.htm -->
<aura:component controller="Stack63364119" implements="force:hasRecordId,force:lightningQuickAction" >
<!-- in the name of all that is holy do not name the JS function same as the Apex class function, it'll give you very cryptic errors to debug -->
<aura:handler name="init" value="{!this}" action="{!c.runCallout}"/>
</aura:component>
For controller (~ JavaScript) paste this
({
runCallout : function(cmp) {
let action = cmp.get('c.doCallout');
action.setParams({contactId : cmp.get('v.recordId')});
action.setCallback(this, function(response){
let state = response.getState();
if (state === "SUCCESS") {
alert('Saved OK: ' + response.getReturnValue());
$A.get("e.force:closeQuickAction").fire(); // if you want to self-close
} else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
console.log("Error message: " + errors[0].message);
}
} else {
console.log("Unknown error");
}
}
});
$A.enqueueAction(action);
}
})
Finally go Object Manager -> Contact -> Buttons Links and Actions. Create new Quick Action.
And add it to page layout(s)!
It should get you started. Maybe you'll decide to split it a bit, Apex would only do the callout, return results to UI and if user is happy - updating the contact can be done with one of these: https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/data_service_save_record.htm. Separation of concerns blah blah (but will the user be happy with 2 clicks).
P.S. If you tweak it and it dies but it's hard to see any JavaScript errors - it'll be because default is to run in release mode, SF rewrites your source code a bit, optimises, polyfills for "browsers" like IE11... Go to Setup -> Debug Mode and enable for your user.
P.P.S. In sandbox / dev org it's good idea to go Setup -> Session Settings -> and untick "Enable secure and persistent browser caching to improve performance". Your component will be always fresh, saves some frantic hitting Ctrl+R. Don't do it in prod ;)

I will write to you my approach
Button on the record page/page layout -> lighting component or flow -> Apex class for collecting data -> apex class request & response API(don't forget to add the endpoint to remote site settings)-> parse response and update contact (you can use queries and DML operations inside Apex)
lighting component it will be very simple just have a the apex class as the controller example
<aura:component implements="force:appHostable,lightning:isUrlAddressable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId,forceCommunity:availableForAllPageTypes,force:lightningQuickAction" controller="contactController" access="global" >
<aura:handler name="init" value="{!this}" action="{!c.fetchContact}"/>
</aura:component>
controller
({
fetchContact : function(component, event, helper) {
helper.fetchContactHelper(component, event, helper);
}
})
helper
({
fetchAccHelper : function(component, event, helper) {
var action = component.get("c.fetchContacts");
action.setParams({
});
action.setCallback(this, function(response){
var state = response.getState();
if (state === "SUCCESS") {
}
});
$A.enqueueAction(action);
}
})
assuming is apex function is fetchContacts and class contactController

Related

CPQ Quote API, I can't save the quote

I can't save the quote.
Doing the query:
select
ApexClass.name, Id, CreatedDate, CreatedById, JobType,
ApexClassId, Status, JobItemsProcessed, TotalJobItems,
NumberOfErrors, CompletedDate, MethodName, ExtendedStatus,
ParentJobId, LastProcessed, LastProcessedOffset
from
AsyncApexJob
order by
CreatedDate desc
I get this error:
Calculation error on quote Q-13761: "UNAUTHORIZED"
Code:
public with sharing class QuoteCalculator {
public void calculate(QuoteModel quote, String callbackClass) {
system.debug('quote: ' +quote);
system.debug('callbackClass: ' +callbackClass);
QuoteCalculatorContext ctx = new QuoteCalculatorContext(quote, callbackClass);
SBQQ.ServiceRouter.load('SBQQ.QuoteAPI.QuoteCalculator', null, JSON.serialize(ctx));
system.debug('QuoteCalculator.calculate');
}
private class QuoteCalculatorContext {
private QuoteModel quote; //The quote and callbackClass properties are called
in the API code by the exact names seen here.
private String callbackClass; //Altering these property names will cause
calculator API calls to fail.
private QuoteCalculatorContext(QuoteModel quote, String callbackClass) {
this.quote = quote;
this.callbackClass = callbackClass;
}
}
}
anonymous window:
QuoteReader reader = new QuoteReader();
QuoteModel quote = reader.read('a0p1w000BhfXzAAJ');
System.debug(quote);
quote.lineItems[0].record.SBQQ__Quantity__c = 2;
QuoteCalculator calculator = new QuoteCalculator();
calculator.calculate(quote, 'MyCallback')
Preface
I had (almost) the same exact code base as yours, and got the same error message.
In my case there was an other sandbox I could test my code, and it turned out to be working properly there.
Cause
Later found out that the Salesforce CPQ's Calculation Quote API is using Heroku to do the calculations in order to avoid apex limits exhaustion.
From this it can be deducted, that it needs to have a Connected App. I checked the Apps -> Connected Apps setup, and found that no record was listed under the "Connected Apps OAuth Usage" page for the Salesforce CPQ. (On my other sandbox there was a "Steelbrick CPQ" row.)
From this I concluded that this might be the reason for this behaviour.
Seems like something went wrong during the "Authorize new Calculation Service" process. (Or there was a sandbox refresh and something else went wrong during it.)
Solution
The bad news is that the option to authorize a new calculation service is only visible for the first time you configure the package, which you might already done. (Well... if you haven't done, then this is a great news, because your problem is probably solved. :D) (Otherwise read further.)
The good news is I figured out a solution for the case when you already done this, yet that "Steelbrick CPQ" row is missing.
Created a scratch org and installed the Salesforce CPQ package, then before I clicked on the "Authorize new Calculation Service" link under the "Pricing and Calculation" tab in the Settings Editor, I checked the source code in hope of finding something of interest.
I did.
This link: https://rest-na.steelbrick.com/oauth/auth/https%3A%2F%2Ftest.salesforce.com/SBQQ
(⚠️NOTE: You might have to change it according to your location. There are several servers across the globe:
rest-au.steelbrick.com
rest-eu.steelbrick.com
rest-jp.steelbrick.com
rest-na.steelbrick.com
But for me the above pasted link was generated on the settings page. Which is only interesting, because I live in the EU, yet, for some reason I got the link to the rest-NA server... whatever.gif
So just make sure if you click on the link, in the address bar you can find the appropriate salesforce instance URL.)
Conclusion
With this link you won't have to reinstall the package, you just have to click on it, and allow the access from Steelbrick and the missing row will appear, and you will be authorized to use the Calculation API.

SalesForce query returns results in Query Editor, but returns null from APEX code in Lightning component

I'm completely new to SalesForce and have inherited a report that's not working. Please excuse any incorrect terminology, since I'm learning about all this as I go. The report has three prompts: states, years, and members. All dropdowns are supposed to populate with data returned from functions in an APEX class. State, which populates from a picklist, and years, which is populated with a loop, work fine. Members, which populates from a SQL query, returns nothing. If I run the report without any prompts selected (which should return an unfiltered list of results from a SQL query), it also returns nothing. Both of the SQL queries return data when I execute them directly in the query editor in the developer console, but they return nothing when called from the APEX functions.
Here's the initialization code from the Lightning controller:
doInit: function (component, event, helper) {
var action = component.get('c.getTrcAccounts');
action.setCallback(this, function (response) {
var state = response.getState();
if (state === 'SUCCESS' && component.isValid()) {
component.set('v.trcAccList', response.getReturnValue());
}
helper.getLocationState(component, event);
helper.getYear(component, event);
});
$A.enqueueAction(action);
},
Here are the two helper functions referenced in that code:
getLocationState: function (component, event) {
var action = component.get('c.getLocationState');
action.setCallback(this, function (response) {
var state = response.getState();
if (state === 'SUCCESS') {
component.set('v.LocationStateList', response.getReturnValue());
}
});
$A.enqueueAction(action);
},
getYear: function (component, event) {
var action = component.get('c.yearsOptions');
action.setCallback(this, function (response) {
var state = response.getState();
if (state === 'SUCCESS') {
component.set('v.LocationYearList', response.getReturnValue());
}
});
$A.enqueueAction(action);
}
Here is the code from the APEX class that returns the data for those three prompts:
Global class DataTableLocations {
#AuraEnabled
Global static List<TRC_Account__c> getTrcAccounts(){
set<string> trcAccountSet = new set<string>();
List<TRC_Account__c> traccList = new List<TRC_Account__c>();
for(TRC_Account__c trcacc : [SELECT Id, Name from TRC_Account__c WHERE TRC_Member__c = True order by Name limit 50000]){
if(!trcAccountSet.contains(trcacc.Name)){
trcAccountSet.add(trcacc.Name);
traccList.add(trcacc);
}
}
if(traccList.size()>0){
return traccList;
}
else{
return null;
}
}
#AuraEnabled
Global static List<string> getLocationState(){
List<string> options = new List<string>();
//options.add(new SelectOption('SelectAll', 'Select All'));
for( Schema.PicklistEntry f : Location__c.Physical_Address_State__c.getDescribe().getPicklistValues()) {
options.add(f.getValue());
}
return options;
}
#AuraEnabled
Global static List<string> yearsOptions() {
List<string> options = new List<string>();
date OldDate= date.today().addYears(-18);
integer oldyear=OldDate.year();
for( integer i=0; i<19 ;i++) {
options.add(string.valueOf(oldyear));
oldyear++;
}
return options;
}
}
If I run SELECT Id, Name from TRC_Account__c WHERE TRC_Member__c = True order by Name limit 50000 directly in the query editor window in the developer console, I get 7 results. However, if I output the response.getReturnValue() for getTrcAccounts in the doInit function, it's null.
Any help is greatly appreciated, as we're in a bit of a time crunch in conjunction with a site redesign. I'm told these reports were working at one point, but no one knows when they stopped working, and we inherited this code from a different company that did the original development. Thank you!
UPDATE:
In case it helps, this is the code in the lightning app that I think is used on the public page:
<aura:application extends="ltng:outApp" access="GLOBAL" implements="ltng:allowGuestAccess">
<aura:dependency resource="c:SearchBinReceiptsByYear"/>
</aura:application>
Edit
Right, it's a public page, it's called "Salesforce Sites". It's exposed to whole world without having to log in. These have special security in place because most of the time you don't want to expose data like that. At best you'd display contact us form, maybe some documents to download, product catalog... It's all very locked down, default is to ban everything and then admin decides what's allowed. It's bit unusual to have a Visualforce page + Aura component but ok, it happens.
You (and any other internal user) can see the results if you'd access this page from within salesforce. Something like https://mydomain.my.salesforce.com/apex/SearchBinReceiptsByYear and for you the page will work fine, "just" not outside of salesforce.
When exposed like that on the web - there's no logged in user. There's special "[Site Name] Guest User", you can see them if you search "Sites" in Setup. It has a special profile, also with [Site Name] in it. And nasty thing is - it doesn't show on the list of Users or Profiles.
Your code broke when Salesforce (auto)activated a critical update. Probably this one: https://releasenotes.docs.salesforce.com/en-us/spring20/release-notes/rn_networks_secure_perms_guests.htm There are some good resources on the net if you Google "Secure Object Permissions for Guest Users", for example https://katiekodes.com/salesforce-spring-20-guest-user/
Ask your system administrator colleague or read up a bit about sharing rules.
You'll have to go to Setup -> Sharing Rules. There's a checkbox that caused your stuff to break and you can't untick it.
Scroll down to your TRC Account object and hit "New". You'll need to create something like this, but with your criteria (TRC Member equals true)
Save, wait a bit (it might take a while to recalculate the sharing, you'll get an email) and try the page.
If it still doesn't work you'll have to check the Guest user's profile, it might need permissions to Read TRC Accounts and their Name field.
If it's Salesforce Sites - try this to find it: https://help.salesforce.com/articleView?id=000334554&type=1&mode=1
If it's a Customer Portal, Community, Digital Experience (they renamed the product few times) - try with https://help.salesforce.com/articleView?id=sf.rss_config_guest_user_profile.htm&type=5
Original answer
It looks like it's running OK because accounts (members?) are fetched first and in that fetch's callback (what to do when data comes back from server) you have helper.getLocationState, helper.getYear. And you wrote that these populate OK. It's not the best performance code but it should get the job done.
In no specific order...
Does the whole thing work OK for sysadmins? Or is it broken for everybody? If it works for sysadmins it might be something to do with sharing, your sysadmin should know (Setup -> Sharing settings is where you control who can see what. Maybe "mortals" are not allowed to see any data? Typically sysadmins bypass it. As a quick & dirty test you can modify the class definition to global without sharing class DataTableLocations but it's a really ugly hack.
What happens if you open DeveloperConsole (upper right corner) while running this component, do you see any errors in the logs? What happens if in the console you go Debug -> Open ExecuteAnonymous and run this piece of code:
System.debug(DataTableLocations.getTrcAccounts());
Does it return something? Throw error?
You can go to Setup -> Debug Mode, tick the checkbox next to your user and save. This slows the system down a bit but lets you debug the javascript better. You can then sprinkle some debugger; or console.log statements in the source code and view what happens in your browser's console (Ctrl+Shift+J in Chrome, Ctrl+Shift+I in firefox). For example
action.setCallback(this, function (response) {
var state = response.getState();
debugger;
console.log(state);
console.log(component.isValid());
console.table(response.getReturnValue());
if (state === 'SUCCESS' && component.isValid()) {
component.set('v.trcAccList', response.getReturnValue());
}
console.log(component.get('v.trcAccList'));
debugger;
helper.getLocationState(component, event);
helper.getYear(component, event);
});
How's the trcAccList variable actually used in the "cmp" file, in the HTML-like file? Maybe it's being set all right and contains 7 records but it's not displayed right?

Meteor with query on publication is not reactive

I have a problem with a meteor publication not being reactive when using a query inside it.
Let's say I have many files, and each file has many projects, so I can go to the route:
http://localhost:3000/file/:file_id/projects
And I would like to both display the projects of the selected file and add new projects to it.
I am currently using angularjs, so the controller would look something like this:
class ProjectsCtrl {
//some setup
constructor($scope, $reactive, $stateParams){
'ngInject'
$reactive(this).attach($scope)
let ctrl = this
//retrieve current file id
ctrl.file_id = Number($stateParams.file)
//get info from DB and save it in a property of the controller
ctrl.subscribe('projects', function(){return [ctrl.file_id]}, function(){
ctrl.projects = Projects.find({file_id: ctrl.file_id}).fetch()
})
//function to add a new project
ctrl.addProject = function(){
if(ctrl.projectName){
Meteor.call('projects.insert', {name: ctrl.projectName, file_id: ctrl.file_id }, function(error, result){
if(error){
console.log(error)
}else{
console.log(result)
}
})
}
}
}
}
The publication looks something like this:
Meteor.publish('projects', function(file_id){
return Projects.find({file_id: file_id})
})
The problem is that, if I insert a new project to the DB the subscription doesn't run again, I mean the array stays the same instead of displaying the new projects I am adding.
I got many problems with this as I thought that meteor would work something like: "Oh there is a new project, let's re run the query and see if the publication change, if it does, let's return the new matching documents"... but no.
I have not found a problem similar to mine as every question regardind querys inside the publication is about how to reactively change the query (the file_id in this case) but that is not the problem here as I don't change the file_id unless I go to another route, and that triggers a new subscription.
My current solution is to expose the complete collection of projects and make the query using minimongo, but I don't know if it is a good workaround (many projects exposed uses too much memory of the browser, minimongo is not as fast as mongo... etc, I don't really know).
Your issue is that the Meteor.subscribe call doesn't know that file_id has changed. There's no reactive relationship between that argument and executing the subscription.
To fix this, whenever you are passing criteria in publish-subscribe, you must write a subscription of Collection inside a tracker.
To know more about trackers, Click here.
While I'm unsure how to do this in Angular, consider this simple Blaze template as an example:
Template.Name.onCreated(function(){
this.autorun(() => {
Meteor.subscribe('projects', file_id);
});
});
Whenever file_id changes, a new subscription is triggered, giving you the desired effect of auto pub-sub utility.
I hope this will give you some insight. It could be easily achieved via Angular JS as well.

Alexa Skill kit -Lambda function - Could not validate SpeechletRequest (java)

I tried to create a HelloWorld Skill based on https://github.com/amzn/alexa-skills-kit-java but when I tested the lambda function it showed this error
{
"errorMessage":"com.amazon.speech.speechlet.SpeechletRequestHandlerException: Could not validate SpeechletRequest null using verifier ApplicationIdSpeechletRequestVerifier, rejecting request",
"errorType": "java.lang.RuntimeException",
"stackTrace": [ "com.amazon.speech.speechlet.lambda.SpeechletRequestStreamHandler.handleRequest(SpeechletRequestStreamHandler.java:101)",
"helloworld.HelloWorldSpeechletRequestStreamHandler.handleRequest(HelloWorldSpeechletRequestStreamHandler.java:43)"
],
"cause": {
"errorMessage": "Could not validate SpeechletRequest null using
verifier ApplicationIdSpeechletRequestVerifier, rejecting request",
"errorType": "com.amazon.speech.speechlet.SpeechletRequestHandlerException",
"stackTrace": [
"com.amazon.speech.speechlet.SpeechletRequestHandler.handleSpeechletCall(SpeechletRequestHandler.java:73)",
"com.amazon.speech.speechlet.lambda.SpeechletRequestStreamHandler.handleRequest(SpeechletRequestStreamHandler.java:98)",
"helloworld.HelloWorldSpeechletRequestStreamHandler.handleRequest(HelloWorldSpeechletRequestStreamHandler.java:43)"
]
}
}
This is my Java file
public final class HelloWorldSpeechletRequestStreamHandler extends SpeechletRequestStreamHandler {
private static final Set<String> supportedApplicationIds = new HashSet<String>();
static {
/*
* This Id can be found on https://developer.amazon.com/edw/home.html#/ "Edit" the relevant
* Alexa Skill and put the relevant Application Ids in this Set.
*/
supportedApplicationIds.add("amzn1.echo-sdk-ams.app.[amzn1.echo-sdk-ams.app.56bcdaf9-97fc-47f9-9918-43cb6a90d9f5]");
}
public HelloWorldSpeechletRequestStreamHandler() {
super(new HelloWorldSpeechlet(), supportedApplicationIds);
}
}
What am i missing??
You have the wrong ID in the supported application ID. That id needs to be the ID of the Alexa Skills application, which can be found on the Skill Information page. It should look something like this:
supportedApplicationIds.add("amzn1.ask.skill.c236d019-7d2a-5c96-a02f-ef8ab6f8e023");
I know the demo has is with [place id here] But you really replace the whole thing.
For me I got this exception because I was trying to run my lambda function without a proper test event JSON under the Actions tab. If you click the 'Actions' tab and then click 'Configure Test Event' you are supposed give your function input in JSON form that it can interpret. After much looking I figured out that you can get this JSON by going to the developer console where you made your skill that has all your skill configurations. On the left hand side click on the 'Test' tab and then go to the section that says 'Service Simulator'. There is a text box that says 'Enter Utterance' where you can enter a voice command to your function in text e.g 'Alexa tell [yourApp] to say Hello'. Click the 'Ask [yourApp] ' button and a Lambda request JSON will be generated on the left hand box, with the output on the right. Then just copy and paste that JSON in the left into your test event in your lambda console and then you should be good.
I tried to create a Address Skill included in https://github.com/amzn/alexa-skills-kit-java, but I got the same type of error.
It turns out that the problem was in DeviceAddressSpeechletRequestStreamHandler and creating instance of Set<String> supportedApplicationIds in static {} block.
When I moved new HashSet<>(); to declaring attributes of class, it started working.
I would put the static code onto the class you have created that extends SpeechletLambda. This is, I believe, where the evaluation takes place and gets resolved before this class is loaded and its static code executed.
Alternatively you can just turn the validation off. If someone knows your development environment well enough to call your private lambda function, they probably know enough to spoof your application ID. So there isn't a lot of security value to validating it. For an example of turning it off, see here.

using the googleapis library in dart to update a calendar and display it on a webpage

I am new to dart and I have been trying to figure out how to use the googleapis library to update a calendars events, then display the calendar/events on a webpage.
So far I have this code that I was hoping would just change the #text id's text to a list of events from the selected calendars ID:
import 'dart:html';
import 'package:googleapis/calendar/v3.dart';
import 'package:googleapis_auth/auth_io.dart';
final _credentials = new ServiceAccountCredentials.fromJson(r'''
{
"private_key_id": "myprivatekeyid",
"private_key": "myprivatekey",
"client_email": "myclientemail",
"client_id": "myclientid",
"type": "service_account"
}
''');
const _SCOPES = const [CalendarApi.CalendarScope];
void main() {
clientViaServiceAccount(_credentials, _SCOPES).then((http_client) {
var calendar = new CalendarApi(http_client);
String adminPanelCalendarId = 'mycalendarID';
var event = calendar.events;
var events = event.list(adminPanelCalendarId);
events.then((showEvents) {
querySelector("#text2").text = showEvents.toString();
});
});
}
But nothing displays on the webpage. I think I am misunderstanding how to use client-side and server-side code in dart... Do I break up the file into multiple files? How would I go about updating a calendar and displaying it on a web page with dart?
I'm familiar with the browser package, but this is the first time I have written anything with server-side libraries(googleapis uses dart:io so I assume it's server-side? I cannot run the code in dartium).
If anybody could point me in the right direction, or provide an example as to how this could be accomplished, I would really appreciate it!
What you might be looking for is the hybrid flow. This produces two items
access credentials (for client side API access)
authorization code (for server side API access using the user credentials)
From the documentation:
Use case: A web application might want to get consent for accessing data on behalf of a user. The client part is a dynamic webapp which wants to open a popup which asks the user for consent. The webapp might want to use the credentials to make API calls, but the server may want to have offline access to user data as well.
The page Google+ Sign-In for server-side apps describes how this flow works.
Using the following code you can display the events of a calendar associated with the logged account. In this example i used createImplicitBrowserFlow ( see the documentation at https://pub.dartlang.org/packages/googleapis_auth ) with id and key from Google Cloud Console Project.
import 'dart:html';
import 'package:googleapis/calendar/v3.dart';
import 'package:googleapis_auth/auth_browser.dart' as auth;
var id = new auth.ClientId("<yourID>", "<yourKey>");
var scopes = [CalendarApi.CalendarScope];
void main() {
auth.createImplicitBrowserFlow(id, scopes).then((auth.BrowserOAuth2Flow flow) {
flow.clientViaUserConsent().then((auth.AuthClient client) {
var calendar = new CalendarApi(client);
String adminPanelCalendarId = 'primary';
var event = calendar.events;
var events = event.list(adminPanelCalendarId);
events.then((showEvents) {
showEvents.items.forEach((Event ev) { print(ev.summary); });
querySelector("#text2").text = showEvents.toString();
});
client.close();
flow.close();
});
});
}

Resources