Forum Documentation Showcase Pricing Learn more

API Connector Strikes Again

Just kind of a kvetch here, but I was just implementing calls for Rebrandly (URL shortener/tracking/etc) and — once again — run into infuriating limitations of the API Connector.

To wit: A call to Rebrabdly’s GET method to return info for custom domains returns an array of JSON objects that describe all available custom domains in the Rebrandly account.

But Bubble can only see the first item. In the RAW JSON DATA returned (and the Rebrandly docs, of course), one can clearly see the structure of the returned data.

It would be no thing to create a new data type to handle those things and then force Bubble to see the raw response as a list of things of that type.

But there is no interface for this. GRRRRRR!!!

There just some very basic flexibility missing in the API Connector. The top things would seem to be flattening responses before parsing and allowing custom data types for responses. These features would likely remove the vast majority of roadblocks one hits with configuring the API Connector.

For better or worse, the API Connector is really a core feature of Bubble since Bubble is really just an awesome system for building and managing kinda-sorta-serverless apps. If one really wants to get Bubble to the “build apps without code” vision, the API Connector needs a few more features. I could easily build a very flexible, full-API implementation of Rebrandly API for other Bubblers to use… except that I can’t due to a few missing (obvious) API Connector features.

Please, please, please, Bubble-gods, add a flattener and support for custom data types on responses. I get that these aren’t “easy” things to build, but neither are they “hard”.

The API Connector is very cool in that it kinda autodetects the response and tries to do the right thing. But if I, as a human being, know what’s being expected, let me tell the Connector! Pretty please?

6 Likes

This has been a pain point since the API Connector has been (re)created, and also affects most of the plugins that use APIs,

Maybe Bubble are now in a position where they have resources to fix it, its worth bugging them about it.

A hacky workaround is:

  1. Create a mock response where every possible combination of structure exists in the first item.
  2. Serve the mock response from a server.
  3. Alter the url in the API connector to point to the mock server.
  4. Initialize the request, which sets up the return types.
  5. Alter the url in the API connector back to the original API.

This leaves the app in the vulnerable position where if anyone presses “Modify return types”, the mocked structure is replaced by the limited one.

2 Likes

Yeah, not gonna do that. Point is: I can achieve my app’s ends by inspecting the raw data and just manually storing the values that I need (unique IDs for my own custom domains, which could become many).

However, as a by-product, I could have released a generic plug-in for non-coder types that would handle anything someone wanted to do. But nope, can’t do it, so… moving on from THAT idea.

(Aside: While Rebrandly is AMAZINGLY COOL and useful, I’m mainly implementing Rebrandly as a solution for the problem of not being able to serve a file directly in bubble. @mishav knows what I’m talking about. See related thread about File functions in API Connector. Anyway, once again, it just exposes the tension between “building apps without code” and being an awesome controller for modern, serverless apps. Can’t serve two masters, folks… ironically, solving either problem solves both. Get off the fence and solve something. Thanks.)

3 Likes

Is it solved yet? I have same problem: array of json objects, that I can’t access :persevere:

And six months later … still infuriating! Especially since @Bubble is the LAST STEP of integrating a new service. By this point I’ve already researched several APIs that could serve my needs, picked the one that I think would be best, signed up for the service, read the full documentation, got the call working in Postman, set it up “correctly” in Bubble, clicked Initialize, and attempted to debug … only then to finally realize/accept that Bubble just can’t process data from this API.

2 Likes

@edd Does raw data contain what you need from the API? If the issue is parsing, there should be workarounds

@edd what service/API are you trying to connect?

Thanks, @neerja. Yes, raw data looks good. I’ve since tried another API to address the same need, and ran into a similar problem in the end. Would you mind walking me through a potential workaround to parse the response? Since @Kfawcett asked, I’m trying to pull flight cost data. First API I liked was Rapid API’s Hipmunk API. There, the raw data is structured as (only showing up to the bits that matter here):

{
"cabin_map": {
    "0": "Basic Coach",
    "1": "Coach",
    "2": "Premium Coach",
    "3": "Business",
    "4": "First",
    "5": "Air Taxi"
},
"booking_agent": {
    "clienttype": "web",
    "mobile": "",
    "platform": "web",
    "is_app": false,
    "version": "",
    "revision": 1
},
"suggestions": {},
"airlines": {
    "AA": {
        "color": "#A60000",
        "baggage_info": {
            "code": "AA",
            "additional": "$150 for 3rd, $200 for 4th",
            "url": "http://www.aa.com/i18n/travelInformation/baggage/baggageAllowance.jsp",
            "second": "$35",
            "first": "$25",
            "name": "American"
        },
        "short_name": "American",
        "name": "American"
    },
    "AS": {
        "color": "#59102D",
        "baggage_info": {
            "code": "AS",
            "additional": "$75",
            "url": "http://www.alaskaair.com/content/travel-info/policies/baggage-checked.aspx",
            "second": "$25",
            "first": "$25",
            "name": "Alaska"
        },
        "short_name": "Alaska",
        "name": "Alaska"
    },
    "UA": {
        "color": "#0000A6",
        "baggage_info": {
            "code": "UA",
            "additional": "$150",
            "url": "http://www.united.com/page/middlepage/0,6823,1031,00.html",
            "second": "$35",
            "first": "$25",
            "name": "United"
        },
        "short_name": "United",
        "name": "United"
    }
},
"itins": {
    "22168fbf1f66205eddc2086f780a1e96": {
        "dependability_score": 0,
        "iden": "22168fbf1f66205eddc2086f780a1e96",
        "agony": 1180.1373163191743,
        "price": 961,
        "weighted_agony": 1180.1373163191743,
        "unrounded_price": 960.67,
        "booking_urls": {
            "iGOoos8": {
                "original_price": 960.67,
                "kind": "flight",
                "booking_currency": "USD",
                "base_fare": 863.14,
                "sort_rank": 0,
                "leg_details": {
                    "branded_fares": [
                        null,
                        "FIRST OR BUSINESS",
                        "FIRST OR BUSINESS"
                    ],
                    "fare_basis_codes": [
                        "QAA4AFFN",
                        "QAA4AFFN",
                        "VAA2AFFN"
                    ],
                    "rbds": [
                        "D",
                        "D",
                        "Z"
                    ]
                },
                "price": 961,
                "iden": "iGOoos8",
                "milefy": [
                    {
                        "program_code": "UMP",
                        "earnings": {
                            "qualifying_segments": 4.5,
                            "miles": 0,
                            "qualifying_miles": 4402
                        }
                    }
                ],
                "smb_offer": null,
                "url_type": "web",
                "unrounded_price": 960.67,
                "provider_type": "AIR",
                "site_mobile_optimized": false,
                "name": "United"
            }
        },
        "routing_idens": [
            "b65c2602500f57976d1b62202ce6b46c"
        ],
        "bundle_iden": "39182bd14f8aa77d94a507d813b509f7"
    },
    "898b9681db89fc3d535559b59e5331f0": {
        "dependability_score": 0,
        "iden": "898b9681db89fc3d535559b59e5331f0",
        "agony": 608.6207907216155,
        "price": 599,
        "weighted_agony": 608.6207907216155,
        "unrounded_price": 598.8,
        "booking_urls": {
            "49mQajO": {
                "original_price": 598.8,
                "kind": "flight",
                "booking_currency": "USD",
                "base_fare": 526.51,
                "sort_rank": 0,
                "leg_details": {
                    "branded_fares": [
                        null,
                        "ECONOMY",
                        "ECONOMY"
                    ],
                    "fare_basis_codes": [
                        "VAA2AFDN",
                        "VAA2AFDN",
                        "VAA2AFDN"
                    ],
                    "rbds": [
                        "V",
                        "V",
                        "V"
                    ]
                },
                "price": 599,
                "iden": "49mQajO",
                "milefy": [
                    {
                        "program_code": "UMP",
                        "earnings": {
                            "qualifying_segments": 3,
                            "miles": 0,
                            "qualifying_miles": 2317
                        }
                    }
                ],
                "smb_offer": null,
                "url_type": "web",
                "unrounded_price": 598.8,
                "provider_type": "AIR",
                "site_mobile_optimized": false,
                "name": "United"
            }
        },
        "routing_idens": [
            "97866949004d753be073e74534d312a3"
        ],
        "bundle_iden": "5ad14db99d5e57835468b9c81d70f180"
    },

What I want/expect is that Bubble will let me specify that “itins” are a type of thing and that each next level object within the itins tree is one of n things. Instead, on initialization Bubble enumerates each unique itin at the top level (“itin 22168fbf1f66205eddc2086f780a1e96”, “itin 898b9681db89fc3d535559b59e5331f0”, etc.) and does not recognize that they are all different enumerations of the same basic thing.

So after I gave up on that one, I tried the Kiwi/Skypicker API, which it turns out I prefer, but I can’t get that to work either. That one looks like this:

{
    "search_id": "bb25c06e-a17e-49a4-b907-a44f7c250e85",
    "data": {},
    "connections": [],
    "time": 1,
    "currency": "USD",
    "currency_rate": 0.8921577549,
    "fx_rate": 1.120878,
    "refresh": [],
    "del": 0,
    "ref_tasks": [],
    "search_params": {
        "flyFrom_type": "airport",
        "to_type": "airport",
        "seats": {
            "passengers": 1,
            "adults": 1,
            "children": 0,
            "infants": 0
        }
    },
    "airlinesList": [
        {
            "filterName": "FLIXBUS"
        },
        {
            "filterName": "NK"
        },
        {
            "filterName": "UA"
        },
        {
            "filterName": "AS"
        },
        {
            "filterName": "G4"
        },
        {
            "filterName": "WN"
        },
        {
            "filterName": "F9"
        },
        {
            "filterName": "DL"
        },
        {
            "filterName": "SY"
        },
        {
            "filterName": "AA"
        }
    ],
    "airportsList": [
        {
            "filterName": "PDX",
            "name": "Portland International"
        },
        {
            "filterName": "SFO",
            "name": "San Francisco International"
        },
        {
            "filterName": "SJC",
            "name": "San Jose International"
        },
        {
            "filterName": "MSP",
            "name": "Minneapolis–Saint Paul International"
        },
        {
            "filterName": "PHX",
            "name": "Phoenix Sky Harbor International"
        },
        {
            "filterName": "DEN",
            "name": "Denver International"
        },
        {
            "filterName": "LAX",
            "name": "Los Angeles International"
        },
        {
            "filterName": "DFW",
            "name": "Dallas/Fort Worth International"
        },
        {
            "filterName": "OAK",
            "name": "Oakland International"
        },
        {
            "filterName": "LAS",
            "name": "McCarran International"
        },
        {
            "filterName": "ORD",
            "name": "O Hare International"
        },
        {
            "filterName": "SAN",
            "name": "San Diego International"
        },
        {
            "filterName": "DAL",
            "name": "Dallas Love Field"
        },
        {
            "filterName": "SLC",
            "name": "Salt Lake City International"
        },
        {
            "filterName": "SEA",
            "name": "Seattle–Tacoma International"
        }
    ],
    "all_airlines": [
        "FLIXBUS",
        "NK",
        "UA",
        "AS",
        "G4",
        "WN",
        "F9",
        "DL",
        "SY",
        "AA"
    ],
    "all_prices": {
        "668-708": 3,
        "708-750": 38,
        "750-794": 80,
        "794-841": 103,
        "841-891": 42,
        "891-944": 8
    }

Here, what would be awesome is if I could get that “all_prices” histogram data dynamically – that is, when referencing the API response data, I can simply fetch all_prices’ item #1’s value, all_prices’ item #2’s value, etc. The problem is twofold: 1) similar to the last example, Bubble does not recognize the “all prices” level, it skips it and instead pulls in each individual price tier as a top-level object (“all_prices 668-708”, “all_prices 708-750”, etc.); and 2) I would need both the label and the value in each pair to make the data useful, since the label contains the price range. These labels are completely dynamic in this API, sometimes they go from 120ish to 450ish, sometimes they have 15 tiers, etc.

In the end, I’m trying to approximate, in real time in my app, how much it would cost Person 1 to go from Location 1 to Location 2. I’ve got everything else working in terms of geocoding users on both sides of my platform, finding their nearest airports, and integrating the travel API(s) … I just can’t get the numbers I need out of the API response. Any help cracking this would be most welcome.

Thank you both (and anyone else that comes along for the ride)!

-Ed

@edd The quickest method might be to return data as plain text and then extract itins

@edd let me try something during the weekend.

Can you paste here or send me via PM the full response of the problematic call to the API?

2 Likes

That would be amazing. So I’ll just stick with the second example, since that’s my preferred if I can get it working. First, here’s the request details; it’s open (no authentication required) so you can make the call yourself if you want.

To keep it relatively simple, you can leave off the last header and last 4 params.

The response currently is:

{
"search_id": "25422172-d914-4c66-aa5d-d9ec51454485",
"data": {},
"connections": [],
"time": 1,
"currency": "USD",
"currency_rate": 0.8961978805,
"fx_rate": 1.115825,
"refresh": [],
"del": 0,
"ref_tasks": [],
"search_params": {
    "flyFrom_type": "airport",
    "to_type": "airport",
    "seats": {
        "passengers": 1,
        "adults": 1,
        "children": 0,
        "infants": 0
    }
},
"airlinesList": [
    {
        "filterName": "AM"
    },
    {
        "filterName": "KL"
    },
    {
        "filterName": "VS"
    },
    {
        "filterName": "G4"
    },
    {
        "filterName": "AS"
    },
    {
        "filterName": "O2"
    },
    {
        "filterName": "FLIXBUS"
    },
    {
        "filterName": "KE"
    },
    {
        "filterName": "UA"
    },
    {
        "filterName": "AF"
    },
    {
        "filterName": "B6"
    },
    {
        "filterName": "NH"
    },
    {
        "filterName": "WN"
    },
    {
        "filterName": "SY"
    },
    {
        "filterName": "DL"
    },
    {
        "filterName": "WS"
    },
    {
        "filterName": "AC"
    },
    {
        "filterName": "LH"
    },
    {
        "filterName": "F9"
    },
    {
        "filterName": "AA"
    },
    {
        "filterName": "NK"
    }
],
"airportsList": [
    {
        "filterName": "PIE",
        "name": "St. Pete–Clearwater International"
    },
    {
        "filterName": "TPA",
        "name": "Tampa International"
    },
    {
        "filterName": "BNA",
        "name": "Nashville International"
    },
    {
        "filterName": "ATL",
        "name": "Hartsfield–Jackson Atlanta International"
    },
    {
        "filterName": "IAD",
        "name": "Washington Dulles International"
    },
    {
        "filterName": "ORD",
        "name": "O Hare International"
    },
    {
        "filterName": "MCO",
        "name": "Orlando International"
    },
    {
        "filterName": "LAS",
        "name": "McCarran International"
    },
    {
        "filterName": "PHL",
        "name": "Philadelphia International"
    },
    {
        "filterName": "PDX",
        "name": "Portland International"
    },
    {
        "filterName": "PHX",
        "name": "Phoenix Sky Harbor International"
    },
    {
        "filterName": "DFW",
        "name": "Dallas/Fort Worth International"
    },
    {
        "filterName": "OAK",
        "name": "Oakland International"
    },
    {
        "filterName": "SMF",
        "name": "Sacramento International"
    },
    {
        "filterName": "DEN",
        "name": "Denver International"
    },
    {
        "filterName": "CLT",
        "name": "Charlotte Douglas International"
    },
    {
        "filterName": "SFB",
        "name": "Orlando Sanford International"
    },
    {
        "filterName": "MSP",
        "name": "Minneapolis–Saint Paul International"
    },
    {
        "filterName": "DTW",
        "name": "Detroit Metropolitan"
    },
    {
        "filterName": "SEA",
        "name": "Seattle–Tacoma International"
    },
    {
        "filterName": "SLC",
        "name": "Salt Lake City International"
    },
    {
        "filterName": "LAX",
        "name": "Los Angeles International"
    },
    {
        "filterName": "BOI",
        "name": "Boise"
    }
],
"all_airlines": [
    "AM",
    "KL",
    "VS",
    "G4",
    "AS",
    "O2",
    "FLIXBUS",
    "KE",
    "UA",
    "AF",
    "B6",
    "NH",
    "WN",
    "SY",
    "DL",
    "WS",
    "AC",
    "LH",
    "F9",
    "AA",
    "NK"
],
"all_prices": {
    "447-473": 1,
    "531-562": 1,
    "562-596": 2,
    "596-631": 4,
    "631-668": 36,
    "668-708": 57,
    "708-750": 106,
    "750-794": 87,
    "794-841": 23,
    "841-891": 2,
    "891-944": 1,
    "944-1000": 2,
    "1413-1496": 1
},
"all_stopover_airports": [
    "PIE",
    "TPA",
    "BNA",
    "ATL",
    "IAD",
    "ORD",
    "MCO",
    "LAS",
    "PHL",
    "PDX",
    "PHX",
    "DFW",
    "OAK",
    "SMF",
    "DEN",
    "CLT",
    "SFB",
    "MSP",
    "DTW",
    "SEA",
    "SLC",
    "LAX",
    "BOI"
],
"best_results": [
    {
        "sort": "quality",
        "price": 471,
        "duration": 54780,
        "quality": 616.732969
    }
],
"hashtags": [
    {
        "name": "outbound:evening",
        "count": 122
    },
    {
        "name": "outbound:wednesday",
        "count": 112
    },
    {
        "name": "outbound:1stop",
        "count": 98
    },
    {
        "name": "outbound:duration:7-8",
        "count": 98
    },
    {
        "name": "outbound:departure:23-24",
        "count": 98
    },
    {
        "name": "outbound:arrival:10-11",
        "count": 99
    },
    {
        "name": "inbound:afternoon",
        "count": 149
    },
    {
        "name": "inbound:evening",
        "count": 22
    },
    {
        "name": "inbound:friday",
        "count": 59
    },
    {
        "name": "inbound:1stop",
        "count": 8
    },
    {
        "name": "inbound:duration:7-8",
        "count": 5
    },
    {
        "name": "inbound:departure:17-18",
        "count": 8
    },
    {
        "name": "inbound:arrival:22-23",
        "count": 26
    },
    {
        "name": "outbound:tuesday",
        "count": 217
    },
    {
        "name": "inbound:thursday",
        "count": 119
    },
    {
        "name": "inbound:2stops",
        "count": 321
    },
    {
        "name": "inbound:duration:31-32",
        "count": 1
    },
    {
        "name": "inbound:departure:18-19",
        "count": 2
    },
    {
        "name": "inbound:arrival:23-24",
        "count": 70
    },
    {
        "name": "inbound:morning",
        "count": 254
    },
    {
        "name": "inbound:duration:15-16",
        "count": 27
    },
    {
        "name": "inbound:departure:8-9",
        "count": 61
    },
    {
        "name": "inbound:arrival:20-21",
        "count": 69
    },
    {
        "name": "inbound:duration:20-21",
        "count": 3
    },
    {
        "name": "inbound:departure:6-7",
        "count": 53
    },
    {
        "name": "outbound:morning",
        "count": 205
    },
    {
        "name": "outbound:2stops",
        "count": 231
    },
    {
        "name": "outbound:duration:13-14",
        "count": 44
    },
    {
        "name": "outbound:departure:7-8",
        "count": 52
    },
    {
        "name": "outbound:arrival:23-24",
        "count": 22
    },
    {
        "name": "inbound:wednesday",
        "count": 151
    },
    {
        "name": "inbound:duration:12-13",
        "count": 60
    },
    {
        "name": "inbound:departure:13-14",
        "count": 81
    },
    {
        "name": "inbound:duration:13-14",
        "count": 53
    },
    {
        "name": "inbound:departure:14-15",
        "count": 40
    },
    {
        "name": "inbound:arrival:0-1",
        "count": 47
    },
    {
        "name": "outbound:departure:6-7",
        "count": 66
    },
    {
        "name": "outbound:arrival:22-23",
        "count": 18
    },
    {
        "name": "inbound:duration:16-17",
        "count": 19
    },
    {
        "name": "inbound:departure:10-11",
        "count": 6
    },
    {
        "name": "outbound:duration:12-13",
        "count": 30
    },
    {
        "name": "outbound:arrival:21-22",
        "count": 118
    },
    {
        "name": "inbound:departure:7-8",
        "count": 13
    },
    {
        "name": "inbound:duration:22-23",
        "count": 1
    },
    {
        "name": "inbound:arrival:9-10",
        "count": 1
    },
    {
        "name": "inbound:departure:19-20",
        "count": 11
    },
    {
        "name": "inbound:arrival:8-9",
        "count": 4
    },
    {
        "name": "inbound:duration:11-12",
        "count": 66
    },
    {
        "name": "inbound:arrival:16-17",
        "count": 16
    },
    {
        "name": "inbound:duration:14-15",
        "count": 12
    },
    {
        "name": "inbound:arrival:7-8",
        "count": 7
    },
    {
        "name": "inbound:duration:21-22",
        "count": 1
    },
    {
        "name": "inbound:arrival:12-13",
        "count": 4
    },
    {
        "name": "outbound:duration:9-10",
        "count": 67
    },
    {
        "name": "outbound:departure:9-10",
        "count": 52
    },
    {
        "name": "outbound:afternoon",
        "count": 20
    },
    {
        "name": "outbound:departure:17-18",
        "count": 16
    },
    {
        "name": "outbound:arrival:9-10",
        "count": 24
    },
    {
        "name": "inbound:duration:17-18",
        "count": 9
    },
    {
        "name": "outbound:duration:17-18",
        "count": 1
    },
    {
        "name": "outbound:departure:21-22",
        "count": 1
    },
    {
        "name": "outbound:arrival:17-18",
        "count": 14
    },
    {
        "name": "inbound:departure:11-12",
        "count": 18
    },
    {
        "name": "inbound:duration:18-19",
        "count": 4
    },
    {
        "name": "outbound:duration:11-12",
        "count": 37
    },
    {
        "name": "outbound:arrival:20-21",
        "count": 22
    },
    {
        "name": "inbound:arrival:15-16",
        "count": 52
    },
    {
        "name": "inbound:duration:10-11",
        "count": 37
    },
    {
        "name": "inbound:departure:12-13",
        "count": 5
    },
    {
        "name": "outbound:duration:8-9",
        "count": 39
    },
    {
        "name": "outbound:departure:10-11",
        "count": 24
    },
    {
        "name": "outbound:departure:8-9",
        "count": 10
    },
    {
        "name": "inbound:arrival:18-19",
        "count": 5
    },
    {
        "name": "outbound:duration:10-11",
        "count": 9
    },
    {
        "name": "outbound:departure:20-21",
        "count": 5
    },
    {
        "name": "outbound:departure:18-19",
        "count": 2
    },
    {
        "name": "inbound:arrival:21-22",
        "count": 11
    },
    {
        "name": "inbound:arrival:14-15",
        "count": 9
    },
    {
        "name": "inbound:duration:19-20",
        "count": 1
    },
    {
        "name": "inbound:departure:15-16",
        "count": 4
    },
    {
        "name": "outbound:duration:18-19",
        "count": 2
    },
    {
        "name": "outbound:arrival:15-16",
        "count": 1
    },
    {
        "name": "inbound:arrival:17-18",
        "count": 5
    },
    {
        "name": "inbound:duration:9-10",
        "count": 30
    },
    {
        "name": "outbound:departure:11-12",
        "count": 1
    },
    {
        "name": "outbound:duration:14-15",
        "count": 2
    },
    {
        "name": "outbound:departure:15-16",
        "count": 2
    },
    {
        "name": "outbound:arrival:19-20",
        "count": 11
    },
    {
        "name": "inbound:departure:9-10",
        "count": 17
    },
    {
        "name": "inbound:departure:21-22",
        "count": 1
    },
    {
        "name": "inbound:arrival:10-11",
        "count": 1
    },
    {
        "name": "inbound:departure:16-17",
        "count": 9
    },
    {
        "name": "inbound:arrival:13-14",
        "count": 2
    }
],
"location_hashtags": [
    "sightseeing&culture",
    "activities"
]
}

… which Bubble parses as (I stitched three screenshots together to capture it all):



My goal is to get the “all_prices” data no matter what the range labels are or how many buckets there are.

Thanks in advance for any/all help.

1 Like

Hi @edd

Here you are. Hope it helps.

This is what’s happening behind the curtain.

Two API calls. One Action API call to Skypicker and one Data call to a Bubble WF endpoint.

  1. You call your Bubble WF API endpoint that will call Skypicker’s API.
  2. Process the result from Skypicker by doing some simple trickery on the json received with Misha’s Toolbox.
  3. Return the full and modified response you need.

Demo Page:

Editor:

There are other ways of working around this limitation but I presented you the one that allows you to keep working through the API connector in case you want to modify the call.

3 Likes

@JonL, thank you so much for taking the time to explain this workaround in depth — prime example of a powerful community in action. And now I’m pretty sure I can figure this out next time the “API Connection Strikes Again” (to use @keith’s term).

2 Likes